[web_dashboard] Navigation rail update (#356)

* add new navigation_rail, update adaptive_scaffold

* add outdated routing_demo

* Add AdaptiveScaffold experiment

* clean up experimental/ directory

* remove web_dashboard from CI script

new NavigationRail widget requires the master channel
pull/365/head
John Ryan 5 years ago committed by GitHub
parent 0a5a5109de
commit e24fef3af5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,35 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:web_dashboard/src/widgets/third_party/adaptive_scaffold.dart';
void main() {
runApp(DashboardWithoutRoutes());
}
class DashboardWithoutRoutes extends StatefulWidget {
@override
_DashboardWithoutRoutesState createState() => _DashboardWithoutRoutesState();
}
class _DashboardWithoutRoutesState extends State<DashboardWithoutRoutes> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AdaptiveScaffold(
currentIndex: 0,
destinations: [
AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home),
AdaptiveScaffoldDestination(title: 'Metrics', icon: Icons.show_chart),
AdaptiveScaffoldDestination(title: 'Settings', icon: Icons.settings),
],
body: Center(
child: Text('Hello, World!'),
),
),
);
}
}

@ -105,7 +105,7 @@ class _AdaptiveScaffoldState extends State<AdaptiveScaffold> {
...widget.destinations.map( ...widget.destinations.map(
(d) => NavigationRailDestination( (d) => NavigationRailDestination(
icon: Icon(d.icon), icon: Icon(d.icon),
title: Text(d.title), label: Text(d.title),
), ),
), ),
], ],

