| // 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); |