Unverified Commit f3049c77 authored by MH Johnson's avatar MH Johnson Committed by GitHub

add navigation bar component (#83047)

parent 61bce1d8
...@@ -99,6 +99,8 @@ export 'src/material/material_localizations.dart'; ...@@ -99,6 +99,8 @@ export 'src/material/material_localizations.dart';
export 'src/material/material_state.dart'; export 'src/material/material_state.dart';
export 'src/material/material_state_mixin.dart'; export 'src/material/material_state_mixin.dart';
export 'src/material/mergeable_material.dart'; export 'src/material/mergeable_material.dart';
export 'src/material/navigation_bar.dart';
export 'src/material/navigation_bar_theme.dart';
export 'src/material/navigation_rail.dart'; export 'src/material/navigation_rail.dart';
export 'src/material/navigation_rail_theme.dart'; export 'src/material/navigation_rail_theme.dart';
export 'src/material/no_splash.dart'; export 'src/material/no_splash.dart';
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'elevation_overlay.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'material_state.dart';
import 'navigation_bar_theme.dart';
import 'theme.dart';
import 'tooltip.dart';
/// Material 3 Navigation Bar component.
///
/// Navigation bars offer a persistent and convenient way to switch between
/// primary destinations in an app.
///
/// This widget does not adjust its size with the [ThemeData.visualDensity].
///
/// The [MediaQueryData.textScaleFactor] does not adjust the size of this widget but
/// rather the size of the [Tooltip]s displayed on long presses of the
/// destinations.
///
/// The style for the icons and text are not affected by parent
/// [DefaultTextStyle]s or [IconTheme]s but rather controlled by parameters or
/// the [NavigationBarThemeData].
///
/// This widget holds a collection of destinations (usually
/// [NavigationDestination]s).
///
/// Usage:
/// ```dart
/// Scaffold(
/// bottomNavigationBar: NavigationBar(
/// onDestinationSelected: (int index) {
/// setState(() { _currentPageIndex = index; }),
/// },
/// selectedIndex: _currentPageIndex,
/// destinations: [
/// NavigationDestination(
/// icon: Icon(Icons.explore),
/// label: 'Explore',
/// ),
/// NavigationDestination(
/// icon: Icon(Icons.commute),
/// label: 'Commute',
/// ),
/// NavigationDestination(
/// selectedIcon: Icon(Icons.bookmark),
/// icon: Icon(Icons.bookmark_border),
/// label: 'Saved',
/// ),
/// ],
/// ),
/// ),
/// ```
class NavigationBar extends StatelessWidget {
/// Creates a Material 3 Navigation Bar component.
///
/// The value of [destinations] must be a list of two or more
/// [NavigationDestination] values.
const NavigationBar({
Key? key,
this.animationDuration,
this.selectedIndex = 0,
required this.destinations,
this.onDestinationSelected,
this.backgroundColor,
this.height,
this.labelBehavior,
}) : assert(destinations != null && destinations.length >= 2),
assert(0 <= selectedIndex && selectedIndex < destinations.length),
super(key: key);
/// Determines the transition time for each destination as it goes between
/// selected and unselected.
final Duration? animationDuration;
/// Determines which one of the [destinations] is currently selected.
///
/// When this is updated, the destination (from [destinations]) at
/// [selectedIndex] goes from unselected to selected.
final int selectedIndex;
/// The list of destinations (usually [NavigationDestination]s) in this
/// [NavigationBar].
///
/// When [selectedIndex] is updated, the destination from this list at
/// [selectedIndex] will animate from 0 (unselected) to 1.0 (selected). When
/// the animation is increasing or completed, the destination is considered
/// selected, when the animation is decreasing or dismissed, the destination
/// is considered unselected.
final List<Widget> destinations;
/// Called when one of the [destinations] is selected.
///
/// This callback usually updates the int passed to [selectedIndex].
///
/// Upon updating [selectedIndex], the [NavigationBar] will be rebuilt.
final ValueChanged<int>? onDestinationSelected;
/// The color of the [NavigationBar] itself.
///
/// If null, [NavigationBarThemeData.backgroundColor] is used. If that
/// is also null, the default blends [ColorScheme.surface] and
/// [ColorScheme.onSurface] using an [ElevationOverlay].
final Color? backgroundColor;
/// The height of the [NavigationBar] itself.
///
/// If this is used in [Scaffold.bottomNavigationBar] and the scaffold is
/// full-screen, the safe area padding is also added to the height
/// automatically.
///
/// The height does not adjust with [ThemeData.visualDensity] or
/// [MediaQueryData.textScaleFactor] as this component loses usability at
/// larger and smaller sizes due to the truncating of labels or smaller tap
/// targets.
///
/// If null, [NavigationBarThemeData.height] is used. If that
/// is also null, the default is 80.
final double? height;
/// Defines how the [destinations]' labels will be laid out and when they'll
/// be displayed.
///
/// Can be used to show all labels, show only the selected label, or hide all
/// labels.
///
/// If null, [NavigationBarThemeData.labelBehavior] is used. If that
/// is also null, the default is
/// [NavigationDestinationLabelBehavior.alwaysShow].
final NavigationDestinationLabelBehavior? labelBehavior;
VoidCallback _handleTap(int index) {
return onDestinationSelected != null
? () => onDestinationSelected!(index)
: () {};
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
final double effectiveHeight = height ?? navigationBarTheme.height ?? 80;
final NavigationDestinationLabelBehavior effectiveLabelBehavior = labelBehavior
?? navigationBarTheme.labelBehavior
?? NavigationDestinationLabelBehavior.alwaysShow;
final double additionalBottomPadding = MediaQuery.of(context).padding.bottom;
return Material(
// With Material 3, the NavigationBar uses an overlay blend for the
// default color regardless of light/dark mode.
color: backgroundColor
?? navigationBarTheme.backgroundColor
?? ElevationOverlay.colorWithOverlay(colorScheme.surface, colorScheme.onSurface, 3.0),
child: Padding(
padding: EdgeInsets.only(bottom: additionalBottomPadding),
child: MediaQuery.removePadding(
context: context,
removeBottom: true,
child: SizedBox(
height: effectiveHeight,
child: Row(
children: <Widget>[
for (int i = 0; i < destinations.length; i++)
Expanded(
child: _SelectableAnimatedBuilder(
duration: animationDuration ?? const Duration(milliseconds: 500),
isSelected: i == selectedIndex,
builder: (BuildContext context, Animation<double> animation) {
return _NavigationDestinationInfo(
index: i,
totalNumberOfDestinations: destinations.length,
selectedAnimation: animation,
labelBehavior: effectiveLabelBehavior,
onTap: _handleTap(i),
child: destinations[i],
);
},
),
),
],
),
),
),
),
);
}
}
/// Specifies when each [NavigationDestination]'s label should appear.
///
/// This is used to determine the behavior of [NavigationBar]'s destinations.
enum NavigationDestinationLabelBehavior {
/// Always shows all of the labels under each navigation bar destination,
/// selected and unselected.
alwaysShow,
/// Never shows any of the labels under the navigation bar destinations,
/// regardless of selected vs unselected.
alwaysHide,
/// Only shows the labels of the selected navigation bar destination.
///
/// When a destination is unselected, the label will be faded out, and the
/// icon will be centered.
///
/// When a destination is selected, the label will fade in and the label and
/// icon will slide up so that they are both centered.
onlyShowSelected,
}
/// Destination Widget for displaying Icons + labels in the Material 3
/// Navigation Bars through [NavigationBar.destinations].
///
/// The destination this widget creates will look something like this:
/// =======
/// |
/// | ☆ <-- [icon] (or [selectedIcon])
/// | text <-- [label]
/// |
/// =======
class NavigationDestination extends StatelessWidget {
/// Creates a navigation bar destination with an icon and a label, to be used
/// in the [NavigationBar.destinations].
const NavigationDestination({
Key? key,
required this.icon,
this.selectedIcon,
required this.label,
this.tooltip,
}) : super(key: key);
/// The [Widget] (usually an [Icon]) that's displayed for this
/// [NavigationDestination].
///
/// The icon will use [NavigationBarThemeData.iconTheme]. If this is
/// null, the default [IconThemeData] would use a size of 24.0 and
/// [ColorScheme.onSurface].
final Widget icon;
/// The optional [Widget] (usually an [Icon]) that's displayed when this
/// [NavigationDestination] is selected.
///
/// If [selectedIcon] is non-null, the destination will fade from
/// [icon] to [selectedIcon] when this destination goes from unselected to
/// selected.
///
/// The icon will use [NavigationBarThemeData.iconTheme] with
/// [MaterialState.selected]. If this is null, the default [IconThemeData]
/// would use a size of 24.0 and [ColorScheme.onSurface].
final Widget? selectedIcon;
/// The text label that appears below the icon of this
/// [NavigationDestination].
///
/// The accompanying [Text] widget will use
/// [NavigationBarThemeData.labelTextStyle]. If this are null, the default
/// text style would use [TextTheme.overline] with [ColorScheme.onSurface].
final String label;
/// The text to display in the tooltip for this [NavigationDestination], when
/// the user long presses the destination.
///
/// If [tooltip] is an empty string, no tooltip will be used.
///
/// Defaults to null, in which case the [label] text will be used.
final String? tooltip;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
final Animation<double> animation = _NavigationDestinationInfo.of(context).selectedAnimation;
return _NavigationDestinationBuilder(
label: label,
tooltip: tooltip,
buildIcon: (BuildContext context) {
final IconThemeData defaultIconTheme = IconThemeData(
size: 24,
color: colorScheme.onSurface,
);
final Widget selectedIconWidget = IconTheme.merge(
data: navigationBarTheme.iconTheme?.resolve(<MaterialState>{MaterialState.selected}) ?? defaultIconTheme,
child: selectedIcon ?? icon,
);
final Widget unselectedIconWidget = IconTheme.merge(
data: navigationBarTheme.iconTheme?.resolve(<MaterialState>{}) ?? defaultIconTheme,
child: icon,
);
return Stack(
alignment: Alignment.center,
children: <Widget>[
_NavigationIndicator(
animation: animation,
color: navigationBarTheme.indicatorColor,
),
_StatusTransitionWidgetBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return animation.isForwardOrCompleted
? selectedIconWidget
: unselectedIconWidget;
},
),
],
);
},
buildLabel: (BuildContext context) {
final TextStyle? defaultTextStyle = theme.textTheme.overline?.copyWith(
color: colorScheme.onSurface,
);
final TextStyle? effectiveSelectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(<MaterialState>{MaterialState.selected}) ?? defaultTextStyle;
final TextStyle? effectiveUnselectedLabelTextStyle = navigationBarTheme.labelTextStyle?.resolve(<MaterialState>{}) ?? defaultTextStyle;
return Padding(
padding: const EdgeInsets.only(top: 4),
child: _ClampTextScaleFactor(
// Don't scale labels of destinations, instead, tooltip text will
// upscale.
upperLimit: 1,
child: Text(
label,
style: animation.isForwardOrCompleted
? effectiveSelectedLabelTextStyle
: effectiveUnselectedLabelTextStyle,
),
),
);
},
);
}
}
/// Widget that handles the semantics and layout of a navigation bar
/// destination.
///
/// Prefer [NavigationDestination] over this widget, as it is a simpler
/// (although less customizable) way to get navigation bar destinations.
///
/// The icon and label of this destination are built with [buildIcon] and
/// [buildLabel]. They should build the unselected and selected icon and label
/// according to [_NavigationDestinationInfo.selectedAnimation], where an
/// animation value of 0 is unselected and 1 is selected.
///
/// See [NavigationDestination] for an example.
class _NavigationDestinationBuilder extends StatelessWidget {
/// Builds a destination (icon + label) to use in a Material 3 [NavigationBar].
const _NavigationDestinationBuilder({
Key? key,
required this.buildIcon,
required this.buildLabel,
required this.label,
this.tooltip,
}) : super(key: key);
/// Builds the icon for an destination in a [NavigationBar].
///
/// To animate between unselected and selected, build the icon based on
/// [_NavigationDestinationInfo.selectedAnimation]. When the animation is 0,
/// the destination is unselected, when the animation is 1, the destination is
/// selected.
///
/// The destination is considered selected as soon as the animation is
/// increasing or completed, and it is considered unselected as soon as the
/// animation is decreasing or dismissed.
final WidgetBuilder buildIcon;
/// Builds the label for an destination in a [NavigationBar].
///
/// To animate between unselected and selected, build the icon based on
/// [_NavigationDestinationInfo.selectedAnimation]. When the animation is
/// 0, the destination is unselected, when the animation is 1, the destination
/// is selected.
///
/// The destination is considered selected as soon as the animation is
/// increasing or completed, and it is considered unselected as soon as the
/// animation is decreasing or dismissed.
final WidgetBuilder buildLabel;
/// The text value of what is in the label widget, this is required for
/// semantics so that screen readers and tooltips can read the proper label.
final String label;
/// The text to display in the tooltip for this [NavigationDestination], when
/// the user long presses the destination.
///
/// If [tooltip] is an empty string, no tooltip will be used.
///
/// Defaults to null, in which case the [label] text will be used.
final String? tooltip;
@override
Widget build(BuildContext context) {
final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
return _NavigationBarDestinationSemantics(
child: _NavigationBarDestinationTooltip(
message: tooltip ?? label,
child: InkWell(
highlightColor: Colors.transparent,
onTap: info.onTap,
child: Row(
children: <Widget>[
Expanded(
child: _NavigationBarDestinationLayout(
icon: buildIcon(context),
label: buildLabel(context),
),
),
],
),
),
),
);
}
}
/// Inherited widget for passing data from the [NavigationBar] to the
/// [NavigationBar.destinations] children widgets.
///
/// Useful for building navigation destinations using:
/// `_NavigationDestinationInfo.of(context)`.
class _NavigationDestinationInfo extends InheritedWidget {
/// Adds the information needed to build a navigation destination to the
/// [child] and descendants.
const _NavigationDestinationInfo({
Key? key,
required this.index,
required this.totalNumberOfDestinations,
required this.selectedAnimation,
required this.labelBehavior,
required this.onTap,
required Widget child,
}) : super(key: key, child: child);
/// Which destination index is this in the navigation bar.
///
/// For example:
/// ```dart
/// NavigationBar(
/// destinations: [
/// NavigationDestination(), // This is destination index 0.
/// NavigationDestination(), // This is destination index 1.
/// NavigationDestination(), // This is destination index 2.
/// ]
/// )
/// ```
///
/// This is required for semantics, so that each destination can have a label
/// "Tab 1 of 3", for example.
final int index;
/// How many total destinations are are in this navigation bar.
///
/// This is required for semantics, so that each destination can have a label
/// "Tab 1 of 4", for example.
final int totalNumberOfDestinations;
/// Indicates whether or not this destination is selected, from 0 (unselected)
/// to 1 (selected).
final Animation<double> selectedAnimation;
/// Determines the behavior for how the labels will layout.
///
/// Can be used to show all labels (the default), show only the selected
/// label, or hide all labels.
final NavigationDestinationLabelBehavior labelBehavior;
/// The callback that should be called when this destination is tapped.
///
/// This is computed by calling [NavigationBar.onDestinationSelected]
/// with [index] passed in.
final VoidCallback onTap;
/// Returns a non null [_NavigationDestinationInfo].
///
/// This will return an error if called with no [_NavigationDestinationInfo]
/// ancestor.
///
/// Used by widgets that are implementing a navigation destination info to
/// get information like the selected animation and destination number.
static _NavigationDestinationInfo of(BuildContext context) {
final _NavigationDestinationInfo? result = context.dependOnInheritedWidgetOfExactType<_NavigationDestinationInfo>();
assert(
result != null,
'Navigation destinations need a _NavigationDestinationInfo parent, '
'which is usually provided by NavigationBar.',
);
return result!;
}
@override
bool updateShouldNotify(_NavigationDestinationInfo oldWidget) {
return index != oldWidget.index
|| totalNumberOfDestinations != oldWidget.totalNumberOfDestinations
|| selectedAnimation != oldWidget.selectedAnimation
|| labelBehavior != oldWidget.labelBehavior
|| onTap != oldWidget.onTap;
}
}
/// Selection Indicator for the Material 3 Navigation Bar component.
///
/// When [animation] is 0, the indicator is not present. As [animation] grows
/// from 0 to 1, the indicator scales in on the x axis.
///
/// Useful in a [Stack] widget behind the icons in the Material 3 Navigation Bar
/// to illuminate the selected destination.
class _NavigationIndicator extends StatelessWidget {
/// Builds an indicator, usually used in a stack behind the icon of a
/// navigation bar destination.
const _NavigationIndicator({
Key? key,
required this.animation,
this.color,
}) : super(key: key);
/// Determines the scale of the indicator.
///
/// When [animation] is 0, the indicator is not present. The indicator scales
/// in as [animation] grows from 0 to 1.
final Animation<double> animation;
/// The fill color of this indicator.
///
/// If null, defaults to [ColorScheme.secondary].
final Color? color;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
// The scale should be 0 when the animation is unselected, as soon as
// the animation starts, the scale jumps to 40%, and then animates to
// 100% along a curve.
final double scale = animation.isDismissed
? 0.0
: Tween<double>(begin: .4, end: 1.0).transform(
CurveTween(curve: Curves.easeInOutCubicEmphasized).transform(animation.value));
return Transform(
alignment: Alignment.center,
// Scale in the X direction only.
transform: Matrix4.diagonal3Values(
scale,
1.0,
1.0,
),
child: child,
);
},
// Fade should be a 100ms animation whenever the parent animation changes
// direction.
child: _StatusTransitionWidgetBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return _SelectableAnimatedBuilder(
isSelected: animation.isForwardOrCompleted,
duration: const Duration(milliseconds: 100),
alwaysDoFullAnimation: true,
builder: (BuildContext context, Animation<double> fadeAnimation) {
return FadeTransition(
opacity: fadeAnimation,
child: Container(
width: 64.0,
height: 32.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
color: color ?? colorScheme.secondary.withOpacity(.24),
),
),
);
},
);
},
),
);
}
}
/// Widget that handles the layout of the icon + label in a navigation bar
/// destination, based on [_NavigationDestinationInfo.labelBehavior] and
/// [_NavigationDestinationInfo.selectedAnimation].
///
/// Depending on the [_NavigationDestinationInfo.labelBehavior], the labels
/// will shift and fade accordingly.
class _NavigationBarDestinationLayout extends StatelessWidget {
/// Builds a widget to layout an icon + label for a destination in a Material
/// 3 [NavigationBar].
const _NavigationBarDestinationLayout({
Key? key,
required this.icon,
required this.label,
}) : super(key: key);
/// The icon widget that sits on top of the label.
///
/// See [NavigationDestination.icon].
final Widget icon;
/// The label widget that sits below the icon.
///
/// This widget will sometimes be faded out, depending on
/// [_NavigationDestinationInfo.selectedAnimation].
///
/// See [NavigationDestination.label].
final Widget label;
static final Key _iconKey = UniqueKey();
static final Key _labelKey = UniqueKey();
@override
Widget build(BuildContext context) {
return _DestinationLayoutAnimationBuilder(
builder: (BuildContext context, Animation<double> animation) {
return CustomMultiChildLayout(
delegate: _NavigationDestinationLayoutDelegate(
animation: animation,
),
children: <Widget>[
LayoutId(
id: _NavigationDestinationLayoutDelegate.iconId,
child: RepaintBoundary(
key: _iconKey,
child: icon,
),
),
LayoutId(
id: _NavigationDestinationLayoutDelegate.labelId,
child: FadeTransition(
alwaysIncludeSemantics: true,
opacity: animation,
child: RepaintBoundary(
key: _labelKey,
child: label,
),
),
),
],
);
},
);
}
}
/// Determines the appropriate [Curve] and [Animation] to use for laying out the
/// [NavigationDestination], based on
/// [_NavigationDestinationInfo.labelBehavior].
///
/// The animation controlling the position and fade of the labels differs
/// from the selection animation, depending on the
/// [NavigationDestinationLabelBehavior]. This widget determines what
/// animation should be used for the position and fade of the labels.
class _DestinationLayoutAnimationBuilder extends StatelessWidget {
/// Builds a child with the appropriate animation [Curve] based on the
/// [_NavigationDestinationInfo.labelBehavior].
const _DestinationLayoutAnimationBuilder({Key? key, required this.builder}) : super(key: key);
/// Builds the child of this widget.
///
/// The [Animation] will be the appropriate [Animation] to use for the layout
/// and fade of the [NavigationDestination], either a curve, always
/// showing (1), or always hiding (0).
final Widget Function(BuildContext, Animation<double>) builder;
@override
Widget build(BuildContext context) {
final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
switch (info.labelBehavior) {
case NavigationDestinationLabelBehavior.alwaysShow:
return builder(context, kAlwaysCompleteAnimation);
case NavigationDestinationLabelBehavior.alwaysHide:
return builder(context, kAlwaysDismissedAnimation);
case NavigationDestinationLabelBehavior.onlyShowSelected:
return _CurvedAnimationBuilder(
animation: info.selectedAnimation,
curve: Curves.easeInOutCubicEmphasized,
reverseCurve: Curves.easeInOutCubicEmphasized.flipped,
builder: (BuildContext context, Animation<double> curvedAnimation) {
return builder(context, curvedAnimation);
},
);
}
}
}
/// Semantics widget for a navigation bar destination.
///
/// Requires a [_NavigationDestinationInfo] parent (normally provided by the
/// [NavigationBar] by default).
///
/// Provides localized semantic labels to the destination, for example, it will
/// read "Home, Tab 1 of 3".
///
/// Used by [_NavigationDestinationBuilder].
class _NavigationBarDestinationSemantics extends StatelessWidget {
/// Adds the the appropriate semantics for navigation bar destinations to the
/// [child].
const _NavigationBarDestinationSemantics({
Key? key,
required this.child,
}) : super(key: key);
/// The widget that should receive the destination semantics.
final Widget child;
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final _NavigationDestinationInfo destinationInfo = _NavigationDestinationInfo.of(context);
// The AnimationStatusBuilder will make sure that the semantics update to
// "selected" when the animation status changes.
return _StatusTransitionWidgetBuilder(
animation: destinationInfo.selectedAnimation,
builder: (BuildContext context, Widget? child) {
return Semantics(
selected: destinationInfo.selectedAnimation.isForwardOrCompleted,
container: true,
child: child,
);
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
child,
Semantics(
label: localizations.tabLabel(
tabIndex: destinationInfo.index + 1,
tabCount: destinationInfo.totalNumberOfDestinations,
),
),
],
),
);
}
}
/// Tooltip widget for use in a [NavigationBar].
///
/// It appears just above the navigation bar when one of the destinations is
/// long pressed.
class _NavigationBarDestinationTooltip extends StatelessWidget {
/// Adds a tooltip to the [child] widget.
const _NavigationBarDestinationTooltip({
Key? key,
required this.message,
required this.child,
}) : super(key: key);
/// The text that is rendered in the tooltip when it appears.
///
/// If [message] is null, no tooltip will be used.
final String? message;
/// The widget that, when pressed, will show a tooltip.
final Widget child;
@override
Widget build(BuildContext context) {
if (message == null) {
return child;
}
return Tooltip(
message: message!,
// TODO(johnsonmh): Make this value configurable/themable.
verticalOffset: 42,
excludeFromSemantics: true,
preferBelow: false,
child: child,
);
}
}
/// Custom layout delegate for shifting navigation bar destinations.
///
/// This will lay out the icon + label according to the [animation].
///
/// When the [animation] is 0, the icon will be centered, and the label will be
/// positioned directly below it.
///
/// When the [animation] is 1, the label will still be positioned directly below
/// the icon, but the icon + label combination will be centered.
///
/// Used in a [CustomMultiChildLayout] widget in the
/// [_NavigationDestinationBuilder].
class _NavigationDestinationLayoutDelegate extends MultiChildLayoutDelegate {
_NavigationDestinationLayoutDelegate({required this.animation}) : super(relayout: animation);
/// The selection animation that indicates whether or not this destination is
/// selected.
///
/// See [_NavigationDestinationInfo.selectedAnimation].
final Animation<double> animation;
/// ID for the icon widget child.
///
/// This is used by the [LayoutId] when this delegate is used in a
/// [CustomMultiChildLayout].
///
/// See [_NavigationDestinationBuilder].
static const int iconId = 1;
/// ID for the label widget child.
///
/// This is used by the [LayoutId] when this delegate is used in a
/// [CustomMultiChildLayout].
///
/// See [_NavigationDestinationBuilder].
static const int labelId = 2;
@override
void performLayout(Size size) {
double halfWidth(Size size) => size.width / 2;
double halfHeight(Size size) => size.height / 2;
final Size iconSize = layoutChild(iconId, BoxConstraints.loose(size));
final Size labelSize = layoutChild(labelId, BoxConstraints.loose(size));
final double yPositionOffset = Tween<double>(
// When unselected, the icon is centered vertically.
begin: halfHeight(iconSize),
// When selected, the icon and label are centered vertically.
end: halfHeight(iconSize) + halfHeight(labelSize),
).transform(animation.value);
final double iconYPosition = halfHeight(size) - yPositionOffset;
// Position the icon.
positionChild(
iconId,
Offset(
// Center the icon horizontally.
halfWidth(size) - halfWidth(iconSize),
iconYPosition,
),
);
// Position the label.
positionChild(
labelId,
Offset(
// Center the label horizontally.
halfWidth(size) - halfWidth(labelSize),
// Label always appears directly below the icon.
iconYPosition + iconSize.height,
),
);
}
@override
bool shouldRelayout(_NavigationDestinationLayoutDelegate oldDelegate) {
return oldDelegate.animation != animation;
}
}
/// Utility Widgets
/// Clamps [MediaQueryData.textScaleFactor] so that if it is greater than
/// [upperLimit] or less than [lowerLimit], [upperLimit] or [lowerLimit] will be
/// used instead for the [child] widget.
///
/// Example:
/// ```
/// _ClampTextScaleFactor(
/// upperLimit: 2.0,
/// child: Text('Foo'), // If textScaleFactor is 3.0, this will only scale 2x.
/// )
/// ```
class _ClampTextScaleFactor extends StatelessWidget {
/// Clamps the text scale factor of descendants by modifying the [MediaQuery]
/// surrounding [child].
const _ClampTextScaleFactor({
Key? key,
this.lowerLimit = 0,
this.upperLimit = double.infinity,
required this.child,
}) : super(key: key);
/// The minimum amount that the text scale factor should be for the [child]
/// widget.
///
/// If this is `.5`, the textScaleFactor for child widgets will never be
/// smaller than `.5`.
final double lowerLimit;
/// The maximum amount that the text scale factor should be for the [child]
/// widget.
///
/// If this is `1.5`, the textScaleFactor for child widgets will never be
/// greater than `1.5`.
final double upperLimit;
/// The [Widget] that should have its (and its descendants) text scale factor
/// clamped.
final Widget child;
@override
Widget build(BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: MediaQuery.of(context).textScaleFactor.clamp(
lowerLimit,
upperLimit,
),
),
child: child,
);
}
}
/// Widget that listens to an animation, and rebuilds when the animation changes
/// [AnimationStatus].
///
/// This can be more efficient than just using an [AnimatedBuilder] when you
/// only need to rebuild when the [Animation.status] changes, since
/// [AnimatedBuilder] rebuilds every time the animation ticks.
class _StatusTransitionWidgetBuilder extends StatusTransitionWidget {
/// Creates a widget that rebuilds when the given animation changes status.
const _StatusTransitionWidgetBuilder({
Key? key,
required Animation<double> animation,
required this.builder,
this.child,
}) : super(animation: animation, key: key);
/// Called every time the [animation] changes [AnimationStatus].
final TransitionBuilder builder;
/// The child widget to pass to the [builder].
///
/// If a [builder] callback's return value contains a subtree that does not
/// depend on the animation, it's more efficient to build that subtree once
/// instead of rebuilding it on every animation status change.
///
/// Using this pre-built child is entirely optional, but can improve
/// performance in some cases and is therefore a good practice.
///
/// See: [AnimatedBuilder.child]
final Widget? child;
@override
Widget build(BuildContext context) => builder(context, child);
}
/// Builder widget for widgets that need to be animated from 0 (unselected) to
/// 1.0 (selected).
///
/// This widget creates and manages an [AnimationController] that it passes down
/// to the child through the [builder] function.
///
/// When [isSelected] is `true`, the animation controller will animate from
/// 0 to 1 (for [duration] time).
///
/// When [isSelected] is `false`, the animation controller will animate from
/// 1 to 0 (for [duration] time).
///
/// If [isSelected] is updated while the widget is animating, the animation will
/// be reversed until it is either 0 or 1 again. If [alwaysDoFullAnimation] is
/// true, the animation will reset to 0 or 1 before beginning the animation, so
/// that the full animation is done.
///
/// Usage:
/// ```dart
/// _SelectableAnimatedBuilder(
/// isSelected: _isDrawerOpen,
/// builder: (context, animation) {
/// return AnimatedIcon(
/// icon: AnimatedIcons.menu_arrow,
/// progress: animation,
/// semanticLabel: 'Show menu',
/// );
/// }
/// )
/// ```
class _SelectableAnimatedBuilder extends StatefulWidget {
/// Builds and maintains an [AnimationController] that will animate from 0 to
/// 1 and back depending on when [isSelected] is true.
const _SelectableAnimatedBuilder({
Key? key,
required this.isSelected,
this.duration = const Duration(milliseconds: 200),
this.alwaysDoFullAnimation = false,
required this.builder,
}) : super(key: key);
/// When true, the widget will animate an animation controller from 0 to 1.
///
/// The animation controller is passed to the child widget through [builder].
final bool isSelected;
/// How long the animation controller should animate for when [isSelected] is
/// updated.
///
/// If the animation is currently running and [isSelected] is updated, only
/// the [duration] left to finish the animation will be run.
final Duration duration;
/// If true, the animation will always go all the way from 0 to 1 when
/// [isSelected] is true, and from 1 to 0 when [isSelected] is false, even
/// when the status changes mid animation.
///
/// If this is false and the status changes mid animation, the animation will
/// reverse direction from it's current point.
///
/// Defaults to false.
final bool alwaysDoFullAnimation;
/// Builds the child widget based on the current animation status.
///
/// When [isSelected] is updated to true, this builder will be called and the
/// animation will animate up to 1. When [isSelected] is updated to
/// `false`, this will be called and the animation will animate down to 0.
final Widget Function(BuildContext, Animation<double>) builder;
@override
_SelectableAnimatedBuilderState createState() =>
_SelectableAnimatedBuilderState();
}
/// State that manages the [AnimationController] that is passed to
/// [_SelectableAnimatedBuilder.builder].
class _SelectableAnimatedBuilderState extends State<_SelectableAnimatedBuilder>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.duration = widget.duration;
_controller.value = widget.isSelected ? 1.0 : 0.0;
}
@override
void didUpdateWidget(_SelectableAnimatedBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.duration != widget.duration) {
_controller.duration = widget.duration;
}
if (oldWidget.isSelected != widget.isSelected) {
if (widget.isSelected) {
_controller.forward(from: widget.alwaysDoFullAnimation ? 0 : null);
} else {
_controller.reverse(from: widget.alwaysDoFullAnimation ? 1 : null);
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.builder(
context,
_controller,
);
}
}
/// Watches [animation] and calls [builder] with the appropriate [Curve]
/// depending on the direction of the [animation] status.
///
/// If [animation.status] is forward or complete, [curve] is used. If
/// [animation.status] is reverse or dismissed, [reverseCurve] is used.
///
/// If the [animation] changes direction while it is already running, the curve
/// used will not change, this will keep the animations smooth until it
/// completes.
///
/// This is similar to [CurvedAnimation] except the animation status listeners
/// are removed when this widget is disposed.
class _CurvedAnimationBuilder extends StatefulWidget {
const _CurvedAnimationBuilder({
Key? key,
required this.animation,
required this.curve,
required this.reverseCurve,
required this.builder,
}) : super(key: key);
final Animation<double> animation;
final Curve curve;
final Curve reverseCurve;
final Widget Function(BuildContext, Animation<double>) builder;
@override
_CurvedAnimationBuilderState createState() => _CurvedAnimationBuilderState();
}
class _CurvedAnimationBuilderState extends State<_CurvedAnimationBuilder> {
late AnimationStatus _animationDirection;
AnimationStatus? _preservedDirection;
@override
void initState() {
super.initState();
_animationDirection = widget.animation.status;
_updateStatus(widget.animation.status);
widget.animation.addStatusListener(_updateStatus);
}
@override
void dispose() {
widget.animation.removeStatusListener(_updateStatus);
super.dispose();
}
// Keeps track of the current animation status, as well as the "preserved
// direction" when the animation changes direction mid animation.
//
// The preserved direction is reset when the animation finishes in either
// direction.
void _updateStatus(AnimationStatus status) {
if (_animationDirection != status) {
setState(() {
_animationDirection = status;
});
}
if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) {
setState(() {
_preservedDirection = null;
});
}
if (_preservedDirection == null && (status == AnimationStatus.forward || status == AnimationStatus.reverse)) {
setState(() {
_preservedDirection = status;
});
}
}
@override
Widget build(BuildContext context) {
final bool shouldUseForwardCurve = (_preservedDirection ?? _animationDirection) != AnimationStatus.reverse;
final Animation<double> curvedAnimation = CurveTween(
curve: shouldUseForwardCurve ? widget.curve : widget.reverseCurve,
).animate(widget.animation);
return widget.builder(context, curvedAnimation);
}
}
/// Convenience static extensions on Animation.
extension _AnimationUtils on Animation<double> {
/// Returns `true` if this animation is ticking forward, or has completed,
/// based on [status].
bool get isForwardOrCompleted => status == AnimationStatus.forward || status == AnimationStatus.completed;
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'material_state.dart';
import 'navigation_bar.dart';
import 'theme.dart';
/// Defines default property values for descendant [NavigationBar]
/// widgets.
///
/// Descendant widgets obtain the current [NavigationBarThemeData] object
/// using `NavigationBarTheme.of(context)`. Instances of
/// [NavigationBarThemeData] can be customized with
/// [NavigationBarThemeData.copyWith].
///
/// Typically a [NavigationBarThemeData] is specified as part of the
/// overall [Theme] with [ThemeData.navigationBarTheme]. Alternatively, a
/// [NavigationBarTheme] inherited widget can be used to theme [NavigationBar]s
/// in a subtree of widgets.
///
/// All [NavigationBarThemeData] properties are `null` by default.
/// When null, the [NavigationBar] will provide its own defaults based on the
/// overall [Theme]'s textTheme and colorScheme. See the individual
/// [NavigationBar] properties for details.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
@immutable
class NavigationBarThemeData with Diagnosticable {
/// Creates a theme that can be used for [ThemeData.navigationBarTheme] and
/// [NavigationBarTheme].
const NavigationBarThemeData({
this.height,
this.backgroundColor,
this.indicatorColor,
this.labelTextStyle,
this.iconTheme,
this.labelBehavior,
});
/// Overrides the default value of [NavigationBar.height].
final double? height;
/// Overrides the default value of [NavigationBar.backgroundColor].
final Color? backgroundColor;
/// Overrides the default value of [NavigationBar]'s selection indicator.
final Color? indicatorColor;
/// The style to merge with the default text style for
/// [NavigationDestination] labels.
///
/// You can use this to specify a different style when the label is selected.
final MaterialStateProperty<TextStyle?>? labelTextStyle;
/// The theme to merge with the default icon theme for
/// [NavigationDestination] icons.
///
/// You can use this to specify a different icon theme when the icon is
/// selected.
final MaterialStateProperty<IconThemeData?>? iconTheme;
/// Overrides the default value of [NavigationBar.labelBehavior].
final NavigationDestinationLabelBehavior? labelBehavior;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
NavigationBarThemeData copyWith({
double? height,
Color? backgroundColor,
Color? indicatorColor,
MaterialStateProperty<TextStyle?>? labelTextStyle,
MaterialStateProperty<IconThemeData?>? iconTheme,
NavigationDestinationLabelBehavior? labelBehavior,
}) {
return NavigationBarThemeData(
height: height ?? this.height,
backgroundColor: backgroundColor ?? this.backgroundColor,
indicatorColor: indicatorColor ?? this.indicatorColor,
labelTextStyle: labelTextStyle ?? this.labelTextStyle,
iconTheme: iconTheme ?? this.iconTheme,
labelBehavior: labelBehavior ?? this.labelBehavior,
);
}
/// Linearly interpolate between two navigation rail themes.
///
/// If both arguments are null then null is returned.
///
/// {@macro dart.ui.shadow.lerp}
static NavigationBarThemeData? lerp(NavigationBarThemeData? a, NavigationBarThemeData? b, double t) {
assert(t != null);
if (a == null && b == null)
return null;
return NavigationBarThemeData(
height: lerpDouble(a?.height, b?.height, t),
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
indicatorColor: Color.lerp(a?.indicatorColor, b?.indicatorColor, t),
labelTextStyle: _lerpProperties<TextStyle?>(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp),
iconTheme: _lerpProperties<IconThemeData?>(a?.iconTheme, b?.iconTheme, t, IconThemeData.lerp),
labelBehavior: t < 0.5 ? a?.labelBehavior : b?.labelBehavior,
);
}
@override
int get hashCode {
return hashValues(
height,
backgroundColor,
indicatorColor,
labelTextStyle,
iconTheme,
labelBehavior,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is NavigationBarThemeData
&& other.height == height
&& other.backgroundColor == backgroundColor
&& other.indicatorColor == indicatorColor
&& other.labelTextStyle == labelTextStyle
&& other.iconTheme == iconTheme
&& other.labelBehavior == labelBehavior;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('height', height, defaultValue: null));
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
properties.add(ColorProperty('indicatorColor', indicatorColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>('labelTextStyle', labelTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<IconThemeData?>>('iconTheme', iconTheme, defaultValue: null));
properties.add(DiagnosticsProperty<NavigationDestinationLabelBehavior>('labelBehavior', labelBehavior, defaultValue: null));
}
static MaterialStateProperty<T>? _lerpProperties<T>(
MaterialStateProperty<T>? a,
MaterialStateProperty<T>? b,
double t,
T Function(T?, T?, double) lerpFunction,
) {
// Avoid creating a _LerpProperties object for a common case.
if (a == null && b == null)
return null;
return _LerpProperties<T>(a, b, t, lerpFunction);
}
}
class _LerpProperties<T> implements MaterialStateProperty<T> {
const _LerpProperties(this.a, this.b, this.t, this.lerpFunction);
final MaterialStateProperty<T>? a;
final MaterialStateProperty<T>? b;
final double t;
final T Function(T?, T?, double) lerpFunction;
@override
T resolve(Set<MaterialState> states) {
final T? resolvedA = a?.resolve(states);
final T? resolvedB = b?.resolve(states);
return lerpFunction(resolvedA, resolvedB, t);
}
}
/// An inherited widget that defines visual properties for [NavigationBar]s and
/// [NavigationDestination]s in this widget's subtree.
///
/// Values specified here are used for [NavigationBar] properties that are not
/// given an explicit non-null value.
///
/// See also:
///
/// * [ThemeData.navigationBarTheme], which describes the
/// [NavigationBarThemeData] in the overall theme for the application.
class NavigationBarTheme extends InheritedTheme {
/// Creates a navigation rail theme that controls the
/// [NavigationBarThemeData] properties for a [NavigationBar].
///
/// The data argument must not be null.
const NavigationBarTheme({
Key? key,
required this.data,
required Widget child,
}) : assert(data != null), super(key: key, child: child);
/// Specifies the background color, label text style, icon theme, and label
/// type values for descendant [NavigationBar] widgets.
final NavigationBarThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [NavigationBarTheme] widget, then
/// [ThemeData.navigationBarTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// NavigationBarTheme theme = NavigationBarTheme.of(context);
/// ```
static NavigationBarThemeData of(BuildContext context) {
final NavigationBarTheme? navigationBarTheme = context.dependOnInheritedWidgetOfExactType<NavigationBarTheme>();
return navigationBarTheme?.data ?? Theme.of(context).navigationBarTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return NavigationBarTheme(data: data, child: child);
}
@override
bool updateShouldNotify(NavigationBarTheme oldWidget) => data != oldWidget.data;
}
...@@ -28,6 +28,7 @@ import 'floating_action_button_theme.dart'; ...@@ -28,6 +28,7 @@ import 'floating_action_button_theme.dart';
import 'ink_splash.dart'; import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory; import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart'; import 'input_decorator.dart';
import 'navigation_bar_theme.dart';
import 'navigation_rail_theme.dart'; import 'navigation_rail_theme.dart';
import 'outlined_button_theme.dart'; import 'outlined_button_theme.dart';
import 'page_transitions_theme.dart'; import 'page_transitions_theme.dart';
...@@ -317,6 +318,7 @@ class ThemeData with Diagnosticable { ...@@ -317,6 +318,7 @@ class ThemeData with Diagnosticable {
ColorScheme? colorScheme, ColorScheme? colorScheme,
DialogTheme? dialogTheme, DialogTheme? dialogTheme,
FloatingActionButtonThemeData? floatingActionButtonTheme, FloatingActionButtonThemeData? floatingActionButtonTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme, NavigationRailThemeData? navigationRailTheme,
Typography? typography, Typography? typography,
NoDefaultCupertinoThemeData? cupertinoOverrideTheme, NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
...@@ -462,6 +464,7 @@ class ThemeData with Diagnosticable { ...@@ -462,6 +464,7 @@ class ThemeData with Diagnosticable {
); );
dialogTheme ??= const DialogTheme(); dialogTheme ??= const DialogTheme();
floatingActionButtonTheme ??= const FloatingActionButtonThemeData(); floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
navigationBarTheme ??= const NavigationBarThemeData();
navigationRailTheme ??= const NavigationRailThemeData(); navigationRailTheme ??= const NavigationRailThemeData();
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
snackBarTheme ??= const SnackBarThemeData(); snackBarTheme ??= const SnackBarThemeData();
...@@ -543,6 +546,7 @@ class ThemeData with Diagnosticable { ...@@ -543,6 +546,7 @@ class ThemeData with Diagnosticable {
colorScheme: colorScheme, colorScheme: colorScheme,
dialogTheme: dialogTheme, dialogTheme: dialogTheme,
floatingActionButtonTheme: floatingActionButtonTheme, floatingActionButtonTheme: floatingActionButtonTheme,
navigationBarTheme: navigationBarTheme,
navigationRailTheme: navigationRailTheme, navigationRailTheme: navigationRailTheme,
typography: typography, typography: typography,
cupertinoOverrideTheme: cupertinoOverrideTheme, cupertinoOverrideTheme: cupertinoOverrideTheme,
...@@ -677,6 +681,7 @@ class ThemeData with Diagnosticable { ...@@ -677,6 +681,7 @@ class ThemeData with Diagnosticable {
required this.colorScheme, required this.colorScheme,
required this.dialogTheme, required this.dialogTheme,
required this.floatingActionButtonTheme, required this.floatingActionButtonTheme,
required this.navigationBarTheme,
required this.navigationRailTheme, required this.navigationRailTheme,
required this.typography, required this.typography,
required this.cupertinoOverrideTheme, required this.cupertinoOverrideTheme,
...@@ -763,6 +768,7 @@ class ThemeData with Diagnosticable { ...@@ -763,6 +768,7 @@ class ThemeData with Diagnosticable {
assert(colorScheme != null), assert(colorScheme != null),
assert(dialogTheme != null), assert(dialogTheme != null),
assert(floatingActionButtonTheme != null), assert(floatingActionButtonTheme != null),
assert(navigationBarTheme != null),
assert(navigationRailTheme != null), assert(navigationRailTheme != null),
assert(typography != null), assert(typography != null),
assert(snackBarTheme != null), assert(snackBarTheme != null),
...@@ -1269,6 +1275,10 @@ class ThemeData with Diagnosticable { ...@@ -1269,6 +1275,10 @@ class ThemeData with Diagnosticable {
/// [FloatingActionButton]. /// [FloatingActionButton].
final FloatingActionButtonThemeData floatingActionButtonTheme; final FloatingActionButtonThemeData floatingActionButtonTheme;
/// A theme for customizing the background color, text style, and icon themes
/// of a [NavigationBar].
final NavigationBarThemeData navigationBarTheme;
/// A theme for customizing the background color, elevation, text style, and /// A theme for customizing the background color, elevation, text style, and
/// icon themes of a [NavigationRail]. /// icon themes of a [NavigationRail].
final NavigationRailThemeData navigationRailTheme; final NavigationRailThemeData navigationRailTheme;
...@@ -1485,6 +1495,7 @@ class ThemeData with Diagnosticable { ...@@ -1485,6 +1495,7 @@ class ThemeData with Diagnosticable {
ColorScheme? colorScheme, ColorScheme? colorScheme,
DialogTheme? dialogTheme, DialogTheme? dialogTheme,
FloatingActionButtonThemeData? floatingActionButtonTheme, FloatingActionButtonThemeData? floatingActionButtonTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme, NavigationRailThemeData? navigationRailTheme,
Typography? typography, Typography? typography,
NoDefaultCupertinoThemeData? cupertinoOverrideTheme, NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
...@@ -1576,6 +1587,7 @@ class ThemeData with Diagnosticable { ...@@ -1576,6 +1587,7 @@ class ThemeData with Diagnosticable {
colorScheme: (colorScheme ?? this.colorScheme).copyWith(brightness: brightness), colorScheme: (colorScheme ?? this.colorScheme).copyWith(brightness: brightness),
dialogTheme: dialogTheme ?? this.dialogTheme, dialogTheme: dialogTheme ?? this.dialogTheme,
floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme, floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme, navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
typography: typography ?? this.typography, typography: typography ?? this.typography,
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme, cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
...@@ -1737,6 +1749,7 @@ class ThemeData with Diagnosticable { ...@@ -1737,6 +1749,7 @@ class ThemeData with Diagnosticable {
colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t), colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t),
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t), dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!, floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!,
navigationBarTheme: NavigationBarThemeData.lerp(a.navigationBarTheme, b.navigationBarTheme, t)!,
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!, navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!,
typography: Typography.lerp(a.typography, b.typography, t), typography: Typography.lerp(a.typography, b.typography, t),
cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme, cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
...@@ -1826,6 +1839,7 @@ class ThemeData with Diagnosticable { ...@@ -1826,6 +1839,7 @@ class ThemeData with Diagnosticable {
&& other.colorScheme == colorScheme && other.colorScheme == colorScheme
&& other.dialogTheme == dialogTheme && other.dialogTheme == dialogTheme
&& other.floatingActionButtonTheme == floatingActionButtonTheme && other.floatingActionButtonTheme == floatingActionButtonTheme
&& other.navigationBarTheme == navigationBarTheme
&& other.navigationRailTheme == navigationRailTheme && other.navigationRailTheme == navigationRailTheme
&& other.typography == typography && other.typography == typography
&& other.cupertinoOverrideTheme == cupertinoOverrideTheme && other.cupertinoOverrideTheme == cupertinoOverrideTheme
...@@ -1914,6 +1928,7 @@ class ThemeData with Diagnosticable { ...@@ -1914,6 +1928,7 @@ class ThemeData with Diagnosticable {
colorScheme, colorScheme,
dialogTheme, dialogTheme,
floatingActionButtonTheme, floatingActionButtonTheme,
navigationBarTheme,
navigationRailTheme, navigationRailTheme,
typography, typography,
cupertinoOverrideTheme, cupertinoOverrideTheme,
...@@ -1999,6 +2014,7 @@ class ThemeData with Diagnosticable { ...@@ -1999,6 +2014,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonThemeData', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonThemeData', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationBarThemeData>('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailThemeData', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailThemeData', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NoDefaultCupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<NoDefaultCupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, level: DiagnosticLevel.debug));
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Navigation bar updates destinations when tapped', (WidgetTester tester) async {
int mutatedIndex = -1;
final Widget widget = _buildWidget(
NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {
mutatedIndex = i;
},
),
);
await tester.pumpWidget(widget);
expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget);
await tester.tap(find.text('Alarm'));
expect(mutatedIndex, 1);
await tester.tap(find.text('AC'));
expect(mutatedIndex, 0);
});
testWidgets('NavigationBar can update background color', (WidgetTester tester) async {
const Color color = Colors.yellow;
await tester.pumpWidget(
_buildWidget(
NavigationBar(
backgroundColor: color,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
);
expect(_getMaterial(tester).color, equals(color));
});
testWidgets('NavigationBar adds bottom padding to height', (WidgetTester tester) async {
const double bottomPadding = 40.0;
await tester.pumpWidget(
_buildWidget(
NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
);
final double defaultSize = tester.getSize(find.byType(NavigationBar)).height;
expect(defaultSize, 80);
await tester.pumpWidget(
_buildWidget(
MediaQuery(
data: const MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)),
child: NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
),
);
final double expectedHeight = defaultSize + bottomPadding;
expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight);
});
testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async {
const String label = 'A';
Widget buildApp({ required double textScaleFactor }) {
return MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
bottomNavigationBar: NavigationBar(
destinations: const <NavigationDestination>[
NavigationDestination(
label: label,
icon: Icon(Icons.ac_unit),
tooltip: label,
),
NavigationDestination(
label: 'B',
icon: Icon(Icons.battery_alert),
),
],
),
);
},
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp(textScaleFactor: 1.0));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(find.text(label), findsNWidgets(2));
// The default size of a tooltip with the text A.
const Size defaultTooltipSize = Size(14.0, 14.0);
expect(tester.getSize(find.text(label).last), defaultTooltipSize);
// The duration is needed to ensure the tooltip disappears.
await tester.pumpAndSettle(const Duration(seconds: 2));
await tester.pumpWidget(buildApp(textScaleFactor: 4.0));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(tester.getSize(find.text(label).last), Size(defaultTooltipSize.width * 4, defaultTooltipSize.height * 4));
});
testWidgets('Custom tooltips in NavigationBarDestination', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBar(
destinations: const <NavigationDestination>[
NavigationDestination(
label: 'A',
tooltip: 'A tooltip',
icon: Icon(Icons.ac_unit),
),
NavigationDestination(
label: 'B',
icon: Icon(Icons.battery_alert),
),
NavigationDestination(
label: 'C',
icon: Icon(Icons.cake),
tooltip: '',
),
],
),
),
),
);
expect(find.text('A'), findsOneWidget);
await tester.longPress(find.text('A'));
expect(find.byTooltip('A tooltip'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
await tester.longPress(find.text('B'));
expect(find.byTooltip('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
await tester.longPress(find.text('C'));
expect(find.byTooltip('C'), findsNothing);
});
testWidgets('Navigation bar semantics', (WidgetTester tester) async {
Widget _widget({int selectedIndex = 0}) {
return _buildWidget(
NavigationBar(
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
);
}
await tester.pumpWidget(_widget(selectedIndex: 0));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: false,
hasTapAction: true,
),
);
await tester.pumpWidget(_widget(selectedIndex: 1));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: false,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
});
testWidgets('Navigation bar semantics with some labels hidden', (WidgetTester tester) async {
Widget _widget({int selectedIndex = 0}) {
return _buildWidget(
NavigationBar(
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
selectedIndex: selectedIndex,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
);
}
await tester.pumpWidget(_widget(selectedIndex: 0));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: false,
hasTapAction: true,
),
);
await tester.pumpWidget(_widget(selectedIndex: 1));
expect(
tester.getSemantics(find.text('AC')),
matchesSemantics(
label: 'AC\nTab 1 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: false,
hasTapAction: true,
),
);
expect(
tester.getSemantics(find.text('Alarm')),
matchesSemantics(
label: 'Alarm\nTab 2 of 2',
textDirection: TextDirection.ltr,
isFocusable: true,
isSelected: true,
hasTapAction: true,
),
);
});
testWidgets('Navigation bar does not grow with text scale factor', (WidgetTester tester) async {
const int _animationMilliseconds = 800;
Widget _widget({double textScaleFactor = 1}) {
return _buildWidget(
MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: NavigationBar(
animationDuration: const Duration(milliseconds: _animationMilliseconds),
destinations: const <NavigationDestination>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
),
),
);
}
await tester.pumpWidget(_widget());
final double initialHeight = tester.getSize(find.byType(NavigationBar)).height;
await tester.pumpWidget(_widget(textScaleFactor: 2));
final double newHeight = tester.getSize(find.byType(NavigationBar)).height;
expect(newHeight, equals(initialHeight));
});
}
Widget _buildWidget(Widget child) {
return MaterialApp(
theme: ThemeData.light(),
home: Scaffold(
bottomNavigationBar: Center(
child: child,
),
),
);
}
Material _getMaterial(WidgetTester tester) {
return tester.firstWidget<Material>(
find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)),
);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('copyWith, ==, hashCode basics', () {
expect(const NavigationBarThemeData(), const NavigationBarThemeData().copyWith());
expect(const NavigationBarThemeData().hashCode, const NavigationBarThemeData().copyWith().hashCode);
});
testWidgets('Default debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const NavigationBarThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('Custom debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
NavigationBarThemeData(
height: 200.0,
backgroundColor: const Color(0x00000099),
indicatorColor: const Color(0x00000098),
labelTextStyle: MaterialStateProperty.all(const TextStyle(fontSize: 7.0)),
iconTheme: MaterialStateProperty.all(const IconThemeData(color: Color(0x00000097))),
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description[0], 'height: 200.0');
expect(description[1], 'backgroundColor: Color(0x00000099)');
expect(description[2], 'indicatorColor: Color(0x00000098)');
expect(description[3], 'labelTextStyle: MaterialStateProperty.all(TextStyle(inherit: true, size: 7.0))');
// Ignore instance address for IconThemeData.
expect(description[4].contains('iconTheme: MaterialStateProperty.all(IconThemeData'), isTrue);
expect(description[4].contains('(color: Color(0x00000097))'), isTrue);
expect(description[5], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide');
});
testWidgets('NavigationBarThemeData values are used when no NavigationBar properties are specified', (WidgetTester tester) async {
const double height = 200.0;
const Color backgroundColor = Color(0x00000001);
const Color indicatorColor = Color(0x00000002);
const double selectedIconSize = 25.0;
const double unselectedIconSize = 23.0;
const Color selectedIconColor = Color(0x00000003);
const Color unselectedIconColor = Color(0x00000004);
const double selectedIconOpacity = 0.99;
const double unselectedIconOpacity = 0.98;
const double selectedLabelFontSize = 13.0;
const double unselectedLabelFontSize = 11.0;
const NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBarTheme(
data: NavigationBarThemeData(
height: height,
backgroundColor: backgroundColor,
indicatorColor: indicatorColor,
iconTheme: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const IconThemeData(
size: selectedIconSize,
color: selectedIconColor,
opacity: selectedIconOpacity,
);
}
return const IconThemeData(
size: unselectedIconSize,
color: unselectedIconColor,
opacity: unselectedIconOpacity,
);
}),
labelTextStyle: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const TextStyle(fontSize: selectedLabelFontSize);
}
return const TextStyle(fontSize: unselectedLabelFontSize);
}),
labelBehavior: labelBehavior,
),
child: NavigationBar(
selectedIndex: 0,
destinations: _destinations(),
),
),
),
),
);
expect(_barHeight(tester), height);
expect(_barMaterial(tester).color, backgroundColor);
expect(_indicator(tester)?.color, indicatorColor);
expect(_selectedIconTheme(tester).size, selectedIconSize);
expect(_selectedIconTheme(tester).color, selectedIconColor);
expect(_selectedIconTheme(tester).opacity, selectedIconOpacity);
expect(_unselectedIconTheme(tester).size, unselectedIconSize);
expect(_unselectedIconTheme(tester).color, unselectedIconColor);
expect(_unselectedIconTheme(tester).opacity, unselectedIconOpacity);
expect(_selectedLabelStyle(tester).fontSize, selectedLabelFontSize);
expect(_unselectedLabelStyle(tester).fontSize, unselectedLabelFontSize);
expect(_labelBehavior(tester), labelBehavior);
});
testWidgets('NavigationBar values take priority over NavigationBarThemeData values when both properties are specified', (WidgetTester tester) async {
const double height = 200.0;
const Color backgroundColor = Color(0x00000001);
const NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: NavigationBarTheme(
data: const NavigationBarThemeData(
height: 100.0,
backgroundColor: Color(0x00000099),
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
),
child: NavigationBar(
height: height,
backgroundColor: backgroundColor,
labelBehavior: labelBehavior,
selectedIndex: 0,
destinations: _destinations(),
),
),
),
),
);
expect(_barHeight(tester), height);
expect(_barMaterial(tester).color, backgroundColor);
expect(_labelBehavior(tester), labelBehavior);
});
}
List<NavigationDestination> _destinations() {
return const <NavigationDestination>[
NavigationDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: 'Abc',
),
NavigationDestination(
icon: Icon(Icons.star_border),
selectedIcon: Icon(Icons.star),
label: 'Def',
),
];
}
double _barHeight(WidgetTester tester) {
return tester.getRect(
find.byType(NavigationBar),
).height;
}
Material _barMaterial(WidgetTester tester) {
return tester.firstWidget<Material>(
find.descendant(
of: find.byType(NavigationBar),
matching: find.byType(Material),
),
);
}
BoxDecoration? _indicator(WidgetTester tester) {
return tester.firstWidget<Container>(
find.descendant(
of: find.byType(FadeTransition),
matching: find.byType(Container),
),
).decoration as BoxDecoration?;
}
IconThemeData _selectedIconTheme(WidgetTester tester) {
return _iconTheme(tester, Icons.favorite);
}
IconThemeData _unselectedIconTheme(WidgetTester tester) {
return _iconTheme(tester, Icons.star_border);
}
IconThemeData _iconTheme(WidgetTester tester, IconData icon) {
return tester.firstWidget<IconTheme>(
find.ancestor(
of: find.byIcon(icon),
matching: find.byType(IconTheme),
),
).data;
}
TextStyle _selectedLabelStyle(WidgetTester tester) {
return tester.widget<RichText>(
find.descendant(
of: find.text('Abc'),
matching: find.byType(RichText),
),
).text.style!;
}
TextStyle _unselectedLabelStyle(WidgetTester tester) {
return tester.widget<RichText>(
find.descendant(
of: find.text('Def'),
matching: find.byType(RichText),
),
).text.style!;
}
NavigationDestinationLabelBehavior _labelBehavior(WidgetTester tester) {
if (_opacityAboveLabel('Abc').evaluate().isNotEmpty && _opacityAboveLabel('Def').evaluate().isNotEmpty) {
return _labelOpacity(tester, 'Abc') == 1
? NavigationDestinationLabelBehavior.onlyShowSelected
: NavigationDestinationLabelBehavior.alwaysHide;
} else {
return NavigationDestinationLabelBehavior.alwaysShow;
}
}
Finder _opacityAboveLabel(String text) {
return find.ancestor(
of: find.text(text),
matching: find.byType(Opacity),
);
}
// Only valid when labelBehavior != alwaysShow.
double _labelOpacity(WidgetTester tester, String text) {
final Opacity opacityWidget = tester.widget<Opacity>(
find.ancestor(
of: find.text(text),
matching: find.byType(Opacity),
),
);
return opacityWidget.opacity;
}
...@@ -324,6 +324,7 @@ void main() { ...@@ -324,6 +324,7 @@ void main() {
colorScheme: const ColorScheme.light(), colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.black), dialogTheme: const DialogTheme(backgroundColor: Colors.black),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black), floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black), navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
typography: Typography.material2018(platform: TargetPlatform.android), typography: Typography.material2018(platform: TargetPlatform.android),
cupertinoOverrideTheme: null, cupertinoOverrideTheme: null,
...@@ -420,6 +421,7 @@ void main() { ...@@ -420,6 +421,7 @@ void main() {
colorScheme: const ColorScheme.light(), colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.white), dialogTheme: const DialogTheme(backgroundColor: Colors.white),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white), floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white), navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
typography: Typography.material2018(platform: TargetPlatform.iOS), typography: Typography.material2018(platform: TargetPlatform.iOS),
cupertinoOverrideTheme: ThemeData.light().cupertinoOverrideTheme, cupertinoOverrideTheme: ThemeData.light().cupertinoOverrideTheme,
...@@ -497,6 +499,7 @@ void main() { ...@@ -497,6 +499,7 @@ void main() {
colorScheme: otherTheme.colorScheme, colorScheme: otherTheme.colorScheme,
dialogTheme: otherTheme.dialogTheme, dialogTheme: otherTheme.dialogTheme,
floatingActionButtonTheme: otherTheme.floatingActionButtonTheme, floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
navigationBarTheme: otherTheme.navigationBarTheme,
navigationRailTheme: otherTheme.navigationRailTheme, navigationRailTheme: otherTheme.navigationRailTheme,
typography: otherTheme.typography, typography: otherTheme.typography,
cupertinoOverrideTheme: otherTheme.cupertinoOverrideTheme, cupertinoOverrideTheme: otherTheme.cupertinoOverrideTheme,
...@@ -571,6 +574,7 @@ void main() { ...@@ -571,6 +574,7 @@ void main() {
expect(themeDataCopy.colorScheme, equals(otherTheme.colorScheme)); expect(themeDataCopy.colorScheme, equals(otherTheme.colorScheme));
expect(themeDataCopy.dialogTheme, equals(otherTheme.dialogTheme)); expect(themeDataCopy.dialogTheme, equals(otherTheme.dialogTheme));
expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme)); expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
expect(themeDataCopy.navigationBarTheme, equals(otherTheme.navigationBarTheme));
expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme)); expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme));
expect(themeDataCopy.typography, equals(otherTheme.typography)); expect(themeDataCopy.typography, equals(otherTheme.typography));
expect(themeDataCopy.cupertinoOverrideTheme, equals(otherTheme.cupertinoOverrideTheme)); expect(themeDataCopy.cupertinoOverrideTheme, equals(otherTheme.cupertinoOverrideTheme));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment