[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(
(d) => NavigationRailDestination(
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
// 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.
// 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';
/// Original pull request: https://github.com/flutter/flutter/pull/49574
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]
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].
/// 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.
///
/// 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].
/// {@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
/// `_currentIndex` updates according to the `onDestinationSelected` callback.
///
/// ```dart
/// int _currentIndex = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// 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}
///
/// See also:
///
/// * [NavigationRail]
class NavigationRailDestination {
/// Creates an destination that is used with [NavigationRail.destinations].
///
/// [icon] should not be null and [title] should not be null when this
/// 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.
/// * [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 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.
///
/// 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.
/// The argument [destinations] must not be null. Additionally, it must be
/// non-empty.
///
/// If [activeIcon] is provided, this will only be displayed when the
/// destination is not selected.
/// If [elevation] is specified, it must be non-negative.
///
/// 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 [preferredWidth] is specified, it must be non-negative, and if
/// [extendedWidth is specified, it must be non-negative and greater than
/// [preferredWidth].
///
/// If this icon is not provided, the [NavigationRail] will display [icon] in
/// either state.
/// The argument [extended] must not be null. [extended] can only be set to
/// true when when the [labelType] is null or [NavigationRailLabelType.none].
///
/// See also:
/// 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.
///
/// * [NavigationRailDestination.icon], for a description of how to pair
/// icons.
final Widget activeIcon;
/// The title of the item. If the title is not provided only the icon will be
/// shown when not used in a [NavigationRail].
final Widget title;
}
/// TODO
class NavigationRail extends StatefulWidget {
/// TODO
/// Typically used within a [Row] of the [Scaffold.body] property.
NavigationRail({
this.backgroundColor,
this.extended = false,
this.leading,
this.destinations,
this.currentIndex,
this.trailing,
@required this.destinations,
this.currentIndex = 0,
this.onDestinationSelected,
this.groupAlignment = NavigationRailGroupAlignment.top,
this.labelType = NavigationRailLabelType.none,
this.labelTextStyle,
this.elevation,
this.groupAlignment,
this.labelType,
this.unselectedLabelTextStyle,
this.selectedLabelTextStyle,
this.iconTheme,
this.unselectedIconTheme,
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,
/// such as a logo.
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
/// navigation rail.
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;
/// 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].
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
/// within the [NavigationRail].
///
@ -130,7 +187,10 @@ class NavigationRail extends StatefulWidget {
/// [bottom], or [center] of a layout.
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:
///
@ -138,47 +198,71 @@ class NavigationRail extends StatefulWidget {
/// types.
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
/// [NavigationRailDestination] is selected, the [selectedLabelTextStyle] will be
/// used instead.
final TextStyle labelTextStyle;
/// When the [NavigationRailDestination] is selected, the
/// [selectedLabelTextStyle] will be used instead.
final TextStyle unselectedLabelTextStyle;
/// The [TextStyle] of the [NavigationRailDestination] labels when they are
/// selected.
///
/// This field overrides the [labelTextStyle] for selected items.
///
/// When the [NavigationRailDestination] is not selected, [labelTextStyle] will be
/// used.
/// When the [NavigationRailDestination] is not selected,
/// [unselectedLabelTextStyle] will be used.
final TextStyle selectedLabelTextStyle;
/// The default size, opacity, and color of the icon in the
/// [NavigationRailDestination].
///
/// 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.
final IconTheme iconTheme;
final IconThemeData unselectedIconTheme;
/// The size, opacity, and color of the icon in the selected
/// [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.
///
/// When the [NavigationRailDestination] is not selected, [iconTheme] will be
/// used.
final IconTheme selectedIconTheme;
/// 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.
///
/// The default value is 256.
final double extendedWidth;
/// 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
_NavigationRailState createState() => _NavigationRailState();
}
class _NavigationRailState extends State<NavigationRail>
with TickerProviderStateMixin {
List<AnimationController> _controllers = <AnimationController>[];
List<Animation<double>> _animations;
class _NavigationRailState extends State<NavigationRail> with TickerProviderStateMixin {
List<AnimationController> _destinationControllers = <AnimationController>[];
List<Animation<double>> _destinationAnimations;
AnimationController _extendedController;
Animation<double> _extendedAnimation;
@override
void initState() {
@ -196,6 +280,14 @@ class _NavigationRailState extends State<NavigationRail>
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();
@ -203,79 +295,133 @@ class _NavigationRailState extends State<NavigationRail>
}
if (widget.currentIndex != oldWidget.currentIndex) {
_controllers[oldWidget.currentIndex].reverse();
_controllers[widget.currentIndex].forward();
_destinationControllers[oldWidget.currentIndex].reverse();
_destinationControllers[widget.currentIndex].forward();
return;
}
}
@override
Widget build(BuildContext context) {
final Widget leading = widget.leading;
return DefaultTextStyle(
style: TextStyle(color: Theme.of(context).colorScheme.primary),
child: Container(
width: _railWidth,
color: Theme.of(context).colorScheme.surface,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_verticalSpacing,
if (leading != null) ...<Widget>[
SizedBox(
height: _railItemHeight,
width: _railItemWidth,
child: Align(
alignment: Alignment.center,
child: leading,
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 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(
children: <Widget>[
_verticalSpacer,
if (widget.leading != null)
...<Widget>[
if (_extendedAnimation.value > 0)
SizedBox(
width: lerpDouble(widget.preferredWidth, widget.extendedWidth, _extendedAnimation.value),
child: widget.leading,
)
else
widget.leading,
_verticalSpacer,
],
Expanded(
child: Column(
mainAxisAlignment: destinationsAlignment,
children: <Widget>[
for (int i = 0; i < widget.destinations.length; i++)
_RailDestinationBox(
width: widget.preferredWidth,
extendedWidth: widget.extendedWidth,
extendedTransitionAnimation: _extendedAnimation,
selected: widget.currentIndex == i,
icon: widget.currentIndex == i ? widget.destinations[i].activeIcon : widget.destinations[i].icon,
label: widget.destinations[i].label,
destinationAnimation: _destinationAnimations[i],
labelType: labelType,
iconTheme: widget.currentIndex == i ? selectedIconTheme : unselectedIconTheme,
labelTextStyle: widget.currentIndex == i ? selectedLabelTextStyle : unselectedLabelTextStyle,
onTap: () {
widget.onDestinationSelected(i);
},
indexLabel: localizations.tabLabel(
tabIndex: i + 1,
tabCount: widget.destinations.length,
),
),
],
),
),
_verticalSpacing,
if (widget.trailing != null)
if (_extendedAnimation.value > 0)
SizedBox(
width: lerpDouble(widget.preferredWidth, widget.extendedWidth, _extendedAnimation.value),
child: widget.trailing,
)
else
widget.trailing,
],
for (int i = 0; i < widget.destinations.length; i++)
_RailItem(
animation: _animations[i],
labelKind: widget.labelType,
selected: widget.currentIndex == i,
icon: widget.currentIndex == i
? widget.destinations[i].activeIcon
: widget.destinations[i].icon,
title: DefaultTextStyle(
style: TextStyle(
color: widget.currentIndex == i
? Theme.of(context).colorScheme.primary
: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.64)),
child: widget.destinations[i].title,
),
onTap: () {
widget.onDestinationSelected(i);
},
),
],
),
),
),
);
}
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() {
for (final AnimationController controller in _controllers)
for (final AnimationController controller in _destinationControllers) {
controller.dispose();
}
_extendedController.dispose();
}
void _initControllers() {
_controllers = List<AnimationController>.generate(
widget.destinations.length, (int index) {
_destinationControllers = List<AnimationController>.generate(widget.destinations.length, (int index) {
return AnimationController(
duration: kThemeAnimationDuration,
vsync: this,
)..addListener(_rebuild);
});
_animations = _controllers
.map((AnimationController controller) => controller.view)
.toList();
_controllers[widget.currentIndex].value = 1.0;
_destinationAnimations = _destinationControllers.map((AnimationController controller) => controller.view).toList();
_destinationControllers[widget.currentIndex].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() {
@ -291,80 +437,159 @@ class _NavigationRailState extends State<NavigationRail>
}
}
class _RailItem extends StatelessWidget {
_RailItem({
this.animation,
this.labelKind,
this.selected,
this.icon,
this.title,
this.onTap,
}) : assert(labelKind != null),
class _RailDestinationBox extends StatelessWidget {
_RailDestinationBox({
@required this.width,
this.extendedWidth,
@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,
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(
parent: ReverseAnimation(animation),
parent: ReverseAnimation(destinationAnimation),
curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut.flipped,
);
final Animation<double> _positionAnimation;
final Animation<double> animation;
final NavigationRailLabelType labelKind;
final bool selected;
final double width;
final double extendedWidth;
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 String indexLabel;
double _fadeInValue() {
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;
}
}
final Animation<double> _positionAnimation;
@override
Widget build(BuildContext context) {
final Widget themedIcon = IconTheme(
data: iconTheme,
child: icon,
);
final Widget styledLabel = DefaultTextStyle.merge(
style: labelTextStyle,
child: label,
);
Widget content;
switch (labelKind) {
switch (labelType) {
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;
case NavigationRailLabelType.selected:
content = SizedBox(
width: 72,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(height: _positionAnimation.value * 18),
icon,
Opacity(
alwaysIncludeSemantics: true,
opacity: selected ? _fadeInValue() : _fadeOutValue(),
child: title,
),
],
final double appearingAnimationValue = 1 - _positionAnimation.value;
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(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(height: lerpedPadding),
themedIcon,
Align(
alignment: Alignment.topCenter,
heightFactor: appearingAnimationValue,
widthFactor: 1.0,
child: Opacity(
alwaysIncludeSemantics: true,
opacity: selected ? _normalLabelFadeInValue() : _normalLabelFadeOutValue(),
child: styledLabel,
),
),
SizedBox(height: lerpedPadding),
],
),
),
);
break;
case NavigationRailLabelType.all:
content = SizedBox(
width: 72,
content = Container(
constraints: BoxConstraints(
minWidth: width,
minHeight: width,
),
padding: const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
icon,
title,
const SizedBox(height: _verticalDestinationPaddingWithLabel),
themedIcon,
styledLabel,
const SizedBox(height: _verticalDestinationPaddingWithLabel),
],
),
);
@ -372,31 +597,158 @@ class _RailItem extends StatelessWidget {
}
final ColorScheme colors = Theme.of(context).colorScheme;
return IconTheme(
data: IconThemeData(
color: selected ? colors.primary : colors.onSurface.withOpacity(0.64),
),
child: SizedBox(
height: 72,
child: Material(
type: MaterialType.transparency,
clipBehavior: Clip.none,
child: InkResponse(
onTap: onTap,
onHover: (_) {},
splashColor:
Theme.of(context).colorScheme.primary.withOpacity(0.12),
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.04),
child: content,
),
),
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(width / 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 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 _railItemWidth = _railWidth;
const double _railItemHeight = _railItemWidth;
const double _spacing = 8;
const Widget _verticalSpacing = SizedBox(height: _spacing);
const double _railWidth = 72.0;
const double _extendedRailWidth = 256.0;
const double _horizontalDestinationPadding = 8.0;
const double _verticalDestinationPaddingNoLabel = 24.0;
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_using_plugin" \
"animations" \
"experimental/web_dashboard" \
"flutter_maps_firestore" \
"gallery" \
"isolate_example" \

Loading…
Cancel
Save