mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[Material] Create a Navigation Rail component and theme (#49574)
This commit is contained in:
parent
c9f999278c
commit
0d111bc91a
@ -81,6 +81,8 @@ export 'src/material/material_button.dart';
|
||||
export 'src/material/material_localizations.dart';
|
||||
export 'src/material/material_state.dart';
|
||||
export 'src/material/mergeable_material.dart';
|
||||
export 'src/material/navigation_rail.dart';
|
||||
export 'src/material/navigation_rail_theme.dart';
|
||||
export 'src/material/outline_button.dart';
|
||||
export 'src/material/page.dart';
|
||||
export 'src/material/page_transitions_theme.dart';
|
||||
|
873
packages/flutter/lib/src/material/navigation_rail.dart
Normal file
873
packages/flutter/lib/src/material/navigation_rail.dart
Normal file
@ -0,0 +1,873 @@
|
||||
// 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';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../scheduler.dart';
|
||||
|
||||
import 'color_scheme.dart';
|
||||
import 'ink_well.dart';
|
||||
import 'material.dart';
|
||||
import 'material_localizations.dart';
|
||||
import 'navigation_rail_theme.dart';
|
||||
import 'theme.dart';
|
||||
import 'theme_data.dart';
|
||||
|
||||
/// A material widget that is meant to be displayed at the left or right of an
|
||||
/// app to navigate between a small number of views, typically between three and
|
||||
/// five.
|
||||
///
|
||||
/// A navigation rail is usually used as the first or last element of a [Row]
|
||||
/// which defines the app's [Scaffold] body.
|
||||
///
|
||||
/// The appearance of all of the [NavigationRail]s within an app can be
|
||||
/// specified with [NavigationRailTheme]. The default values for null theme
|
||||
/// properties are based on the [Theme]'s [ThemeData.textTheme],
|
||||
/// [ThemeData.iconTheme], and [ThemeData.colorScheme].
|
||||
//
|
||||
/// The navigation rail is meant for layouts with wide viewports, such as a
|
||||
/// desktop web or tablet landscape layout. For smaller layouts, like mobile
|
||||
/// portrait, a [BottomNavigationBar] should be used instead.
|
||||
///
|
||||
/// Adaptive layouts can build different instances of the [Scaffold] in order to
|
||||
/// have a navigation rail for more horizontal layouts and a bottom navigation
|
||||
/// bar for more vertical layouts. See
|
||||
/// [https://github.com/flutter/samples/blob/master/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart]
|
||||
/// for an example.
|
||||
///
|
||||
/// {@tool dartpad --template=stateful_widget_material}
|
||||
///
|
||||
/// This example shows a [NavigationRail] used within a Scaffold with 3
|
||||
/// [NavigationRailDestination]s. The main content is separated by a divider
|
||||
/// (although elevation on the navigation rail can be used instead). The
|
||||
/// `_selectedIndex` is updated by the `onDestinationSelected` callback.
|
||||
///
|
||||
/// ```dart
|
||||
/// int _selectedIndex = 0;
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// body: Row(
|
||||
/// children: <Widget>[
|
||||
/// NavigationRail(
|
||||
/// selectedIndex: _selectedIndex,
|
||||
/// onDestinationSelected: (int index) {
|
||||
/// setState(() {
|
||||
/// _selectedIndex = index;
|
||||
/// });
|
||||
/// },
|
||||
/// labelType: NavigationRailLabelType.selected,
|
||||
/// destinations: [
|
||||
/// NavigationRailDestination(
|
||||
/// icon: Icon(Icons.favorite_border),
|
||||
/// selectedIcon: Icon(Icons.favorite),
|
||||
/// label: Text('First'),
|
||||
/// ),
|
||||
/// NavigationRailDestination(
|
||||
/// icon: Icon(Icons.bookmark_border),
|
||||
/// selectedIcon: Icon(Icons.book),
|
||||
/// label: Text('Second'),
|
||||
/// ),
|
||||
/// NavigationRailDestination(
|
||||
/// icon: Icon(Icons.star_border),
|
||||
/// selectedIcon: Icon(Icons.star),
|
||||
/// label: Text('Third'),
|
||||
/// ),
|
||||
/// ],
|
||||
/// ),
|
||||
/// VerticalDivider(thickness: 1, width: 1),
|
||||
/// // This is the main content.
|
||||
/// Expanded(
|
||||
/// child: Center(
|
||||
/// child: Text('selectedIndex: $_selectedIndex'),
|
||||
/// ),
|
||||
/// )
|
||||
/// ],
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Scaffold], which can display the navigation rail within a [Row] of the
|
||||
/// [Scaffold.body] slot.
|
||||
/// * [NavigationRailDestination], which is used as a model to create tappable
|
||||
/// destinations in the navigation rail.
|
||||
/// * [BottomNavigationBar], which is a similar navigation widget that's laid
|
||||
/// out horizontally.
|
||||
/// * [https://material.io/components/navigation-rail/]
|
||||
class NavigationRail extends StatefulWidget {
|
||||
/// Creates a material design navigation rail.
|
||||
///
|
||||
/// The value of [destinations] must be a list of one or more
|
||||
/// [NavigationRailDestination] values.
|
||||
///
|
||||
/// If [elevation] is specified, it must be non-negative.
|
||||
///
|
||||
/// If [minWidth] is specified, it must be non-negative, and if
|
||||
/// [minExtendedWidth] is specified, it must be non-negative and greater than
|
||||
/// [minWidth].
|
||||
///
|
||||
/// The argument [extended] must not be null. [extended] can only be set to
|
||||
/// true when when the [labelType] is null or [NavigationRailLabelType.none].
|
||||
///
|
||||
/// If [backgroundColor], [elevation], [groupAlignment], [labelType],
|
||||
/// [unselectedLabelTextStyle], [unselectedLabelTextStyle],
|
||||
/// [unselectedIconTheme], or [selectedIconTheme] are null, then their
|
||||
/// [NavigationRailThemeData] values will be used. If the corresponding
|
||||
/// [NavigationRailThemeData] property is null, then the navigation rail
|
||||
/// defaults are used. See the individual properties for more information.
|
||||
///
|
||||
/// Typically used within a [Row] that defines the [Scaffold.body] property.
|
||||
const NavigationRail({
|
||||
this.backgroundColor,
|
||||
this.extended = false,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
@required this.destinations,
|
||||
@required this.selectedIndex,
|
||||
this.onDestinationSelected,
|
||||
this.elevation,
|
||||
this.groupAlignment,
|
||||
this.labelType,
|
||||
this.unselectedLabelTextStyle,
|
||||
this.selectedLabelTextStyle,
|
||||
this.unselectedIconTheme,
|
||||
this.selectedIconTheme,
|
||||
this.minWidth,
|
||||
this.minExtendedWidth,
|
||||
}) : assert(destinations != null && destinations.length >= 2),
|
||||
assert(selectedIndex != null),
|
||||
assert(0 <= selectedIndex && selectedIndex < destinations.length),
|
||||
assert(elevation == null || elevation > 0),
|
||||
assert(minWidth == null || minWidth > 0),
|
||||
assert(minExtendedWidth == null || minExtendedWidth > 0),
|
||||
assert((minWidth == null || minExtendedWidth == null) || minExtendedWidth >= minWidth),
|
||||
assert(extended != null),
|
||||
assert(!extended || (labelType == null || labelType == NavigationRailLabelType.none));
|
||||
|
||||
/// Sets the color of the Container that holds all of the [NavigationRail]'s
|
||||
/// contents.
|
||||
///
|
||||
/// The default value is [NavigationRailThemeData.backgroundColor]. If
|
||||
/// [NavigationRailThemeData.backgroundColor] is null, then the default value
|
||||
/// is based on [ThemeData.colorScheme.surface].
|
||||
final Color backgroundColor;
|
||||
|
||||
/// Indicates that the [NavigationRail] should be in the extended state.
|
||||
///
|
||||
/// The extended state has a wider rail container, and the labels are
|
||||
/// positioned next to the icons. [minExtendedWidth] can be used to set the
|
||||
/// the minimum width of the rail when it is in this state.
|
||||
///
|
||||
/// The rail will implicitly animate between the extended and normal state.
|
||||
///
|
||||
/// If the rail is going to be in the extended state, then the [labelType]
|
||||
/// must be set to [NavigationRailLabelType.none].
|
||||
///
|
||||
/// The default value is false.
|
||||
final bool extended;
|
||||
|
||||
/// The leading widget in the rail that is placed above the destinations.
|
||||
///
|
||||
/// It is placed at the top of the rail, above the [destinations]. Its
|
||||
/// location is not affected by [groupAlignment].
|
||||
///
|
||||
/// This is commonly a [FloatingActionButton], but may also be a non-button,
|
||||
/// such as a logo.
|
||||
///
|
||||
/// The default value is null.
|
||||
final Widget leading;
|
||||
|
||||
/// The trailing widget in the rail that is placed below the destinations.
|
||||
///
|
||||
/// The trailing widget is placed below the last [NavigationRailDestination].
|
||||
/// It's location is affected by [groupAlignment].
|
||||
///
|
||||
/// This is commonly a list of additional options or destinations that is
|
||||
/// usually only rendered when [extended] is true.
|
||||
///
|
||||
/// The default value is null.
|
||||
final Widget trailing;
|
||||
|
||||
/// Defines the appearance of the button items that are arrayed within the
|
||||
/// navigation rail.
|
||||
///
|
||||
/// The value must be a list of two or more [NavigationRailDestination]
|
||||
/// values.
|
||||
final List<NavigationRailDestination> destinations;
|
||||
|
||||
/// The index into [destinations] for the current selected
|
||||
/// [NavigationRailDestination].
|
||||
final int selectedIndex;
|
||||
|
||||
/// Called when one of the [destinations] is selected.
|
||||
///
|
||||
/// The stateful widget that creates the navigation rail needs to keep
|
||||
/// track of the index of the selected [NavigationRailDestination] and call
|
||||
/// `setState` to rebuild the navigation rail with the new [selectedIndex].
|
||||
final ValueChanged<int> onDestinationSelected;
|
||||
|
||||
/// The rail's elevation or z-coordinate.
|
||||
///
|
||||
/// If [Directionality] is [TextDirection.LTR], the inner side is the right
|
||||
/// side, and if [Directionality] is [TextDirection.RTL], it is the left side.
|
||||
///
|
||||
/// The default value is 0.
|
||||
final double elevation;
|
||||
|
||||
/// The vertical alignment for the group of [destinations] within the rail.
|
||||
///
|
||||
/// The [NavigationRailDestination]s are grouped together with the [trailing]
|
||||
/// widget, between the [leading] widget and the bottom of the rail.
|
||||
///
|
||||
/// The value must be between -1.0 and 1.0.
|
||||
///
|
||||
/// If [groupAlignment] is -1.0, then the items are aligned to the top. If
|
||||
/// [groupAlignment] is 0.0, then the items are aligned to the center. If
|
||||
/// [groupAlignment] is 1.0, then the items are aligned to the bottom.
|
||||
///
|
||||
/// The default is -1.0.
|
||||
///
|
||||
/// See also:
|
||||
/// * [Alignment.y]
|
||||
///
|
||||
final double groupAlignment;
|
||||
|
||||
/// Defines the layout and behavior of the labels for the default, unextended
|
||||
/// [NavigationRail].
|
||||
///
|
||||
/// When a navigation rail is [extended], the labels are always shown.
|
||||
///
|
||||
/// The default value is [NavigationRailThemeData.labelType]. If
|
||||
/// [NavigationRailThemeData.labelType] is null, then the default value is
|
||||
/// [NavigationRailLabelType.none].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationRailLabelType] for information on the meaning of different
|
||||
/// types.
|
||||
final NavigationRailLabelType labelType;
|
||||
|
||||
/// The [TextStyle] of a destination's label when it is unselected.
|
||||
///
|
||||
/// When one of the [destinations] is selected the [selectedLabelTextStyle]
|
||||
/// will be used instead.
|
||||
///
|
||||
/// The default value is based on the [Theme]'s
|
||||
/// [ThemeData.textTheme.bodyText]. The default color is based on the
|
||||
/// [Theme]'s [ColorScheme.onSurface].
|
||||
///
|
||||
/// Properties from this text style, or
|
||||
/// [NavigationRailThemeData.unselectedLabelTextStyle] if this is null, are
|
||||
/// merged into the defaults.
|
||||
final TextStyle unselectedLabelTextStyle;
|
||||
|
||||
/// The [TextStyle] of a destination's label when it is selected.
|
||||
///
|
||||
/// When a [NavigationRailDestination] is not selected,
|
||||
/// [unselectedLabelTextStyle] will be used.
|
||||
///
|
||||
/// The default value is based on the [Theme]'s
|
||||
/// [ThemeData.textTheme.bodyText]. The default color is based on the
|
||||
/// [Theme]'s [ColorScheme.primary].
|
||||
///
|
||||
/// Properties from this text style,
|
||||
/// or [NavigationRailThemeData.selectedLabelTextStyle] if this is null, are
|
||||
/// merged into the defaults.
|
||||
final TextStyle selectedLabelTextStyle;
|
||||
|
||||
/// The visual properties of the icon in the unselected destination.
|
||||
///
|
||||
/// If this field is not provided, or provided with any null properties, then
|
||||
/// a copy of the [IconThemeData.fallback] with a custom [NavigationRail]
|
||||
/// specific color will be used.
|
||||
///
|
||||
/// The default value is Is the [Theme]'s [ThemeData.iconTheme] with a color
|
||||
/// of the [Theme]'s [ColorScheme.onSurface] with an opacity of 0.64.
|
||||
/// Properties from this icon theme, or
|
||||
/// [NavigationRailThemeData.unselectedIconTheme] if this is null, are
|
||||
/// merged into the defaults.
|
||||
final IconThemeData unselectedIconTheme;
|
||||
|
||||
/// The visual properties of the icon in the selected destination.
|
||||
///
|
||||
/// When a [NavigationRailDestination] is not selected,
|
||||
/// [unselectedIconTheme] will be used.
|
||||
///
|
||||
/// The default value is Is the [Theme]'s [ThemeData.iconTheme] with a color
|
||||
/// of the [Theme]'s [ColorScheme.primary]. Properties from this icon theme,
|
||||
/// or [NavigationRailThemeData.selectedIconTheme] if this is null, are
|
||||
/// merged into the defaults.
|
||||
final IconThemeData selectedIconTheme;
|
||||
|
||||
/// The smallest possible width for the rail regardless of the destination's
|
||||
/// icon or label size.
|
||||
///
|
||||
/// The default is 72.
|
||||
///
|
||||
/// This value also defines the min width and min height of the destinations.
|
||||
///
|
||||
/// To make a compact rail, set this to 56 and use
|
||||
/// [NavigationRailLabelType.none].
|
||||
final double minWidth;
|
||||
|
||||
/// The final width when the animation is complete for setting [extended] to
|
||||
/// true.
|
||||
///
|
||||
/// This is only used when [extended] is set to true.
|
||||
///
|
||||
/// The default value is 256.
|
||||
final double minExtendedWidth;
|
||||
|
||||
/// Returns the animation that controls the [NavigationRail.extended] state.
|
||||
///
|
||||
/// This can be used to synchronize animations in the [leading] or [trailing]
|
||||
/// widget, such as an animated menu or a [FloatingActionButton] animation.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// This example shows how to use this animation to create a
|
||||
/// [FloatingActionButton] that animates itself between the normal and
|
||||
/// extended states of the [NavigationRail].
|
||||
///
|
||||
/// An instance of `ExtendableFab` would be created for
|
||||
/// [NavigationRail.leading].
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'dart:ui';
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// final Animation<double> animation = NavigationRail.extendedAnimation(context);
|
||||
/// return AnimatedBuilder(
|
||||
/// animation: animation,
|
||||
/// builder: (BuildContext context, Widget child) {
|
||||
/// // The extended fab has a shorter height than the regular fab.
|
||||
/// return Container(
|
||||
/// height: 56,
|
||||
/// padding: EdgeInsets.symmetric(
|
||||
/// vertical: lerpDouble(0, 6, animation.value),
|
||||
/// ),
|
||||
/// child: animation.value == 0
|
||||
/// ? FloatingActionButton(
|
||||
/// child: Icon(Icons.add),
|
||||
/// onPressed: () {},
|
||||
/// )
|
||||
/// : Align(
|
||||
/// alignment: AlignmentDirectional.centerStart,
|
||||
/// widthFactor: animation.value,
|
||||
/// child: Padding(
|
||||
/// padding: const EdgeInsetsDirectional.only(start: 8),
|
||||
/// child: FloatingActionButton.extended(
|
||||
/// icon: Icon(Icons.add),
|
||||
/// label: Text('CREATE'),
|
||||
/// onPressed: () {},
|
||||
/// ),
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// },
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// {@end-tool}
|
||||
static Animation<double> extendedAnimation(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<_ExtendedNavigationRailAnimation>().animation;
|
||||
}
|
||||
|
||||
@override
|
||||
_NavigationRailState createState() => _NavigationRailState();
|
||||
}
|
||||
|
||||
class _NavigationRailState extends State<NavigationRail> with TickerProviderStateMixin {
|
||||
List<AnimationController> _destinationControllers = <AnimationController>[];
|
||||
List<Animation<double>> _destinationAnimations;
|
||||
AnimationController _extendedController;
|
||||
Animation<double> _extendedAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initControllers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposeControllers();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(NavigationRail oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.extended != oldWidget.extended) {
|
||||
if (widget.extended) {
|
||||
_extendedController.forward();
|
||||
} else {
|
||||
_extendedController.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
// No animated segue if the length of the items list changes.
|
||||
if (widget.destinations.length != oldWidget.destinations.length) {
|
||||
_resetState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.selectedIndex != oldWidget.selectedIndex) {
|
||||
_destinationControllers[oldWidget.selectedIndex].reverse();
|
||||
_destinationControllers[widget.selectedIndex].forward();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final NavigationRailThemeData navigationRailTheme = NavigationRailTheme.of(context);
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
|
||||
final Color backgroundColor = widget.backgroundColor ?? navigationRailTheme.backgroundColor ?? theme.colorScheme.surface;
|
||||
final double elevation = widget.elevation ?? navigationRailTheme.elevation ?? 0;
|
||||
final double minWidth = widget.minWidth ?? _minRailWidth;
|
||||
final double minExtendedWidth = widget.minExtendedWidth ?? _minExtendedRailWidth;
|
||||
final Color baseSelectedColor = theme.colorScheme.primary;
|
||||
final Color baseColor = theme.colorScheme.onSurface.withOpacity(0.64);
|
||||
final IconThemeData defaultUnselectedIconTheme = widget.unselectedIconTheme ?? navigationRailTheme.unselectedIconTheme;
|
||||
final IconThemeData unselectedIconTheme = IconThemeData(
|
||||
size: defaultUnselectedIconTheme?.size ?? 24.0,
|
||||
color: defaultUnselectedIconTheme?.color ?? theme.colorScheme.onSurface,
|
||||
opacity: defaultUnselectedIconTheme?.opacity ?? 1.0,
|
||||
);
|
||||
final IconThemeData defaultSelectedIconTheme = widget.selectedIconTheme ?? navigationRailTheme.selectedIconTheme;
|
||||
final IconThemeData selectedIconTheme = IconThemeData(
|
||||
size: defaultSelectedIconTheme?.size ?? 24.0,
|
||||
color: defaultSelectedIconTheme?.color ?? theme.colorScheme.primary,
|
||||
opacity: defaultSelectedIconTheme?.opacity ?? 0.64,
|
||||
);
|
||||
final TextStyle unselectedLabelTextStyle = theme.textTheme.bodyText1.copyWith(color: baseColor).merge(widget.unselectedLabelTextStyle ?? navigationRailTheme.unselectedLabelTextStyle);
|
||||
final TextStyle selectedLabelTextStyle = theme.textTheme.bodyText1.copyWith(color: baseSelectedColor).merge(widget.selectedLabelTextStyle ?? navigationRailTheme.selectedLabelTextStyle);
|
||||
final double groupAlignment = widget.groupAlignment ?? navigationRailTheme.groupAlignment ?? -1.0;
|
||||
final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? NavigationRailLabelType.none;
|
||||
|
||||
return _ExtendedNavigationRailAnimation(
|
||||
animation: _extendedAnimation,
|
||||
child: Semantics(
|
||||
explicitChildNodes: true,
|
||||
child: Material(
|
||||
elevation: elevation,
|
||||
color: backgroundColor,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_verticalSpacer,
|
||||
if (widget.leading != null)
|
||||
...<Widget>[
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: lerpDouble(minWidth, minExtendedWidth, _extendedAnimation.value),
|
||||
),
|
||||
child: widget.leading,
|
||||
),
|
||||
_verticalSpacer,
|
||||
],
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment(0, groupAlignment),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
for (int i = 0; i < widget.destinations.length; i += 1)
|
||||
_RailDestination(
|
||||
minWidth: minWidth,
|
||||
minExtendedWidth: minExtendedWidth,
|
||||
extendedTransitionAnimation: _extendedAnimation,
|
||||
selected: widget.selectedIndex == i,
|
||||
icon: widget.selectedIndex == i ? widget.destinations[i].selectedIcon : widget.destinations[i].icon,
|
||||
label: widget.destinations[i].label,
|
||||
destinationAnimation: _destinationAnimations[i],
|
||||
labelType: labelType,
|
||||
iconTheme: widget.selectedIndex == i ? selectedIconTheme : unselectedIconTheme,
|
||||
labelTextStyle: widget.selectedIndex == i ? selectedLabelTextStyle : unselectedLabelTextStyle,
|
||||
onTap: () {
|
||||
widget.onDestinationSelected(i);
|
||||
},
|
||||
indexLabel: localizations.tabLabel(
|
||||
tabIndex: i + 1,
|
||||
tabCount: widget.destinations.length,
|
||||
),
|
||||
),
|
||||
if (widget.trailing != null)
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: lerpDouble(minWidth, minExtendedWidth, _extendedAnimation.value),
|
||||
),
|
||||
child: widget.trailing,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _disposeControllers() {
|
||||
for (final AnimationController controller in _destinationControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
_extendedController.dispose();
|
||||
}
|
||||
|
||||
void _initControllers() {
|
||||
_destinationControllers = List<AnimationController>.generate(widget.destinations.length, (int index) {
|
||||
return AnimationController(
|
||||
duration: kThemeAnimationDuration,
|
||||
vsync: this,
|
||||
)..addListener(_rebuild);
|
||||
});
|
||||
_destinationAnimations = _destinationControllers.map((AnimationController controller) => controller.view).toList();
|
||||
_destinationControllers[widget.selectedIndex].value = 1.0;
|
||||
_extendedController = AnimationController(
|
||||
duration: kThemeAnimationDuration,
|
||||
vsync: this,
|
||||
value: widget.extended ? 1.0 : 0.0,
|
||||
);
|
||||
_extendedAnimation = CurvedAnimation(
|
||||
parent: _extendedController,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
_extendedController.addListener(() {
|
||||
_rebuild();
|
||||
});
|
||||
}
|
||||
|
||||
void _resetState() {
|
||||
_disposeControllers();
|
||||
_initControllers();
|
||||
}
|
||||
|
||||
void _rebuild() {
|
||||
setState(() {
|
||||
// Rebuilding when any of the controllers tick, i.e. when the items are
|
||||
// animating.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _RailDestination extends StatelessWidget {
|
||||
_RailDestination({
|
||||
@required this.minWidth,
|
||||
@required this.minExtendedWidth,
|
||||
@required this.icon,
|
||||
@required this.label,
|
||||
@required this.destinationAnimation,
|
||||
@required this.extendedTransitionAnimation,
|
||||
@required this.labelType,
|
||||
@required this.selected,
|
||||
@required this.iconTheme,
|
||||
@required this.labelTextStyle,
|
||||
@required this.onTap,
|
||||
@required this.indexLabel,
|
||||
}) : assert(minWidth != null),
|
||||
assert(minExtendedWidth != null),
|
||||
assert(icon != null),
|
||||
assert(label != null),
|
||||
assert(destinationAnimation != null),
|
||||
assert(extendedTransitionAnimation != null),
|
||||
assert(labelType != null),
|
||||
assert(selected != null),
|
||||
assert(iconTheme != null),
|
||||
assert(labelTextStyle != null),
|
||||
assert(onTap != null),
|
||||
assert(indexLabel != null),
|
||||
_positionAnimation = CurvedAnimation(
|
||||
parent: ReverseAnimation(destinationAnimation),
|
||||
curve: Curves.easeInOut,
|
||||
reverseCurve: Curves.easeInOut.flipped,
|
||||
);
|
||||
|
||||
final double minWidth;
|
||||
final double minExtendedWidth;
|
||||
final Widget icon;
|
||||
final Widget label;
|
||||
final Animation<double> destinationAnimation;
|
||||
final NavigationRailLabelType labelType;
|
||||
final bool selected;
|
||||
final Animation<double> extendedTransitionAnimation;
|
||||
final IconThemeData iconTheme;
|
||||
final TextStyle labelTextStyle;
|
||||
final VoidCallback onTap;
|
||||
final String indexLabel;
|
||||
|
||||
final Animation<double> _positionAnimation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget themedIcon = IconTheme(
|
||||
data: iconTheme,
|
||||
child: icon,
|
||||
);
|
||||
final Widget styledLabel = DefaultTextStyle(
|
||||
style: labelTextStyle,
|
||||
child: label,
|
||||
);
|
||||
Widget content;
|
||||
switch (labelType) {
|
||||
case NavigationRailLabelType.none:
|
||||
final Widget iconPart = SizedBox(
|
||||
width: minWidth,
|
||||
height: minWidth,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: themedIcon,
|
||||
),
|
||||
);
|
||||
if (extendedTransitionAnimation.value == 0) {
|
||||
content = Stack(
|
||||
children: <Widget>[
|
||||
iconPart,
|
||||
// For semantics when label is not showing,
|
||||
SizedBox(
|
||||
width: 0,
|
||||
height: 0,
|
||||
child: Opacity(
|
||||
alwaysIncludeSemantics: true,
|
||||
opacity: 0.0,
|
||||
child: label,
|
||||
),
|
||||
),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
content = ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: lerpDouble(minWidth, minExtendedWidth, extendedTransitionAnimation.value),
|
||||
),
|
||||
child: ClipRect(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
iconPart,
|
||||
Align(
|
||||
heightFactor: 1.0,
|
||||
widthFactor: extendedTransitionAnimation.value,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Opacity(
|
||||
alwaysIncludeSemantics: true,
|
||||
opacity: _extendedLabelFadeValue(),
|
||||
child: styledLabel,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _horizontalDestinationPadding),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case NavigationRailLabelType.selected:
|
||||
final double appearingAnimationValue = 1 - _positionAnimation.value;
|
||||
final double verticalPadding = lerpDouble(_verticalDestinationPaddingNoLabel, _verticalDestinationPaddingWithLabel, appearingAnimationValue);
|
||||
content = Container(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: minWidth,
|
||||
minHeight: minWidth,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
|
||||
child: ClipRect(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SizedBox(height: verticalPadding),
|
||||
themedIcon,
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: appearingAnimationValue,
|
||||
widthFactor: 1.0,
|
||||
child: Opacity(
|
||||
alwaysIncludeSemantics: true,
|
||||
opacity: selected ? _normalLabelFadeInValue() : _normalLabelFadeOutValue(),
|
||||
child: styledLabel,
|
||||
),
|
||||
),
|
||||
SizedBox(height: verticalPadding),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case NavigationRailLabelType.all:
|
||||
content = Container(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: minWidth,
|
||||
minHeight: minWidth,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const SizedBox(height: _verticalDestinationPaddingWithLabel),
|
||||
themedIcon,
|
||||
styledLabel,
|
||||
const SizedBox(height: _verticalDestinationPaddingWithLabel),
|
||||
],
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
final ColorScheme colors = Theme.of(context).colorScheme;
|
||||
return Semantics(
|
||||
container: true,
|
||||
selected: selected,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
clipBehavior: Clip.none,
|
||||
child: InkResponse(
|
||||
onTap: onTap,
|
||||
onHover: (_) {},
|
||||
highlightShape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.all(Radius.circular(minWidth / 2.0)),
|
||||
containedInkWell: true,
|
||||
splashColor: colors.primary.withOpacity(0.12),
|
||||
hoverColor: colors.primary.withOpacity(0.04),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
label: indexLabel,
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _normalLabelFadeInValue() {
|
||||
if (destinationAnimation.value < 0.25) {
|
||||
return 0;
|
||||
} else if (destinationAnimation.value < 0.75) {
|
||||
return (destinationAnimation.value - 0.25) * 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
double _normalLabelFadeOutValue() {
|
||||
if (destinationAnimation.value > 0.75) {
|
||||
return (destinationAnimation.value - 0.75) * 4.0;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
double _extendedLabelFadeValue() {
|
||||
return extendedTransitionAnimation.value < 0.25 ? extendedTransitionAnimation.value * 4.0 : 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the behavior of the labels of a [NavigationRail].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationRail]
|
||||
enum NavigationRailLabelType {
|
||||
/// Only the [NavigationRailDestination]s are shown.
|
||||
none,
|
||||
|
||||
/// Only the selected [NavigationRailDestination] will show its label.
|
||||
///
|
||||
/// The label will animate in and out as new [NavigationRailDestination]s are
|
||||
/// selected.
|
||||
selected,
|
||||
|
||||
/// All [NavigationRailDestination]s will show their label.
|
||||
all,
|
||||
}
|
||||
|
||||
/// Defines a [NavigationRail] button that represents one "destination" view.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationRail]
|
||||
class NavigationRailDestination {
|
||||
/// Creates a destination that is used with [NavigationRail.destinations].
|
||||
///
|
||||
/// [icon] and [label] must be non-null. When the [NavigationRail.labelType]
|
||||
/// is [NavigationRailLabelType.none], the label is still used for semantics,
|
||||
/// and may still be used if [NavigationRail.extended] is true.
|
||||
const NavigationRailDestination({
|
||||
@required this.icon,
|
||||
Widget selectedIcon,
|
||||
this.label,
|
||||
}) : selectedIcon = selectedIcon ?? icon,
|
||||
assert(icon != null);
|
||||
|
||||
/// The icon of the destination.
|
||||
///
|
||||
/// Typically the icon is an [Icon] or an [ImageIcon] widget. If another type
|
||||
/// of widget is provided then it should configure itself to match the current
|
||||
/// [IconTheme] size and color.
|
||||
///
|
||||
/// If [selectedIcon] is provided, this will only be displayed when the
|
||||
/// destination is not selected.
|
||||
///
|
||||
/// To make the [NavigationRail] more accessible, consider choosing an
|
||||
/// icon with a stroked and filled version, such as [Icons.cloud] and
|
||||
/// [Icons.cloud_queue]. The [icon] should be set to the stroked version and
|
||||
/// [selectedIcon] to the filled version.
|
||||
final Widget icon;
|
||||
|
||||
/// An alternative icon displayed when this destination is selected.
|
||||
///
|
||||
/// If this icon is not provided, the [NavigationRail] will display [icon] in
|
||||
/// either state. The size, color, and opacity of the
|
||||
/// [NavigationRail.selectedIconTheme] will still apply.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationRailDestination.icon], for a description of how to pair
|
||||
/// icons.
|
||||
final Widget selectedIcon;
|
||||
|
||||
/// The label for the destination.
|
||||
///
|
||||
/// The label must be provided when used with the [NavigationRail]. When the
|
||||
/// [NavigationRail.labelType] is [NavigationRailLabelType.none], the label is
|
||||
/// still used for semantics, and may still be used if
|
||||
/// [NavigationRail.extended] is true.
|
||||
final Widget label;
|
||||
}
|
||||
|
||||
class _ExtendedNavigationRailAnimation extends InheritedWidget {
|
||||
const _ExtendedNavigationRailAnimation({
|
||||
Key key,
|
||||
@required this.animation,
|
||||
@required Widget child,
|
||||
}) : assert(child != null),
|
||||
super(key: key, child: child);
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_ExtendedNavigationRailAnimation old) => animation != old.animation;
|
||||
}
|
||||
|
||||
const double _minRailWidth = 72.0;
|
||||
const double _minExtendedRailWidth = 256.0;
|
||||
const double _horizontalDestinationPadding = 8.0;
|
||||
const double _verticalDestinationPaddingNoLabel = 24.0;
|
||||
const double _verticalDestinationPaddingWithLabel = 16.0;
|
||||
const Widget _verticalSpacer = SizedBox(height: 8.0);
|
215
packages/flutter/lib/src/material/navigation_rail_theme.dart
Normal file
215
packages/flutter/lib/src/material/navigation_rail_theme.dart
Normal file
@ -0,0 +1,215 @@
|
||||
// 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 'navigation_rail.dart';
|
||||
import 'theme.dart';
|
||||
import 'theme_data.dart';
|
||||
|
||||
/// Defines default property values for descendant [NavigationRail]
|
||||
/// widgets.
|
||||
///
|
||||
/// Descendant widgets obtain the current [NavigationRailThemeData] object
|
||||
/// using `NavigationRailTheme.of(context)`. Instances of
|
||||
/// [NavigationRailThemeData] can be customized with
|
||||
/// [NavigationRailThemeData.copyWith].
|
||||
///
|
||||
/// Typically a [NavigationRailThemeData] is specified as part of the
|
||||
/// overall [Theme] with [ThemeData.navigationRailTheme].
|
||||
///
|
||||
/// All [NavigationRailThemeData] properties are `null` by default.
|
||||
/// When null, the [NavigationRail] will use the values from [ThemeData]
|
||||
/// if they exist, otherwise it will provide its own defaults based on the
|
||||
/// overall [Theme]'s textTheme and colorScheme. See the individual
|
||||
/// [NavigationRail] properties for details.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ThemeData], which describes the overall theme information for the
|
||||
/// application.
|
||||
class NavigationRailThemeData with Diagnosticable {
|
||||
/// Creates a theme that can be used for [ThemeData.navigationRailTheme].
|
||||
const NavigationRailThemeData({
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.unselectedLabelTextStyle,
|
||||
this.selectedLabelTextStyle,
|
||||
this.unselectedIconTheme,
|
||||
this.selectedIconTheme,
|
||||
this.groupAlignment,
|
||||
this.labelType,
|
||||
});
|
||||
|
||||
/// Color to be used for the [NavigationRail]'s background.
|
||||
final Color backgroundColor;
|
||||
|
||||
/// The z-coordinate to be used for the [NavigationRail]'s elevation.
|
||||
final double elevation;
|
||||
|
||||
/// The style to merge with the default text style for
|
||||
/// [NavigationRailDestination] labels, when the destination is not selected.
|
||||
final TextStyle unselectedLabelTextStyle;
|
||||
|
||||
/// The style to merge with the default text style for
|
||||
/// [NavigationRailDestination] labels, when the destination is selected.
|
||||
final TextStyle selectedLabelTextStyle;
|
||||
|
||||
/// The theme to merge with the default icon theme for
|
||||
/// [NavigationRailDestination] icons, when the destination is not selected.
|
||||
final IconThemeData unselectedIconTheme;
|
||||
|
||||
/// The theme to merge with the default icon theme for
|
||||
/// [NavigationRailDestination] icons, when the destination is selected.
|
||||
final IconThemeData selectedIconTheme;
|
||||
|
||||
/// The alignment for the [NavigationRailDestination]s as they are positioned
|
||||
/// within the [NavigationRail].
|
||||
final double groupAlignment;
|
||||
|
||||
/// The type that defines the layout and behavior of the labels in the
|
||||
/// [NavigationRail].
|
||||
final NavigationRailLabelType labelType;
|
||||
|
||||
/// Creates a copy of this object with the given fields replaced with the
|
||||
/// new values.
|
||||
NavigationRailThemeData copyWith({
|
||||
Color backgroundColor,
|
||||
double elevation,
|
||||
TextStyle unselectedLabelTextStyle,
|
||||
TextStyle selectedLabelTextStyle,
|
||||
IconThemeData unselectedIconTheme,
|
||||
IconThemeData selectedIconTheme,
|
||||
double groupAlignment,
|
||||
NavigationRailLabelType labelType,
|
||||
}) {
|
||||
return NavigationRailThemeData(
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
elevation: elevation ?? this.elevation,
|
||||
unselectedLabelTextStyle: unselectedLabelTextStyle ?? this.unselectedLabelTextStyle,
|
||||
selectedLabelTextStyle: selectedLabelTextStyle ?? this.selectedLabelTextStyle,
|
||||
unselectedIconTheme: unselectedIconTheme ?? this.unselectedIconTheme,
|
||||
selectedIconTheme: selectedIconTheme ?? this.selectedIconTheme,
|
||||
groupAlignment: groupAlignment ?? this.groupAlignment,
|
||||
labelType: labelType ?? this.labelType,
|
||||
);
|
||||
}
|
||||
|
||||
/// Linearly interpolate between two navigation rail themes.
|
||||
///
|
||||
/// If both arguments are null then null is returned.
|
||||
///
|
||||
/// {@macro dart.ui.shadow.lerp}
|
||||
static NavigationRailThemeData lerp(NavigationRailThemeData a, NavigationRailThemeData b, double t) {
|
||||
assert(t != null);
|
||||
if (a == null && b == null)
|
||||
return null;
|
||||
return NavigationRailThemeData(
|
||||
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
|
||||
elevation: lerpDouble(a?.elevation, b?.elevation, t),
|
||||
unselectedLabelTextStyle: TextStyle.lerp(a?.unselectedLabelTextStyle, b?.unselectedLabelTextStyle, t),
|
||||
selectedLabelTextStyle: TextStyle.lerp(a?.selectedLabelTextStyle, b?.selectedLabelTextStyle, t),
|
||||
unselectedIconTheme: IconThemeData.lerp(a?.unselectedIconTheme, b?.unselectedIconTheme, t),
|
||||
selectedIconTheme: IconThemeData.lerp(a?.selectedIconTheme, b?.selectedIconTheme, t),
|
||||
groupAlignment: lerpDouble(a?.groupAlignment, b?.groupAlignment, t),
|
||||
labelType: t < 0.5 ? a?.labelType : b?.labelType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return hashValues(
|
||||
backgroundColor,
|
||||
elevation,
|
||||
unselectedLabelTextStyle,
|
||||
selectedLabelTextStyle,
|
||||
unselectedIconTheme,
|
||||
selectedIconTheme,
|
||||
groupAlignment,
|
||||
labelType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other.runtimeType != runtimeType)
|
||||
return false;
|
||||
return other is NavigationRailThemeData
|
||||
&& other.backgroundColor == backgroundColor
|
||||
&& other.elevation == elevation
|
||||
&& other.unselectedLabelTextStyle == unselectedLabelTextStyle
|
||||
&& other.selectedLabelTextStyle == selectedLabelTextStyle
|
||||
&& other.unselectedIconTheme == unselectedIconTheme
|
||||
&& other.selectedIconTheme == selectedIconTheme
|
||||
&& other.groupAlignment == groupAlignment
|
||||
&& other.labelType == labelType;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
const NavigationRailThemeData defaultData = NavigationRailThemeData();
|
||||
|
||||
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor));
|
||||
properties.add(DoubleProperty('elevation', elevation, defaultValue: defaultData.elevation));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('unselectedLabelTextStyle', unselectedLabelTextStyle, defaultValue: defaultData.unselectedLabelTextStyle));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('selectedLabelTextStyle', selectedLabelTextStyle, defaultValue: defaultData.selectedLabelTextStyle));
|
||||
properties.add(DiagnosticsProperty<IconThemeData>('unselectedIconTheme', unselectedIconTheme, defaultValue: defaultData.unselectedIconTheme));
|
||||
properties.add(DiagnosticsProperty<IconThemeData>('selectedIconTheme', selectedIconTheme, defaultValue: defaultData.selectedIconTheme));
|
||||
properties.add(DoubleProperty('groupAlignment', groupAlignment, defaultValue: defaultData.groupAlignment));
|
||||
properties.add(DiagnosticsProperty<NavigationRailLabelType>('labelType', labelType, defaultValue: defaultData.labelType));
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines visual properties for [NavigationRail]s and
|
||||
/// [NavigationRailDestination]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [NavigationRail] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class NavigationRailTheme extends InheritedTheme {
|
||||
/// Creates a navigation rail theme that controls the
|
||||
/// [NavigationRailThemeData] properties for a [NavigationRail].
|
||||
///
|
||||
/// The data argument must not be null.
|
||||
const NavigationRailTheme({
|
||||
Key key,
|
||||
@required this.data,
|
||||
Widget child,
|
||||
}) : assert(data != null), super(key: key, child: child);
|
||||
|
||||
/// Specifies the background color, elevation, label text style, icon theme,
|
||||
/// group alignment, and label type and border values for descendant
|
||||
/// [NavigationRail] widgets.
|
||||
final NavigationRailThemeData data;
|
||||
|
||||
/// The closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// If there is no enclosing [NavigationRailTheme] widget, then
|
||||
/// [ThemeData.navigationRailTheme] is used.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// NavigationRailTheme theme = NavigationRailTheme.of(context);
|
||||
/// ```
|
||||
static NavigationRailThemeData of(BuildContext context) {
|
||||
final NavigationRailTheme navigationRailTheme = context.dependOnInheritedWidgetOfExactType<NavigationRailTheme>();
|
||||
return navigationRailTheme?.data ?? Theme.of(context).navigationRailTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
final NavigationRailTheme ancestorTheme = context.findAncestorWidgetOfExactType<NavigationRailTheme>();
|
||||
return identical(this, ancestorTheme) ? child : NavigationRailTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(NavigationRailTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
@ -25,6 +25,7 @@ import 'floating_action_button_theme.dart';
|
||||
import 'ink_splash.dart';
|
||||
import 'ink_well.dart' show InteractiveInkFeatureFactory;
|
||||
import 'input_decorator.dart';
|
||||
import 'navigation_rail_theme.dart';
|
||||
import 'page_transitions_theme.dart';
|
||||
import 'popup_menu_theme.dart';
|
||||
import 'slider_theme.dart';
|
||||
@ -256,6 +257,7 @@ class ThemeData with Diagnosticable {
|
||||
ColorScheme colorScheme,
|
||||
DialogTheme dialogTheme,
|
||||
FloatingActionButtonThemeData floatingActionButtonTheme,
|
||||
NavigationRailThemeData navigationRailTheme,
|
||||
Typography typography,
|
||||
CupertinoThemeData cupertinoOverrideTheme,
|
||||
SnackBarThemeData snackBarTheme,
|
||||
@ -364,6 +366,7 @@ class ThemeData with Diagnosticable {
|
||||
);
|
||||
dialogTheme ??= const DialogTheme();
|
||||
floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
|
||||
navigationRailTheme ??= const NavigationRailThemeData();
|
||||
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
||||
snackBarTheme ??= const SnackBarThemeData();
|
||||
bottomSheetTheme ??= const BottomSheetThemeData();
|
||||
@ -428,6 +431,7 @@ class ThemeData with Diagnosticable {
|
||||
colorScheme: colorScheme,
|
||||
dialogTheme: dialogTheme,
|
||||
floatingActionButtonTheme: floatingActionButtonTheme,
|
||||
navigationRailTheme: navigationRailTheme,
|
||||
typography: typography,
|
||||
cupertinoOverrideTheme: cupertinoOverrideTheme,
|
||||
snackBarTheme: snackBarTheme,
|
||||
@ -505,6 +509,7 @@ class ThemeData with Diagnosticable {
|
||||
@required this.colorScheme,
|
||||
@required this.dialogTheme,
|
||||
@required this.floatingActionButtonTheme,
|
||||
@required this.navigationRailTheme,
|
||||
@required this.typography,
|
||||
@required this.cupertinoOverrideTheme,
|
||||
@required this.snackBarTheme,
|
||||
@ -566,6 +571,7 @@ class ThemeData with Diagnosticable {
|
||||
assert(colorScheme != null),
|
||||
assert(dialogTheme != null),
|
||||
assert(floatingActionButtonTheme != null),
|
||||
assert(navigationRailTheme != null),
|
||||
assert(typography != null),
|
||||
assert(snackBarTheme != null),
|
||||
assert(bottomSheetTheme != null),
|
||||
@ -982,6 +988,10 @@ class ThemeData with Diagnosticable {
|
||||
/// [FloatingActionButton].
|
||||
final FloatingActionButtonThemeData floatingActionButtonTheme;
|
||||
|
||||
/// A theme for customizing the background color, elevation, text style, and
|
||||
/// icon themes of a [NavigationRail].
|
||||
final NavigationRailThemeData navigationRailTheme;
|
||||
|
||||
/// The color and geometry [TextTheme] values used to configure [textTheme],
|
||||
/// [primaryTextTheme], and [accentTextTheme].
|
||||
final Typography typography;
|
||||
@ -1072,6 +1082,7 @@ class ThemeData with Diagnosticable {
|
||||
ColorScheme colorScheme,
|
||||
DialogTheme dialogTheme,
|
||||
FloatingActionButtonThemeData floatingActionButtonTheme,
|
||||
NavigationRailThemeData navigationRailTheme,
|
||||
Typography typography,
|
||||
CupertinoThemeData cupertinoOverrideTheme,
|
||||
SnackBarThemeData snackBarTheme,
|
||||
@ -1138,6 +1149,7 @@ class ThemeData with Diagnosticable {
|
||||
colorScheme: colorScheme ?? this.colorScheme,
|
||||
dialogTheme: dialogTheme ?? this.dialogTheme,
|
||||
floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
|
||||
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
|
||||
typography: typography ?? this.typography,
|
||||
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
|
||||
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
|
||||
@ -1282,6 +1294,7 @@ class ThemeData with Diagnosticable {
|
||||
colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t),
|
||||
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t),
|
||||
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t),
|
||||
typography: Typography.lerp(a.typography, b.typography, t),
|
||||
cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
|
||||
snackBarTheme: SnackBarThemeData.lerp(a.snackBarTheme, b.snackBarTheme, t),
|
||||
@ -1354,6 +1367,7 @@ class ThemeData with Diagnosticable {
|
||||
&& other.colorScheme == colorScheme
|
||||
&& other.dialogTheme == dialogTheme
|
||||
&& other.floatingActionButtonTheme == floatingActionButtonTheme
|
||||
&& other.navigationRailTheme == navigationRailTheme
|
||||
&& other.typography == typography
|
||||
&& other.cupertinoOverrideTheme == cupertinoOverrideTheme
|
||||
&& other.snackBarTheme == snackBarTheme
|
||||
@ -1425,6 +1439,7 @@ class ThemeData with Diagnosticable {
|
||||
colorScheme,
|
||||
dialogTheme,
|
||||
floatingActionButtonTheme,
|
||||
navigationRailTheme,
|
||||
typography,
|
||||
cupertinoOverrideTheme,
|
||||
snackBarTheme,
|
||||
@ -1492,6 +1507,7 @@ class ThemeData with Diagnosticable {
|
||||
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<FloatingActionButtonThemeData>('floatingActionButtonThemeData', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, 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<CupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<SnackBarThemeData>('snackBarTheme', snackBarTheme, defaultValue: defaultData.snackBarTheme, level: DiagnosticLevel.debug));
|
||||
|
2151
packages/flutter/test/material/navigation_rail_test.dart
Normal file
2151
packages/flutter/test/material/navigation_rail_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
319
packages/flutter/test/material/navigation_rail_theme_test.dart
Normal file
319
packages/flutter/test/material/navigation_rail_theme_test.dart
Normal file
@ -0,0 +1,319 @@
|
||||
// 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/foundation.dart';
|
||||
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 NavigationRailThemeData(), const NavigationRailThemeData().copyWith());
|
||||
expect(const NavigationRailThemeData().hashCode, const NavigationRailThemeData().copyWith().hashCode);
|
||||
});
|
||||
|
||||
testWidgets('Default values are used when no NavigationRail or NavigationRailThemeData properties are specified', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: NavigationRail(
|
||||
selectedIndex: 0,
|
||||
destinations: _destinations(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(_railMaterial(tester).color, ThemeData().colorScheme.surface);
|
||||
expect(_railMaterial(tester).elevation, 0);
|
||||
expect(_selectedIconTheme(tester).size, 24.0);
|
||||
expect(_selectedIconTheme(tester).color, ThemeData().colorScheme.primary);
|
||||
expect(_selectedIconTheme(tester).opacity, 0.64);
|
||||
expect(_unselectedIconTheme(tester).size, 24.0);
|
||||
expect(_unselectedIconTheme(tester).color, ThemeData().colorScheme.onSurface);
|
||||
expect(_unselectedIconTheme(tester).opacity, 1.0);
|
||||
expect(_selectedLabelStyle(tester).fontSize, 14.0);
|
||||
expect(_unselectedLabelStyle(tester).fontSize, 14.0);
|
||||
expect(_destinationsAlign(tester).alignment, Alignment.topCenter);
|
||||
expect(_labelType(tester), NavigationRailLabelType.none);
|
||||
});
|
||||
|
||||
testWidgets('NavigationRailThemeData values are used when no NavigationRail properties are specified', (WidgetTester tester) async {
|
||||
const Color backgroundColor = Color(0x00000001);
|
||||
const double elevation = 7.0;
|
||||
const double selectedIconSize = 25.0;
|
||||
const double unselectedIconSize = 23.0;
|
||||
const Color selectedIconColor = Color(0x00000002);
|
||||
const Color unselectedIconColor = Color(0x00000003);
|
||||
const double selectedIconOpacity = 0.99;
|
||||
const double unselectedIconOpacity = 0.98;
|
||||
const double selectedLabelFontSize = 13.0;
|
||||
const double unselectedLabelFontSize = 11.0;
|
||||
const double groupAlignment = 0.0;
|
||||
const NavigationRailLabelType labelType = NavigationRailLabelType.all;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: NavigationRailTheme(
|
||||
data: const NavigationRailThemeData(
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
selectedIconTheme: IconThemeData(
|
||||
size: selectedIconSize,
|
||||
color: selectedIconColor,
|
||||
opacity: selectedIconOpacity,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
size: unselectedIconSize,
|
||||
color: unselectedIconColor,
|
||||
opacity: unselectedIconOpacity,
|
||||
),
|
||||
selectedLabelTextStyle: TextStyle(fontSize: selectedLabelFontSize),
|
||||
unselectedLabelTextStyle: TextStyle(fontSize: unselectedLabelFontSize),
|
||||
groupAlignment: groupAlignment,
|
||||
labelType: labelType,
|
||||
),
|
||||
child: NavigationRail(
|
||||
selectedIndex: 0,
|
||||
destinations: _destinations(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(_railMaterial(tester).color, backgroundColor);
|
||||
expect(_railMaterial(tester).elevation, elevation);
|
||||
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(_destinationsAlign(tester).alignment, Alignment.center);
|
||||
expect(_labelType(tester), labelType);
|
||||
});
|
||||
|
||||
testWidgets('NavigationRail values take priority over NavigationRailThemeData values when both properties are specified', (WidgetTester tester) async {
|
||||
const Color backgroundColor = Color(0x00000001);
|
||||
const double elevation = 7.0;
|
||||
const double selectedIconSize = 25.0;
|
||||
const double unselectedIconSize = 23.0;
|
||||
const Color selectedIconColor = Color(0x00000002);
|
||||
const Color unselectedIconColor = Color(0x00000003);
|
||||
const double selectedIconOpacity = 0.99;
|
||||
const double unselectedIconOpacity = 0.98;
|
||||
const double selectedLabelFontSize = 13.0;
|
||||
const double unselectedLabelFontSize = 11.0;
|
||||
const double groupAlignment = 0.0;
|
||||
const NavigationRailLabelType labelType = NavigationRailLabelType.all;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: NavigationRailTheme(
|
||||
data: const NavigationRailThemeData(
|
||||
backgroundColor: Color(0x00000099),
|
||||
elevation: 5,
|
||||
selectedIconTheme: IconThemeData(
|
||||
size: 31.0,
|
||||
color: Color(0x00000098),
|
||||
opacity: 0.81,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
size: 37.0,
|
||||
color: Color(0x00000097),
|
||||
opacity: 0.82,
|
||||
),
|
||||
selectedLabelTextStyle: TextStyle(fontSize: 9.0),
|
||||
unselectedLabelTextStyle: TextStyle(fontSize: 7.0),
|
||||
groupAlignment: 1.0,
|
||||
labelType: NavigationRailLabelType.selected,
|
||||
),
|
||||
child: NavigationRail(
|
||||
selectedIndex: 0,
|
||||
destinations: _destinations(),
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
selectedIconTheme: const IconThemeData(
|
||||
size: selectedIconSize,
|
||||
color: selectedIconColor,
|
||||
opacity: selectedIconOpacity,
|
||||
),
|
||||
unselectedIconTheme: const IconThemeData(
|
||||
size: unselectedIconSize,
|
||||
color: unselectedIconColor,
|
||||
opacity: unselectedIconOpacity,
|
||||
),
|
||||
selectedLabelTextStyle: const TextStyle(fontSize: selectedLabelFontSize),
|
||||
unselectedLabelTextStyle: const TextStyle(fontSize: unselectedLabelFontSize),
|
||||
groupAlignment: groupAlignment,
|
||||
labelType: labelType,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(_railMaterial(tester).color, backgroundColor);
|
||||
expect(_railMaterial(tester).elevation, elevation);
|
||||
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(_destinationsAlign(tester).alignment, Alignment.center);
|
||||
expect(_labelType(tester), labelType);
|
||||
});
|
||||
|
||||
testWidgets('Default debugFillProperties', (WidgetTester tester) async {
|
||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||
const NavigationRailThemeData().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();
|
||||
const NavigationRailThemeData(
|
||||
backgroundColor: Color(0x00000099),
|
||||
elevation: 5,
|
||||
selectedIconTheme: IconThemeData(color: Color(0x00000098)),
|
||||
unselectedIconTheme: IconThemeData(color: Color(0x00000097)),
|
||||
selectedLabelTextStyle: TextStyle(fontSize: 9.0),
|
||||
unselectedLabelTextStyle: TextStyle(fontSize: 7.0),
|
||||
groupAlignment: 1.0,
|
||||
labelType: NavigationRailLabelType.selected,
|
||||
).debugFillProperties(builder);
|
||||
|
||||
final List<String> description = builder.properties
|
||||
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
|
||||
.map((DiagnosticsNode node) => node.toString())
|
||||
.toList();
|
||||
|
||||
expect(description[0], 'backgroundColor: Color(0x00000099)');
|
||||
expect(description[1], 'elevation: 5.0');
|
||||
expect(description[2], 'unselectedLabelTextStyle: TextStyle(inherit: true, size: 7.0)');
|
||||
expect(description[3], 'selectedLabelTextStyle: TextStyle(inherit: true, size: 9.0)');
|
||||
|
||||
// Ignore instance address for IconThemeData.
|
||||
expect(description[4].contains('unselectedIconTheme: IconThemeData'), isTrue);
|
||||
expect(description[4].contains('(color: Color(0x00000097))'), isTrue);
|
||||
expect(description[5].contains('selectedIconTheme: IconThemeData'), isTrue);
|
||||
expect(description[5].contains('(color: Color(0x00000098))'), isTrue);
|
||||
|
||||
expect(description[6], 'groupAlignment: 1.0');
|
||||
expect(description[7], 'labelType: NavigationRailLabelType.selected');
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
List<NavigationRailDestination> _destinations() {
|
||||
return const <NavigationRailDestination>[
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.favorite_border),
|
||||
selectedIcon: Icon(Icons.favorite),
|
||||
label: Text('Abc'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.star_border),
|
||||
selectedIcon: Icon(Icons.star),
|
||||
label: Text('Def'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Material _railMaterial(WidgetTester tester) {
|
||||
// The first material is for the rail, and the rest are for the destinations.
|
||||
return tester.firstWidget<Material>(
|
||||
find.descendant(
|
||||
of: find.byType(NavigationRail),
|
||||
matching: find.byType(Material),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
// The first IconTheme is the one added by the navigation rail.
|
||||
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;
|
||||
}
|
||||
|
||||
Align _destinationsAlign(WidgetTester tester) {
|
||||
// The first Expanded widget is the one within the main Column for the rail
|
||||
// content.
|
||||
return tester.firstWidget<Align>(
|
||||
find.descendant(
|
||||
of: find.byType(Expanded),
|
||||
matching: find.byType(Align),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
NavigationRailLabelType _labelType(WidgetTester tester) {
|
||||
if (_opacityAboveLabel('Abc').evaluate().isNotEmpty && _opacityAboveLabel('Def').evaluate().isNotEmpty) {
|
||||
return _labelOpacity(tester, 'Abc') == 1 ? NavigationRailLabelType.selected : NavigationRailLabelType.none;
|
||||
} else {
|
||||
return NavigationRailLabelType.all;
|
||||
}
|
||||
}
|
||||
|
||||
Finder _opacityAboveLabel(String text) {
|
||||
return find.ancestor(
|
||||
of: find.text(text),
|
||||
matching: find.byType(Opacity),
|
||||
);
|
||||
}
|
||||
|
||||
// Only valid when labelType != all.
|
||||
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;
|
||||
}
|
@ -256,6 +256,7 @@ void main() {
|
||||
colorScheme: const ColorScheme.light(),
|
||||
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
|
||||
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
|
||||
typography: Typography.material2018(platform: TargetPlatform.android),
|
||||
cupertinoOverrideTheme: null,
|
||||
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.black),
|
||||
@ -335,6 +336,7 @@ void main() {
|
||||
colorScheme: const ColorScheme.light(),
|
||||
dialogTheme: const DialogTheme(backgroundColor: Colors.white),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
|
||||
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
|
||||
typography: Typography.material2018(platform: TargetPlatform.iOS),
|
||||
cupertinoOverrideTheme: ThemeData.light().cupertinoOverrideTheme,
|
||||
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.white),
|
||||
@ -400,6 +402,7 @@ void main() {
|
||||
colorScheme: otherTheme.colorScheme,
|
||||
dialogTheme: otherTheme.dialogTheme,
|
||||
floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
|
||||
navigationRailTheme: otherTheme.navigationRailTheme,
|
||||
typography: otherTheme.typography,
|
||||
cupertinoOverrideTheme: otherTheme.cupertinoOverrideTheme,
|
||||
snackBarTheme: otherTheme.snackBarTheme,
|
||||
@ -466,6 +469,7 @@ void main() {
|
||||
expect(themeDataCopy.colorScheme, equals(otherTheme.colorScheme));
|
||||
expect(themeDataCopy.dialogTheme, equals(otherTheme.dialogTheme));
|
||||
expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
|
||||
expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme));
|
||||
expect(themeDataCopy.typography, equals(otherTheme.typography));
|
||||
expect(themeDataCopy.cupertinoOverrideTheme, equals(otherTheme.cupertinoOverrideTheme));
|
||||
expect(themeDataCopy.snackBarTheme, equals(otherTheme.snackBarTheme));
|
||||
|
Loading…
Reference in New Issue
Block a user