@ -1,119 +1,168 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file // Copyright 2014 The Flutter Authors. All rights reserved.
// for details. All rights reserved. Use of this source code is governed by a // Use of this source code is governed by a BSD-style license that can be
// BSD-style license that can be found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
/// Original pull request: https://github.com/flutter/flutter/pull/49574
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
/// Defines the behavior of the labels of a [NavigationRail]. import 'navigation_rail_theme.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.
/// ///
/// See also: /// A navigation rail is usually used inside a [Row] of a [Scaffold] body.
/// ///
/// * [NavigationRail] /// The navigation rail is meant for layouts with wide viewports, such as a
enum NavigationRailLabelType { /// desktop web or tablet landscape layout. For smaller layouts, like mobile
/// Only the icons of a navigation rail item are shown. /// portrait, a [BottomNavigationBar] should be used instead. Adaptive layouts
none, /// can build different instances of the [Scaffold] in order to have a
/// navigation rail for more horizontal layouts and a bottom navigation bar
/// Only the selected navigation rail item will show its label. /// for more vertical layouts.
/// ///
/// The label will animate in and out as new items are selected. /// {@tool dartpad --template=stateful_widget_material}
selected,
/// All navigation rail items will show their label.
all,
}
/// Defines the alignment for the group of [NavigationRailDestination]s within
/// a [NavigationRail].
/// ///
/// Navigation rail destinations can be aligned as a group to the [top], /// This example shows a [NavigationRail] used within a Scaffold with 3
/// [bottom], or [center] of a layout. /// [NavigationRailDestination]s. The main content is separated by a divider
enum NavigationRailGroupAlignment { /// (although elevation on the navigation rail can be used instead). The
/// Place the [NavigationRailDestination]s at the top of the rail. /// `_currentIndex` updates according to the `onDestinationSelected` callback.
top,
/// Place the [NavigationRailDestination]s in the center of the rail.
center,
/// Place the [NavigationRailDestination]s at the bottom of the rail.
bottom,
}
/// A description for an interactive button within a [NavigationRail].
/// ///
/// See also: /// ```dart
/// int _currentIndex = 0;
/// ///
/// * [NavigationRail] /// @override
class NavigationRailDestination { /// Widget build(BuildContext context) {
/// Creates an destination that is used with [NavigationRail.destinations]. /// return Scaffold(
/// body: Row(
/// children: <Widget>[
/// NavigationRail(
/// currentIndex: _currentIndex,
/// labelType: NavigationRailLabelType.selected,
/// destinations: [
/// NavigationRailDestination(
/// icon: Icon(Icons.favorite_border),
/// activeIcon: Icon(Icons.favorite),
/// label: Text('First'),
/// ),
/// NavigationRailDestination(
/// icon: Icon(Icons.bookmark_border),
/// activeIcon: Icon(Icons.book),
/// label: Text('Second'),
/// ),
/// NavigationRailDestination(
/// icon: Icon(Icons.star_border),
/// activeIcon: Icon(Icons.star),
/// label: Text('Third'),
/// ),
/// ],
/// onDestinationSelected: (int index) {
/// setState(() {
/// _currentIndex = index;
/// });
/// },
/// ),
/// VerticalDivider(thickness: 1, width: 1),
/// Expanded(
/// child: Center(
/// child: Text('currentIndex: $_currentIndex'),
/// ),
/// )
/// ],
/// ),
/// );
/// }
/// ```
/// {@end-tool}
/// ///
/// [icon] should not be null and [title] should not be null when this /// See also:
/// destination is used in the [NavigationRail].
const NavigationRailDestination({
@required this.icon,
Widget activeIcon,
this.title,
}) : activeIcon = activeIcon ?? icon,
assert(icon != null);
/// The icon of the destination.
/// ///
/// Typically the icon is an [Icon] or an [ImageIcon] widget. If another type /// * [Scaffold], which can display the navigation rail within a [Row] of the
/// of widget is provided then it should configure itself to match the current /// [Scaffold.body] slot.
/// [IconTheme] size and color. /// * [NavigationRailDestination], which is used as a model to create tappable
/// destinations in the navigation rail.
/// * [BottomNavigationBar], which is used as a horizontal alternative for
/// the same style of navigation as the navigation rail.
class NavigationRail extends StatefulWidget {
/// Creates a material design navigation rail.
/// ///
/// If [activeIcon] is provided, this will only be displayed when the /// The argument [destinations] must not be null. Additionally, it must be
/// destination is not selected. /// non-empty.
/// ///
/// To make the [NavigationRail] more accessible, consider choosing an /// If [elevation] is specified, it must be non-negative.
/// icon with a stroked and filled version, such as [Icons.cloud] and
/// [Icons.cloud_queue]. [icon] should be set to the stroked version and
/// [activeIcon] 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 /// If [preferredWidth] is specified, it must be non-negative, and if
/// either state. /// [extendedWidth is specified, it must be non-negative and greater than
/// [preferredWidth].
/// ///
/// See also: /// The argument [extended] must not be null. [extended] can only be set to
/// true when when the [labelType] is null or [NavigationRailLabelType.none].
/// ///
/// * [NavigationRailDestination.icon], for a description of how to pair /// If [backgroundColor], [elevation], [groupAlignment], [labelType],
/// icons. /// [unselectedLabelTextStyle], [unselectedLabelTextStyle],
final Widget activeIcon; /// [unselectedIconTheme], or [selectedIconTheme] are null, then their
/// [NavigationRailThemeData] values will be used. If the corresponding
/// The title of the item. If the title is not provided only the icon will be /// [NavigationRailThemeData] property is null, then the navigation rail
/// shown when not used in a [NavigationRail]. /// defaults are used.
final Widget title; ///
} /// Typically used within a [Row] of the [Scaffold.body] property.
/// TODO
class NavigationRail extends StatefulWidget {
/// TODO
NavigationRail({ NavigationRail({
this.backgroundColor,
this.extended = false,
this.leading, this.leading,
this.destinations, this.trailing,
this.currentIndex, @required this.destinations,
this.currentIndex = 0,
this.onDestinationSelected, this.onDestinationSelected,
this.groupAlignment = NavigationRailGroupAlignment.top, this.elevation,
this.labelType = NavigationRailLabelType.none, this.groupAlignment,
this.labelTextStyle, this.labelType,
this.unselectedLabelTextStyle,
this.selectedLabelTextStyle, this.selectedLabelTextStyle,
this.iconTheme, this.unselectedIconTheme,
this.selectedIconTheme, this.selectedIconTheme,
}); this.preferredWidth = _railWidth,
this.extendedWidth = _extendedRailWidth,
}) : assert(destinations != null && destinations.isNotEmpty),
assert(0 <= currentIndex && currentIndex < destinations.length),
assert(elevation == null || elevation > 0),
assert(preferredWidth == null || preferredWidth > 0),
assert(extendedWidth == null || extendedWidth > 0),
assert((preferredWidth == null || extendedWidth == null) || extendedWidth >= preferredWidth),
assert(extended != null),
assert(!extended || (labelType == null || labelType == NavigationRailLabelType.none));
/// Sets the color of the Container that holds all of the [NavigationRail]'s
/// contents.
final Color backgroundColor;
/// Indicates of the [NavigationRail] should be in the extended 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]
/// should be set to [NavigationRailLabelType.none].
final bool extended;
/// The leading widget in the rail that is placed above the items. /// The leading widget in the rail that is placed above the destinations.
/// ///
/// This is commonly a [FloatingActionButton], but may also be a non-button, /// This is commonly a [FloatingActionButton], but may also be a non-button,
/// such as a logo. /// such as a logo.
final Widget leading; final Widget leading;
/// The trailing widget in the rail that is placed below the destinations.
///
/// This is commonly a list of additional options or destinations that is
/// usually only rendered when [extended] is true.
final Widget trailing;
/// Defines the appearance of the button items that are arrayed within the /// Defines the appearance of the button items that are arrayed within the
/// navigation rail. /// navigation rail.
final List<NavigationRailDestination> destinations; final List<NavigationRailDestination> destinations;
/// The index into [destinations] for the current active [NavigationRailDestination]. /// The index into [destinations] for the current active
/// [NavigationRailDestination].
final int currentIndex; final int currentIndex;
/// Called when one of the [destinations] is selected. /// Called when one of the [destinations] is selected.
@ -123,6 +172,14 @@ class NavigationRail extends StatefulWidget {
/// `setState` to rebuild the navigation rail with the new [currentIndex]. /// `setState` to rebuild the navigation rail with the new [currentIndex].
final ValueChanged<int> onDestinationSelected; final ValueChanged<int> onDestinationSelected;
/// The elevation for the inner side of the rail.
///
/// The shadow only shows on the inner side of the rail.
///
/// In LTR configurations, the inner side is the right side, and in RTL
/// configurations, it is the left side.
final double elevation;
/// The alignment for the [NavigationRailDestination]s as they are positioned /// The alignment for the [NavigationRailDestination]s as they are positioned
/// within the [NavigationRail]. /// within the [NavigationRail].
/// ///
@ -130,7 +187,10 @@ class NavigationRail extends StatefulWidget {
/// [bottom], or [center] of a layout. /// [bottom], or [center] of a layout.
final NavigationRailGroupAlignment groupAlignment; final NavigationRailGroupAlignment groupAlignment;
/// Defines the layout and behavior of the labels in the [NavigationRail]. /// Defines the layout and behavior of the labels for the default, unextended
/// [NavigationRail].
///
/// When the navigation rail is extended, the labels are always shown.
/// ///
/// See also: /// See also:
/// ///
@ -138,20 +198,17 @@ class NavigationRail extends StatefulWidget {
/// types. /// types.
final NavigationRailLabelType labelType; final NavigationRailLabelType labelType;
/// The [TextStyle] of the [NavigationRailDestination] labels. /// The [TextStyle] of the unselected [NavigationRailDestination] labels.
/// ///
/// This is the default [TextStyle] for all labels. When the /// When the [NavigationRailDestination] is selected, the
/// [NavigationRailDestination] is selected, the [selectedLabelTextStyle] will be /// [selectedLabelTextStyle] will be used instead.
/// used instead. final TextStyle unselectedLabelTextStyle;
final TextStyle labelTextStyle;
/// The [TextStyle] of the [NavigationRailDestination] labels when they are /// The [TextStyle] of the [NavigationRailDestination] labels when they are
/// selected. /// selected.
/// ///
/// This field overrides the [labelTextStyle] for selected items. /// When the [NavigationRailDestination] is not selected,
/// /// [unselectedLabelTextStyle] will be used.
/// When the [NavigationRailDestination] is not selected, [labelTextStyle] will be
/// used.
final TextStyle selectedLabelTextStyle; final TextStyle selectedLabelTextStyle;
/// The default size, opacity, and color of the icon in the /// The default size, opacity, and color of the icon in the
@ -160,25 +217,52 @@ class NavigationRail extends StatefulWidget {
/// If this field is not provided, or provided with any null properties, then /// If this field is not provided, or provided with any null properties, then
/// a copy of the [IconThemeData.fallback] with a custom [NavigationRail] /// a copy of the [IconThemeData.fallback] with a custom [NavigationRail]
/// specific color will be used. /// specific color will be used.
final IconTheme iconTheme; final IconThemeData unselectedIconTheme;
/// The size, opacity, and color of the icon in the selected /// The size, opacity, and color of the icon in the selected
/// [NavigationRailDestination]. /// [NavigationRailDestination].
/// ///
/// This field overrides the [iconTheme] for selected items. /// When the [NavigationRailDestination] is not selected,
/// [unselectedIconTheme] will be used.
final IconThemeData selectedIconTheme;
/// The smallest possible width for the rail regardless of the destination
/// content size.
///
/// The default is 72.
///
/// This value also defines the min width and min height of the destination
/// boxes.
///
/// To make a compact rail, set this to 56 and use
/// [NavigationRailLabelType.none].
final double preferredWidth;
/// The final width when the animation is complete for setting [extended] to
/// true.
///
/// This is only used when [extended] is set to true.
/// ///
/// When the [NavigationRailDestination] is not selected, [iconTheme] will be /// The default value is 256.
/// used. final double extendedWidth;
final IconTheme selectedIconTheme;
/// 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.
static Animation<double> extendedAnimation(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_ExtendedNavigationRailAnimation>().animation;
}
@override @override
_NavigationRailState createState() => _NavigationRailState(); _NavigationRailState createState() => _NavigationRailState();
} }
class _NavigationRailState extends State<NavigationRail> class _NavigationRailState extends State<NavigationRail> with TickerProviderStateMixin {
with TickerProviderStateMixin { List<AnimationController> _destinationControllers = <AnimationController>[];
List<AnimationController> _controllers = <AnimationController>[]; List<Animation<double>> _destinationAnimations;
List<Animation<double>> _animations; AnimationController _extendedController;
Animation<double> _extendedAnimation;
@override @override
void initState() { void initState() {
@ -196,6 +280,14 @@ class _NavigationRailState extends State<NavigationRail>
void didUpdateWidget(NavigationRail oldWidget) { void didUpdateWidget(NavigationRail oldWidget) {
super.didUpdateWidget(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. // No animated segue if the length of the items list changes.
if (widget.destinations.length != oldWidget.destinations.length) { if (widget.destinations.length != oldWidget.destinations.length) {
_resetState(); _resetState();
@ -203,79 +295,133 @@ class _NavigationRailState extends State<NavigationRail>
} }
if (widget.currentIndex != oldWidget.currentIndex) { if (widget.currentIndex != oldWidget.currentIndex) {
_controllers[oldWidget.currentIndex].reverse(); _destinationControllers[oldWidget.currentIndex].reverse();
_controllers[widget.currentIndex].forward(); _destinationControllers[widget.currentIndex].forward();
return;
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget leading = widget.leading; final ThemeData theme = Theme.of(context);
return DefaultTextStyle( final NavigationRailThemeData navigationRailTheme = NavigationRailTheme.of(context);
style: TextStyle(color: Theme.of(context).colorScheme.primary), final MaterialLocalizations localizations = MaterialLocalizations.of(context);
child: Container(
width: _railWidth, final Color backgroundColor = widget.backgroundColor ?? navigationRailTheme.backgroundColor ?? theme.colorScheme.surface;
color: Theme.of(context).colorScheme.surface, final double elevation = widget.elevation ?? navigationRailTheme.elevation ?? 0;
final Color baseSelectedColor = theme.colorScheme.primary;
final Color baseColor = theme.colorScheme.onSurface.withOpacity(0.64);
final IconThemeData unselectedIconTheme = theme.iconTheme.copyWith(color: baseColor).merge(widget.unselectedIconTheme ?? navigationRailTheme.unselectedIconTheme);
final IconThemeData selectedIconTheme = theme.iconTheme.copyWith(color: baseSelectedColor).merge(widget.selectedIconTheme ?? navigationRailTheme.selectedIconTheme);
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 NavigationRailGroupAlignment groupAlignment = widget.groupAlignment ?? navigationRailTheme.groupAlignment ?? NavigationRailGroupAlignment.top;
final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? NavigationRailLabelType.none;
final MainAxisAlignment destinationsAlignment = _resolveGroupAlignment(groupAlignment);
return _ExtendedNavigationRailAnimation(
animation: _extendedAnimation,
child: Semantics(
explicitChildNodes: true,
child: Material(
elevation: elevation,
color: backgroundColor,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
_verticalSpacing, _verticalSpacer,
if (leading != null) ...<Widget>[ if (widget.leading != null)
...<Widget>[
if (_extendedAnimation.value > 0)
SizedBox( SizedBox(
height: _railItemHeight, width: lerpDouble(widget.preferredWidth, widget.extendedWidth, _extendedAnimation.value),
width: _railItemWidth, child: widget.leading,
child: Align( )
alignment: Alignment.center, else
child: leading, widget.leading,
), _verticalSpacer,
),
_verticalSpacing,
], ],
Expanded(
child: Column(
mainAxisAlignment: destinationsAlignment,
children: <Widget>[
for (int i = 0; i < widget.destinations.length; i++) for (int i = 0; i < widget.destinations.length; i++)
_RailItem( _RailDestinationBox(
animation: _animations[i], width: widget.preferredWidth,
labelKind: widget.labelType, extendedWidth: widget.extendedWidth,
extendedTransitionAnimation: _extendedAnimation,
selected: widget.currentIndex == i, selected: widget.currentIndex == i,
icon: widget.currentIndex == i icon: widget.currentIndex == i ? widget.destinations[i].activeIcon : widget.destinations[i].icon,
? widget.destinations[i].activeIcon label: widget.destinations[i].label,
: widget.destinations[i].icon, destinationAnimation: _destinationAnimations[i],
title: DefaultTextStyle( labelType: labelType,
style: TextStyle( iconTheme: widget.currentIndex == i ? selectedIconTheme : unselectedIconTheme,
color: widget.currentIndex == i labelTextStyle: widget.currentIndex == i ? selectedLabelTextStyle : unselectedLabelTextStyle,
? Theme.of(context).colorScheme.primary
: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.64)),
child: widget.destinations[i].title,
),
onTap: () { onTap: () {
widget.onDestinationSelected(i); widget.onDestinationSelected(i);
}, },
indexLabel: localizations.tabLabel(
tabIndex: i + 1,
tabCount: widget.destinations.length,
), ),
),
],
),
),
if (widget.trailing != null)
if (_extendedAnimation.value > 0)
SizedBox(
width: lerpDouble(widget.preferredWidth, widget.extendedWidth, _extendedAnimation.value),
child: widget.trailing,
)
else
widget.trailing,
], ],
), ),
), ),
),
); );
} }
MainAxisAlignment _resolveGroupAlignment(NavigationRailGroupAlignment groupAlignment) {
switch (groupAlignment) {
case NavigationRailGroupAlignment.top:
return MainAxisAlignment.start;
case NavigationRailGroupAlignment.center:
return MainAxisAlignment.center;
case NavigationRailGroupAlignment.bottom:
return MainAxisAlignment.end;
}
return MainAxisAlignment.start;
}
void _disposeControllers() { void _disposeControllers() {
for (final AnimationController controller in _controllers) for (final AnimationController controller in _destinationControllers) {
controller.dispose(); controller.dispose();
} }
_extendedController.dispose();
}
void _initControllers() { void _initControllers() {
_controllers = List<AnimationController>.generate( _destinationControllers = List<AnimationController>.generate(widget.destinations.length, (int index) {
widget.destinations.length, (int index) {
return AnimationController( return AnimationController(
duration: kThemeAnimationDuration, duration: kThemeAnimationDuration,
vsync: this, vsync: this,
)..addListener(_rebuild); )..addListener(_rebuild);
}); });
_animations = _controllers _destinationAnimations = _destinationControllers.map((AnimationController controller) => controller.view).toList();
.map((AnimationController controller) => controller.view) _destinationControllers[widget.currentIndex].value = 1.0;
.toList(); _extendedController = AnimationController(
_controllers[widget.currentIndex].value = 1.0; duration: kThemeAnimationDuration,
vsync: this,
value: widget.extended ? 1.0 : 0.0,
);
_extendedAnimation = CurvedAnimation(
parent: _extendedController,
curve: Curves.easeInOut,
);
_extendedController.addListener(() {
_rebuild();
});
} }
void _resetState() { void _resetState() {
@ -291,80 +437,159 @@ class _NavigationRailState extends State<NavigationRail>
} }
} }
class _RailItem extends StatelessWidget { class _RailDestinationBox extends StatelessWidget {
_RailItem({ _RailDestinationBox({
this.animation, @required this.width,
this.labelKind, this.extendedWidth,
this.selected, @required this.icon,
this.icon, @required this.label,
this.title, @required this.destinationAnimation,
this.onTap, @required this.extendedTransitionAnimation,
}) : assert(labelKind != null), @required this.labelType,
@required this.selected,
@required this.iconTheme,
@required this.labelTextStyle,
@required this.onTap,
this.indexLabel,
}) : assert(width != 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),
_positionAnimation = CurvedAnimation( _positionAnimation = CurvedAnimation(
parent: ReverseAnimation(animation), parent: ReverseAnimation(destinationAnimation),
curve: Curves.easeInOut, curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut.flipped, reverseCurve: Curves.easeInOut.flipped,
); );
final Animation<double> _positionAnimation; final double width;
final double extendedWidth;
final Animation<double> animation;
final NavigationRailLabelType labelKind;
final bool selected;
final Widget icon; final Widget icon;
final Widget title; 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 VoidCallback onTap;
final String indexLabel;
double _fadeInValue() { final Animation<double> _positionAnimation;
if (animation.value < 0.25) {
return 0;
} else if (animation.value < 0.75) {
return (animation.value - 0.25) * 2;
} else {
return 1;
}
}
double _fadeOutValue() {
if (animation.value > 0.75) {
return (animation.value - 0.75) * 4;
} else {
return 0;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget themedIcon = IconTheme(
data: iconTheme,
child: icon,
);
final Widget styledLabel = DefaultTextStyle.merge(
style: labelTextStyle,
child: label,
);
Widget content; Widget content;
switch (labelKind) { switch (labelType) {
case NavigationRailLabelType.none: case NavigationRailLabelType.none:
content = SizedBox(width: _railItemWidth, child: icon); if (extendedTransitionAnimation.value == 0) {
content = Stack(
children: <Widget>[
SizedBox(
width: width,
height: width,
child: themedIcon,
),
// For semantics when label is not showing,
SizedBox(
width: 0,
height: 0,
child: Opacity(
alwaysIncludeSemantics: true,
opacity: 0.0,
child: label,
),
),
]
);
} else {
final TextDirection textDirection = Directionality.of(context);
content = SizedBox(
width: lerpDouble(width, extendedWidth, extendedTransitionAnimation.value),
child: Stack(
children: <Widget>[
Positioned(
child: SizedBox(
width: width,
height: width,
child: themedIcon,
),
),
Positioned.directional(
textDirection: textDirection,
start: width,
height: width,
child: Opacity(
alwaysIncludeSemantics: true,
opacity: _extendedLabelFadeValue(),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: styledLabel,
),
),
),
],
),
);
}
break; break;
case NavigationRailLabelType.selected: case NavigationRailLabelType.selected:
content = SizedBox( final double appearingAnimationValue = 1 - _positionAnimation.value;
width: 72, final double lerpedPadding = lerpDouble(_verticalDestinationPaddingNoLabel, _verticalDestinationPaddingWithLabel, appearingAnimationValue);
content = Container(
constraints: BoxConstraints(
minWidth: width,
minHeight: width,
),
padding: const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
child: ClipRect(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
SizedBox(height: _positionAnimation.value * 18), SizedBox(height: lerpedPadding),
icon, themedIcon,
Opacity( Align(
alignment: Alignment.topCenter,
heightFactor: appearingAnimationValue,
widthFactor: 1.0,
child: Opacity(
alwaysIncludeSemantics: true, alwaysIncludeSemantics: true,
opacity: selected ? _fadeInValue() : _fadeOutValue(), opacity: selected ? _normalLabelFadeInValue() : _normalLabelFadeOutValue(),
child: title, child: styledLabel,
),
), ),
SizedBox(height: lerpedPadding),
], ],
), ),
),
); );
break; break;
case NavigationRailLabelType.all: case NavigationRailLabelType.all:
content = SizedBox( content = Container(
width: 72, constraints: BoxConstraints(
minWidth: width,
minHeight: width,
),
padding: const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
icon, const SizedBox(height: _verticalDestinationPaddingWithLabel),
title, themedIcon,
styledLabel,
const SizedBox(height: _verticalDestinationPaddingWithLabel),
], ],
), ),
); );
@ -372,31 +597,158 @@ class _RailItem extends StatelessWidget {
} }
final ColorScheme colors = Theme.of(context).colorScheme; final ColorScheme colors = Theme.of(context).colorScheme;
return IconTheme( return Semantics(
data: IconThemeData( container: true,
color: selected ? colors.primary : colors.onSurface.withOpacity(0.64), selected: selected,
), child: Stack(
child: SizedBox( children: <Widget>[
height: 72, Material(
child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,
clipBehavior: Clip.none, clipBehavior: Clip.none,
child: InkResponse( child: InkResponse(
onTap: onTap, onTap: onTap,
onHover: (_) {}, onHover: (_) {},
splashColor: highlightShape: BoxShape.rectangle,
Theme.of(context).colorScheme.primary.withOpacity(0.12), borderRadius: BorderRadius.all(Radius.circular(width / 2.0)),
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.04), containedInkWell: true,
splashColor: colors.primary.withOpacity(0.12),
hoverColor: colors.primary.withOpacity(0.04),
child: content, 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 icons of a navigation rail item are shown.
none,
/// Only the selected navigation rail item will show its label.
///
/// The label will animate in and out as new items are selected.
selected,
/// All navigation rail items will show their label.
all,
}
/// Defines the alignment for the group of [NavigationRailDestination]s within
/// a [NavigationRail].
///
/// Navigation rail destinations can be aligned as a group to the [top],
/// [bottom], or [center] of a layout.
enum NavigationRailGroupAlignment {
/// Place the [NavigationRailDestination]s at the top of the rail.
top,
/// Place the [NavigationRailDestination]s in the center of the rail.
center,
/// Place the [NavigationRailDestination]s at the bottom of the rail.
bottom,
}
/// A description for an interactive button within a [NavigationRail].
///
/// See also:
///
/// * [NavigationRail]
class NavigationRailDestination {
/// Creates a destination that is used with [NavigationRail.destinations].
///
/// [icon] should not be null and [label] should not be null when this
/// destination is used in the [NavigationRail].
const NavigationRailDestination({
@required this.icon,
Widget activeIcon,
this.label,
}) : activeIcon = activeIcon ?? 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 [activeIcon] 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]. [icon] should be set to the stroked version and
/// [activeIcon] 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.
///
/// See also:
///
/// * [NavigationRailDestination.icon], for a description of how to pair
/// icons.
final Widget activeIcon;
/// The label for the destination.
///
/// The label should be provided when used with the [NavigationRail]. When
/// the labelType is [NavigationRailLabelType.none] and the rail is not
/// extended, then it can be null, but should be used for semantics.
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 _railWidth = 72; const double _railWidth = 72.0;
const double _railItemWidth = _railWidth; const double _extendedRailWidth = 256.0;
const double _railItemHeight = _railItemWidth; const double _horizontalDestinationPadding = 8.0;
const double _spacing = 8; const double _verticalDestinationPaddingNoLabel = 24.0;
const Widget _verticalSpacing = SizedBox(height: _spacing); const double _verticalDestinationPaddingWithLabel = 16.0;
const Widget _verticalSpacer = SizedBox(height: 8.0);

@ -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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'navigation_rail.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.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
class NavigationRailThemeData extends 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 unselected, enabled [NavigationRail]'s
/// background.
final Color backgroundColor;
/// The z-coordinate to be used for the unselected, enabled
/// [NavigationRail]'s elevation foreground.
final double elevation;
/// The style on which to base the destination label, when the destination
/// is not selected.
final TextStyle unselectedLabelTextStyle;
/// The style on which to base the destination label, when the destination
/// is selected.
final TextStyle selectedLabelTextStyle;
/// The theme on which to base the destination icon, when the destination
/// is not selected.
final IconThemeData unselectedIconTheme;
/// The theme on which to base the destination icon, when the destination
/// is selected.
final IconThemeData selectedIconTheme;
/// The alignment for the [NavigationRailDestination]s as they are positioned
/// within the [NavigationRail].
final NavigationRailGroupAlignment 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,
NavigationRailGroupAlignment 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: t < 0.5 ? a.groupAlignment : b.groupAlignment,
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(DiagnosticsProperty<NavigationRailGroupAlignment>('groupAlignment', groupAlignment, defaultValue: defaultData.groupAlignment));
properties.add(DiagnosticsProperty<NavigationRailLabelType>('labelType', labelType, defaultValue: defaultData.labelType));
}
}
/// An inherited widget that defines background color, elevation, label text
/// style, icon theme, group alignment, and label type parameters for
/// [NavigationRail]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 ?? NavigationRailThemeData(); // ?? 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;
}

@ -29,7 +29,6 @@ declare -ar PROJECT_NAMES=(
"add_to_app/flutter_module" \ "add_to_app/flutter_module" \
"add_to_app/flutter_module_using_plugin" \ "add_to_app/flutter_module_using_plugin" \
"animations" \ "animations" \
"experimental/web_dashboard" \
"flutter_maps_firestore" \ "flutter_maps_firestore" \
"gallery" \ "gallery" \
"isolate_example" \ "isolate_example" \

Loading…
Cancel
Save