From e24fef3af5f69926a5673dd1d08647030c6e8db8 Mon Sep 17 00:00:00 2001 From: John Ryan Date: Mon, 9 Mar 2020 16:20:01 -0700 Subject: [PATCH] [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 --- .../experimental/adaptive_scaffold_demo.dart | 35 + .../third_party/adaptive_scaffold.dart | 2 +- .../widgets/third_party/navigation_rail.dart | 816 +++++++++++++----- .../third_party/navigation_rail_theme.dart | 215 +++++ tool/travis_flutter_script.sh | 1 - 5 files changed, 835 insertions(+), 234 deletions(-) create mode 100644 experimental/web_dashboard/lib/src/experimental/adaptive_scaffold_demo.dart create mode 100644 experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail_theme.dart diff --git a/experimental/web_dashboard/lib/src/experimental/adaptive_scaffold_demo.dart b/experimental/web_dashboard/lib/src/experimental/adaptive_scaffold_demo.dart new file mode 100644 index 000000000..c44386c23 --- /dev/null +++ b/experimental/web_dashboard/lib/src/experimental/adaptive_scaffold_demo.dart @@ -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 { + @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!'), + ), + ), + ); + } +} diff --git a/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart b/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart index a452f1f04..3e2e3e5be 100644 --- a/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart +++ b/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart @@ -105,7 +105,7 @@ class _AdaptiveScaffoldState extends State { ...widget.destinations.map( (d) => NavigationRailDestination( icon: Icon(d.icon), - title: Text(d.title), + label: Text(d.title), ), ), ], diff --git a/experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail.dart b/experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail.dart index 219cb63f2..2e82eb192 100644 --- a/experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail.dart +++ b/experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail.dart @@ -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: [ +/// 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 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 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 extendedAnimation(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_ExtendedNavigationRailAnimation>().animation; + } @override _NavigationRailState createState() => _NavigationRailState(); } -class _NavigationRailState extends State - with TickerProviderStateMixin { - List _controllers = []; - List> _animations; +class _NavigationRailState extends State with TickerProviderStateMixin { + List _destinationControllers = []; + List> _destinationAnimations; + AnimationController _extendedController; + Animation _extendedAnimation; @override void initState() { @@ -196,6 +280,14 @@ class _NavigationRailState extends State 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 } 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: [ - _verticalSpacing, - if (leading != null) ...[ - 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: [ + _verticalSpacer, + if (widget.leading != null) + ...[ + 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: [ + 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.generate( - widget.destinations.length, (int index) { + _destinationControllers = List.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 } } -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 _positionAnimation; - - final Animation animation; - final NavigationRailLabelType labelKind; - final bool selected; + final double width; + final double extendedWidth; final Widget icon; - final Widget title; + final Widget label; + final Animation destinationAnimation; + final NavigationRailLabelType labelType; + final bool selected; + final Animation 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 _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: [ + 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: [ + 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: [ - 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: [ + 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: [ - 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: [ + 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 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); \ No newline at end of file diff --git a/experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail_theme.dart b/experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail_theme.dart new file mode 100644 index 000000000..a7ec83d91 --- /dev/null +++ b/experimental/web_dashboard/lib/src/widgets/third_party/navigation_rail_theme.dart @@ -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('unselectedLabelTextStyle', unselectedLabelTextStyle, defaultValue: defaultData.unselectedLabelTextStyle)); + properties.add(DiagnosticsProperty('selectedLabelTextStyle', selectedLabelTextStyle, defaultValue: defaultData.selectedLabelTextStyle)); + properties.add(DiagnosticsProperty('unselectedIconTheme', unselectedIconTheme, defaultValue: defaultData.unselectedIconTheme)); + properties.add(DiagnosticsProperty('selectedIconTheme', selectedIconTheme, defaultValue: defaultData.selectedIconTheme)); + properties.add(DiagnosticsProperty('groupAlignment', groupAlignment, defaultValue: defaultData.groupAlignment)); + properties.add(DiagnosticsProperty('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(); + return navigationRailTheme?.data ?? NavigationRailThemeData(); // ?? Theme.of(context).navigationRailTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) { + final NavigationRailTheme ancestorTheme = context.findAncestorWidgetOfExactType(); + return identical(this, ancestorTheme) ? child : NavigationRailTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(NavigationRailTheme oldWidget) => data != oldWidget.data; +} \ No newline at end of file diff --git a/tool/travis_flutter_script.sh b/tool/travis_flutter_script.sh index 4b77b297f..87b3fd717 100755 --- a/tool/travis_flutter_script.sh +++ b/tool/travis_flutter_script.sh @@ -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" \