Unverified Commit 0d111bc9 authored by Anthony's avatar Anthony Committed by GitHub

[Material] Create a Navigation Rail component and theme (#49574)

parent c9f99927
......@@ -81,6 +81,8 @@ export 'src/material/material_button.dart';
export 'src/material/material_localizations.dart';
export 'src/material/material_state.dart';
export 'src/material/mergeable_material.dart';
export 'src/material/navigation_rail.dart';
export 'src/material/navigation_rail_theme.dart';
export 'src/material/outline_button.dart';
export 'src/material/page.dart';
export 'src/material/page_transitions_theme.dart';
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/widgets.dart';
import '../../scheduler.dart';
import 'color_scheme.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'navigation_rail_theme.dart';
import 'theme.dart';
import 'theme_data.dart';
/// A material widget that is meant to be displayed at the left or right of an
/// app to navigate between a small number of views, typically between three and
/// five.
///
/// A navigation rail is usually used as the first or last element of a [Row]
/// which defines the app's [Scaffold] body.
///
/// The appearance of all of the [NavigationRail]s within an app can be
/// specified with [NavigationRailTheme]. The default values for null theme
/// properties are based on the [Theme]'s [ThemeData.textTheme],
/// [ThemeData.iconTheme], and [ThemeData.colorScheme].
//
/// The navigation rail is meant for layouts with wide viewports, such as a
/// desktop web or tablet landscape layout. For smaller layouts, like mobile
/// portrait, a [BottomNavigationBar] should be used instead.
///
/// Adaptive layouts can build different instances of the [Scaffold] in order to
/// have a navigation rail for more horizontal layouts and a bottom navigation
/// bar for more vertical layouts. See
/// [https://github.com/flutter/samples/blob/master/experimental/web_dashboard/lib/src/widgets/third_party/adaptive_scaffold.dart]
/// for an example.
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// This example shows a [NavigationRail] used within a Scaffold with 3
/// [NavigationRailDestination]s. The main content is separated by a divider
/// (although elevation on the navigation rail can be used instead). The
/// `_selectedIndex` is updated by the `onDestinationSelected` callback.
///
/// ```dart
/// int _selectedIndex = 0;
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: Row(
/// children: <Widget>[
/// NavigationRail(
/// selectedIndex: _selectedIndex,
/// onDestinationSelected: (int index) {
/// setState(() {
/// _selectedIndex = index;
/// });
/// },
/// labelType: NavigationRailLabelType.selected,
/// destinations: [
/// NavigationRailDestination(
/// icon: Icon(Icons.favorite_border),
/// selectedIcon: Icon(Icons.favorite),
/// label: Text('First'),
/// ),
/// NavigationRailDestination(
/// icon: Icon(Icons.bookmark_border),
/// selectedIcon: Icon(Icons.book),
/// label: Text('Second'),
/// ),
/// NavigationRailDestination(
/// icon: Icon(Icons.star_border),
/// selectedIcon: Icon(Icons.star),
/// label: Text('Third'),
/// ),
/// ],
/// ),
/// VerticalDivider(thickness: 1, width: 1),
/// // This is the main content.
/// Expanded(
/// child: Center(
/// child: Text('selectedIndex: $_selectedIndex'),
/// ),
/// )
/// ],
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [Scaffold], which can display the navigation rail within a [Row] of the
/// [Scaffold.body] slot.
/// * [NavigationRailDestination], which is used as a model to create tappable
/// destinations in the navigation rail.
/// * [BottomNavigationBar], which is a similar navigation widget that's laid
/// out horizontally.
/// * [https://material.io/components/navigation-rail/]
class NavigationRail extends StatefulWidget {
/// Creates a material design navigation rail.
///
/// The value of [destinations] must be a list of one or more
/// [NavigationRailDestination] values.
///
/// If [elevation] is specified, it must be non-negative.
///
/// If [minWidth] is specified, it must be non-negative, and if
/// [minExtendedWidth] is specified, it must be non-negative and greater than
/// [minWidth].
///
/// The argument [extended] must not be null. [extended] can only be set to
/// true when when the [labelType] is null or [NavigationRailLabelType.none].
///
/// If [backgroundColor], [elevation], [groupAlignment], [labelType],
/// [unselectedLabelTextStyle], [unselectedLabelTextStyle],
/// [unselectedIconTheme], or [selectedIconTheme] are null, then their
/// [NavigationRailThemeData] values will be used. If the corresponding
/// [NavigationRailThemeData] property is null, then the navigation rail
/// defaults are used. See the individual properties for more information.
///
/// Typically used within a [Row] that defines the [Scaffold.body] property.
const NavigationRail({
this.backgroundColor,
this.extended = false,
this.leading,
this.trailing,
@required this.destinations,
@required this.selectedIndex,
this.onDestinationSelected,
this.elevation,
this.groupAlignment,
this.labelType,
this.unselectedLabelTextStyle,
this.selectedLabelTextStyle,
this.unselectedIconTheme,
this.selectedIconTheme,
this.minWidth,
this.minExtendedWidth,
}) : assert(destinations != null && destinations.length >= 2),
assert(selectedIndex != null),
assert(0 <= selectedIndex && selectedIndex < destinations.length),
assert(elevation == null || elevation > 0),
assert(minWidth == null || minWidth > 0),
assert(minExtendedWidth == null || minExtendedWidth > 0),
assert((minWidth == null || minExtendedWidth == null) || minExtendedWidth >= minWidth),
assert(extended != null),
assert(!extended || (labelType == null || labelType == NavigationRailLabelType.none));
/// Sets the color of the Container that holds all of the [NavigationRail]'s
/// contents.
///
/// The default value is [NavigationRailThemeData.backgroundColor]. If
/// [NavigationRailThemeData.backgroundColor] is null, then the default value
/// is based on [ThemeData.colorScheme.surface].
final Color backgroundColor;
/// Indicates that the [NavigationRail] should be in the extended state.
///
/// The extended state has a wider rail container, and the labels are
/// positioned next to the icons. [minExtendedWidth] can be used to set the
/// the minimum width of the rail when it is in this state.
///
/// The rail will implicitly animate between the extended and normal state.
///
/// If the rail is going to be in the extended state, then the [labelType]
/// must be set to [NavigationRailLabelType.none].
///
/// The default value is false.
final bool extended;
/// The leading widget in the rail that is placed above the destinations.
///
/// It is placed at the top of the rail, above the [destinations]. Its
/// location is not affected by [groupAlignment].
///
/// This is commonly a [FloatingActionButton], but may also be a non-button,
/// such as a logo.
///
/// The default value is null.
final Widget leading;
/// The trailing widget in the rail that is placed below the destinations.
///
/// The trailing widget is placed below the last [NavigationRailDestination].
/// It's location is affected by [groupAlignment].
///
/// This is commonly a list of additional options or destinations that is
/// usually only rendered when [extended] is true.
///
/// The default value is null.
final Widget trailing;
/// Defines the appearance of the button items that are arrayed within the
/// navigation rail.
///
/// The value must be a list of two or more [NavigationRailDestination]
/// values.
final List<NavigationRailDestination> destinations;
/// The index into [destinations] for the current selected
/// [NavigationRailDestination].
final int selectedIndex;
/// Called when one of the [destinations] is selected.
///
/// The stateful widget that creates the navigation rail needs to keep
/// track of the index of the selected [NavigationRailDestination] and call
/// `setState` to rebuild the navigation rail with the new [selectedIndex].
final ValueChanged<int> onDestinationSelected;
/// The rail's elevation or z-coordinate.
///
/// If [Directionality] is [TextDirection.LTR], the inner side is the right
/// side, and if [Directionality] is [TextDirection.RTL], it is the left side.
///
/// The default value is 0.
final double elevation;
/// The vertical alignment for the group of [destinations] within the rail.
///
/// The [NavigationRailDestination]s are grouped together with the [trailing]
/// widget, between the [leading] widget and the bottom of the rail.
///
/// The value must be between -1.0 and 1.0.
///
/// If [groupAlignment] is -1.0, then the items are aligned to the top. If
/// [groupAlignment] is 0.0, then the items are aligned to the center. If
/// [groupAlignment] is 1.0, then the items are aligned to the bottom.
///
/// The default is -1.0.
///
/// See also:
/// * [Alignment.y]
///
final double groupAlignment;
/// Defines the layout and behavior of the labels for the default, unextended
/// [NavigationRail].
///
/// When a navigation rail is [extended], the labels are always shown.
///
/// The default value is [NavigationRailThemeData.labelType]. If
/// [NavigationRailThemeData.labelType] is null, then the default value is
/// [NavigationRailLabelType.none].
///
/// See also:
///
/// * [NavigationRailLabelType] for information on the meaning of different
/// types.
final NavigationRailLabelType labelType;
/// The [TextStyle] of a destination's label when it is unselected.
///
/// When one of the [destinations] is selected the [selectedLabelTextStyle]
/// will be used instead.
///
/// The default value is based on the [Theme]'s
/// [ThemeData.textTheme.bodyText]. The default color is based on the
/// [Theme]'s [ColorScheme.onSurface].
///
/// Properties from this text style, or
/// [NavigationRailThemeData.unselectedLabelTextStyle] if this is null, are
/// merged into the defaults.
final TextStyle unselectedLabelTextStyle;
/// The [TextStyle] of a destination's label when it is selected.
///
/// When a [NavigationRailDestination] is not selected,
/// [unselectedLabelTextStyle] will be used.
///
/// The default value is based on the [Theme]'s
/// [ThemeData.textTheme.bodyText]. The default color is based on the
/// [Theme]'s [ColorScheme.primary].
///
/// Properties from this text style,
/// or [NavigationRailThemeData.selectedLabelTextStyle] if this is null, are
/// merged into the defaults.
final TextStyle selectedLabelTextStyle;
/// The visual properties of the icon in the unselected destination.
///
/// If this field is not provided, or provided with any null properties, then
/// a copy of the [IconThemeData.fallback] with a custom [NavigationRail]
/// specific color will be used.
///
/// The default value is Is the [Theme]'s [ThemeData.iconTheme] with a color
/// of the [Theme]'s [ColorScheme.onSurface] with an opacity of 0.64.
/// Properties from this icon theme, or
/// [NavigationRailThemeData.unselectedIconTheme] if this is null, are
/// merged into the defaults.
final IconThemeData unselectedIconTheme;
/// The visual properties of the icon in the selected destination.
///
/// When a [NavigationRailDestination] is not selected,
/// [unselectedIconTheme] will be used.
///
/// The default value is Is the [Theme]'s [ThemeData.iconTheme] with a color
/// of the [Theme]'s [ColorScheme.primary]. Properties from this icon theme,
/// or [NavigationRailThemeData.selectedIconTheme] if this is null, are
/// merged into the defaults.
final IconThemeData selectedIconTheme;
/// The smallest possible width for the rail regardless of the destination's
/// icon or label size.
///
/// The default is 72.
///
/// This value also defines the min width and min height of the destinations.
///
/// To make a compact rail, set this to 56 and use
/// [NavigationRailLabelType.none].
final double minWidth;
/// The final width when the animation is complete for setting [extended] to
/// true.
///
/// This is only used when [extended] is set to true.
///
/// The default value is 256.
final double minExtendedWidth;
/// Returns the animation that controls the [NavigationRail.extended] state.
///
/// This can be used to synchronize animations in the [leading] or [trailing]
/// widget, such as an animated menu or a [FloatingActionButton] animation.
///
/// {@tool snippet}
///
/// This example shows how to use this animation to create a
/// [FloatingActionButton] that animates itself between the normal and
/// extended states of the [NavigationRail].
///
/// An instance of `ExtendableFab` would be created for
/// [NavigationRail.leading].
///
/// ```dart
/// import 'dart:ui';
///
/// @override
/// Widget build(BuildContext context) {
/// final Animation<double> animation = NavigationRail.extendedAnimation(context);
/// return AnimatedBuilder(
/// animation: animation,
/// builder: (BuildContext context, Widget child) {
/// // The extended fab has a shorter height than the regular fab.
/// return Container(
/// height: 56,
/// padding: EdgeInsets.symmetric(
/// vertical: lerpDouble(0, 6, animation.value),
/// ),
/// child: animation.value == 0
/// ? FloatingActionButton(
/// child: Icon(Icons.add),
/// onPressed: () {},
/// )
/// : Align(
/// alignment: AlignmentDirectional.centerStart,
/// widthFactor: animation.value,
/// child: Padding(
/// padding: const EdgeInsetsDirectional.only(start: 8),
/// child: FloatingActionButton.extended(
/// icon: Icon(Icons.add),
/// label: Text('CREATE'),
/// onPressed: () {},
/// ),
/// ),
/// ),
/// );
/// },
/// );
/// }
/// ```
///
/// {@end-tool}
static Animation<double> extendedAnimation(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_ExtendedNavigationRailAnimation>().animation;
}
@override
_NavigationRailState createState() => _NavigationRailState();
}
class _NavigationRailState extends State<NavigationRail> with TickerProviderStateMixin {
List<AnimationController> _destinationControllers = <AnimationController>[];
List<Animation<double>> _destinationAnimations;
AnimationController _extendedController;
Animation<double> _extendedAnimation;
@override
void initState() {
super.initState();
_initControllers();
}
@override
void dispose() {
_disposeControllers();
super.dispose();
}
@override
void didUpdateWidget(NavigationRail oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.extended != oldWidget.extended) {
if (widget.extended) {
_extendedController.forward();
} else {
_extendedController.reverse();
}
}
// No animated segue if the length of the items list changes.
if (widget.destinations.length != oldWidget.destinations.length) {
_resetState();
return;
}
if (widget.selectedIndex != oldWidget.selectedIndex) {
_destinationControllers[oldWidget.selectedIndex].reverse();
_destinationControllers[widget.selectedIndex].forward();
return;
}
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final NavigationRailThemeData navigationRailTheme = NavigationRailTheme.of(context);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final Color backgroundColor = widget.backgroundColor ?? navigationRailTheme.backgroundColor ?? theme.colorScheme.surface;
final double elevation = widget.elevation ?? navigationRailTheme.elevation ?? 0;
final double minWidth = widget.minWidth ?? _minRailWidth;
final double minExtendedWidth = widget.minExtendedWidth ?? _minExtendedRailWidth;
final Color baseSelectedColor = theme.colorScheme.primary;
final Color baseColor = theme.colorScheme.onSurface.withOpacity(0.64);
final IconThemeData defaultUnselectedIconTheme = widget.unselectedIconTheme ?? navigationRailTheme.unselectedIconTheme;
final IconThemeData unselectedIconTheme = IconThemeData(
size: defaultUnselectedIconTheme?.size ?? 24.0,
color: defaultUnselectedIconTheme?.color ?? theme.colorScheme.onSurface,
opacity: defaultUnselectedIconTheme?.opacity ?? 1.0,
);
final IconThemeData defaultSelectedIconTheme = widget.selectedIconTheme ?? navigationRailTheme.selectedIconTheme;
final IconThemeData selectedIconTheme = IconThemeData(
size: defaultSelectedIconTheme?.size ?? 24.0,
color: defaultSelectedIconTheme?.color ?? theme.colorScheme.primary,
opacity: defaultSelectedIconTheme?.opacity ?? 0.64,
);
final TextStyle unselectedLabelTextStyle = theme.textTheme.bodyText1.copyWith(color: baseColor).merge(widget.unselectedLabelTextStyle ?? navigationRailTheme.unselectedLabelTextStyle);
final TextStyle selectedLabelTextStyle = theme.textTheme.bodyText1.copyWith(color: baseSelectedColor).merge(widget.selectedLabelTextStyle ?? navigationRailTheme.selectedLabelTextStyle);
final double groupAlignment = widget.groupAlignment ?? navigationRailTheme.groupAlignment ?? -1.0;
final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? NavigationRailLabelType.none;
return _ExtendedNavigationRailAnimation(
animation: _extendedAnimation,
child: Semantics(
explicitChildNodes: true,
child: Material(
elevation: elevation,
color: backgroundColor,
child: Column(
children: <Widget>[
_verticalSpacer,
if (widget.leading != null)
...<Widget>[
ConstrainedBox(
constraints: BoxConstraints(
minWidth: lerpDouble(minWidth, minExtendedWidth, _extendedAnimation.value),
),
child: widget.leading,
),
_verticalSpacer,
],
Expanded(
child: Align(
alignment: Alignment(0, groupAlignment),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
for (int i = 0; i < widget.destinations.length; i += 1)
_RailDestination(
minWidth: minWidth,
minExtendedWidth: minExtendedWidth,
extendedTransitionAnimation: _extendedAnimation,
selected: widget.selectedIndex == i,
icon: widget.selectedIndex == i ? widget.destinations[i].selectedIcon : widget.destinations[i].icon,
label: widget.destinations[i].label,
destinationAnimation: _destinationAnimations[i],
labelType: labelType,
iconTheme: widget.selectedIndex == i ? selectedIconTheme : unselectedIconTheme,
labelTextStyle: widget.selectedIndex == i ? selectedLabelTextStyle : unselectedLabelTextStyle,
onTap: () {
widget.onDestinationSelected(i);
},
indexLabel: localizations.tabLabel(
tabIndex: i + 1,
tabCount: widget.destinations.length,
),
),
if (widget.trailing != null)
ConstrainedBox(
constraints: BoxConstraints(
minWidth: lerpDouble(minWidth, minExtendedWidth, _extendedAnimation.value),
),
child: widget.trailing,
),
],
),
),
),
],
),
),
),
);
}
void _disposeControllers() {
for (final AnimationController controller in _destinationControllers) {
controller.dispose();
}
_extendedController.dispose();
}
void _initControllers() {
_destinationControllers = List<AnimationController>.generate(widget.destinations.length, (int index) {
return AnimationController(
duration: kThemeAnimationDuration,
vsync: this,
)..addListener(_rebuild);
});
_destinationAnimations = _destinationControllers.map((AnimationController controller) => controller.view).toList();
_destinationControllers[widget.selectedIndex].value = 1.0;
_extendedController = AnimationController(
duration: kThemeAnimationDuration,
vsync: this,
value: widget.extended ? 1.0 : 0.0,
);
_extendedAnimation = CurvedAnimation(
parent: _extendedController,
curve: Curves.easeInOut,
);
_extendedController.addListener(() {
_rebuild();
});
}
void _resetState() {
_disposeControllers();
_initControllers();
}
void _rebuild() {
setState(() {
// Rebuilding when any of the controllers tick, i.e. when the items are
// animating.
});
}
}
class _RailDestination extends StatelessWidget {
_RailDestination({
@required this.minWidth,
@required this.minExtendedWidth,
@required this.icon,
@required this.label,
@required this.destinationAnimation,
@required this.extendedTransitionAnimation,
@required this.labelType,
@required this.selected,
@required this.iconTheme,
@required this.labelTextStyle,
@required this.onTap,
@required this.indexLabel,
}) : assert(minWidth != null),
assert(minExtendedWidth != null),
assert(icon != null),
assert(label != null),
assert(destinationAnimation != null),
assert(extendedTransitionAnimation != null),
assert(labelType != null),
assert(selected != null),
assert(iconTheme != null),
assert(labelTextStyle != null),
assert(onTap != null),
assert(indexLabel != null),
_positionAnimation = CurvedAnimation(
parent: ReverseAnimation(destinationAnimation),
curve: Curves.easeInOut,
reverseCurve: Curves.easeInOut.flipped,
);
final double minWidth;
final double minExtendedWidth;
final Widget icon;
final Widget label;
final Animation<double> destinationAnimation;
final NavigationRailLabelType labelType;
final bool selected;
final Animation<double> extendedTransitionAnimation;
final IconThemeData iconTheme;
final TextStyle labelTextStyle;
final VoidCallback onTap;
final String indexLabel;
final Animation<double> _positionAnimation;
@override
Widget build(BuildContext context) {
final Widget themedIcon = IconTheme(
data: iconTheme,
child: icon,
);
final Widget styledLabel = DefaultTextStyle(
style: labelTextStyle,
child: label,
);
Widget content;
switch (labelType) {
case NavigationRailLabelType.none:
final Widget iconPart = SizedBox(
width: minWidth,
height: minWidth,
child: Align(
alignment: Alignment.center,
child: themedIcon,
),
);
if (extendedTransitionAnimation.value == 0) {
content = Stack(
children: <Widget>[
iconPart,
// For semantics when label is not showing,
SizedBox(
width: 0,
height: 0,
child: Opacity(
alwaysIncludeSemantics: true,
opacity: 0.0,
child: label,
),
),
]
);
} else {
content = ConstrainedBox(
constraints: BoxConstraints(
minWidth: lerpDouble(minWidth, minExtendedWidth, extendedTransitionAnimation.value),
),
child: ClipRect(
child: Row(
children: <Widget>[
iconPart,
Align(
heightFactor: 1.0,
widthFactor: extendedTransitionAnimation.value,
alignment: AlignmentDirectional.centerStart,
child: Opacity(
alwaysIncludeSemantics: true,
opacity: _extendedLabelFadeValue(),
child: styledLabel,
),
),
const SizedBox(width: _horizontalDestinationPadding),
],
),
),
);
}
break;
case NavigationRailLabelType.selected:
final double appearingAnimationValue = 1 - _positionAnimation.value;
final double verticalPadding = lerpDouble(_verticalDestinationPaddingNoLabel, _verticalDestinationPaddingWithLabel, appearingAnimationValue);
content = Container(
constraints: BoxConstraints(
minWidth: minWidth,
minHeight: minWidth,
),
padding: const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
child: ClipRect(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizedBox(height: verticalPadding),
themedIcon,
Align(
alignment: Alignment.topCenter,
heightFactor: appearingAnimationValue,
widthFactor: 1.0,
child: Opacity(
alwaysIncludeSemantics: true,
opacity: selected ? _normalLabelFadeInValue() : _normalLabelFadeOutValue(),
child: styledLabel,
),
),
SizedBox(height: verticalPadding),
],
),
),
);
break;
case NavigationRailLabelType.all:
content = Container(
constraints: BoxConstraints(
minWidth: minWidth,
minHeight: minWidth,
),
padding: const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding),
child: Column(
children: <Widget>[
const SizedBox(height: _verticalDestinationPaddingWithLabel),
themedIcon,
styledLabel,
const SizedBox(height: _verticalDestinationPaddingWithLabel),
],
),
);
break;
}
final ColorScheme colors = Theme.of(context).colorScheme;
return Semantics(
container: true,
selected: selected,
child: Stack(
children: <Widget>[
Material(
type: MaterialType.transparency,
clipBehavior: Clip.none,
child: InkResponse(
onTap: onTap,
onHover: (_) {},
highlightShape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(minWidth / 2.0)),
containedInkWell: true,
splashColor: colors.primary.withOpacity(0.12),
hoverColor: colors.primary.withOpacity(0.04),
child: content,
),
),
Semantics(
label: indexLabel,
),
]
),
);
}
double _normalLabelFadeInValue() {
if (destinationAnimation.value < 0.25) {
return 0;
} else if (destinationAnimation.value < 0.75) {
return (destinationAnimation.value - 0.25) * 2;
} else {
return 1;
}
}
double _normalLabelFadeOutValue() {
if (destinationAnimation.value > 0.75) {
return (destinationAnimation.value - 0.75) * 4.0;
} else {
return 0;
}
}
double _extendedLabelFadeValue() {
return extendedTransitionAnimation.value < 0.25 ? extendedTransitionAnimation.value * 4.0 : 1.0;
}
}
/// Defines the behavior of the labels of a [NavigationRail].
///
/// See also:
///
/// * [NavigationRail]
enum NavigationRailLabelType {
/// Only the [NavigationRailDestination]s are shown.
none,
/// Only the selected [NavigationRailDestination] will show its label.
///
/// The label will animate in and out as new [NavigationRailDestination]s are
/// selected.
selected,
/// All [NavigationRailDestination]s will show their label.
all,
}
/// Defines a [NavigationRail] button that represents one "destination" view.
///
/// See also:
///
/// * [NavigationRail]
class NavigationRailDestination {
/// Creates a destination that is used with [NavigationRail.destinations].
///
/// [icon] and [label] must be non-null. When the [NavigationRail.labelType]
/// is [NavigationRailLabelType.none], the label is still used for semantics,
/// and may still be used if [NavigationRail.extended] is true.
const NavigationRailDestination({
@required this.icon,
Widget selectedIcon,
this.label,
}) : selectedIcon = selectedIcon ?? icon,
assert(icon != null);
/// The icon of the destination.
///
/// Typically the icon is an [Icon] or an [ImageIcon] widget. If another type
/// of widget is provided then it should configure itself to match the current
/// [IconTheme] size and color.
///
/// If [selectedIcon] is provided, this will only be displayed when the
/// destination is not selected.
///
/// To make the [NavigationRail] more accessible, consider choosing an
/// icon with a stroked and filled version, such as [Icons.cloud] and
/// [Icons.cloud_queue]. The [icon] should be set to the stroked version and
/// [selectedIcon] to the filled version.
final Widget icon;
/// An alternative icon displayed when this destination is selected.
///
/// If this icon is not provided, the [NavigationRail] will display [icon] in
/// either state. The size, color, and opacity of the
/// [NavigationRail.selectedIconTheme] will still apply.
///
/// See also:
///
/// * [NavigationRailDestination.icon], for a description of how to pair
/// icons.
final Widget selectedIcon;
/// The label for the destination.
///
/// The label must be provided when used with the [NavigationRail]. When the
/// [NavigationRail.labelType] is [NavigationRailLabelType.none], the label is
/// still used for semantics, and may still be used if
/// [NavigationRail.extended] is true.
final Widget label;
}
class _ExtendedNavigationRailAnimation extends InheritedWidget {
const _ExtendedNavigationRailAnimation({
Key key,
@required this.animation,
@required Widget child,
}) : assert(child != null),
super(key: key, child: child);
final Animation<double> animation;
@override
bool updateShouldNotify(_ExtendedNavigationRailAnimation old) => animation != old.animation;
}
const double _minRailWidth = 72.0;
const double _minExtendedRailWidth = 256.0;
const double _horizontalDestinationPadding = 8.0;
const double _verticalDestinationPaddingNoLabel = 24.0;
const double _verticalDestinationPaddingWithLabel = 16.0;
const Widget _verticalSpacer = SizedBox(height: 8.0);
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'navigation_rail.dart';
import 'theme.dart';
import 'theme_data.dart';
/// Defines default property values for descendant [NavigationRail]
/// widgets.
///
/// Descendant widgets obtain the current [NavigationRailThemeData] object
/// using `NavigationRailTheme.of(context)`. Instances of
/// [NavigationRailThemeData] can be customized with
/// [NavigationRailThemeData.copyWith].
///
/// Typically a [NavigationRailThemeData] is specified as part of the
/// overall [Theme] with [ThemeData.navigationRailTheme].
///
/// All [NavigationRailThemeData] properties are `null` by default.
/// When null, the [NavigationRail] will use the values from [ThemeData]
/// if they exist, otherwise it will provide its own defaults based on the
/// overall [Theme]'s textTheme and colorScheme. See the individual
/// [NavigationRail] properties for details.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
class NavigationRailThemeData with Diagnosticable {
/// Creates a theme that can be used for [ThemeData.navigationRailTheme].
const NavigationRailThemeData({
this.backgroundColor,
this.elevation,
this.unselectedLabelTextStyle,
this.selectedLabelTextStyle,
this.unselectedIconTheme,
this.selectedIconTheme,
this.groupAlignment,
this.labelType,
});
/// Color to be used for the [NavigationRail]'s background.
final Color backgroundColor;
/// The z-coordinate to be used for the [NavigationRail]'s elevation.
final double elevation;
/// The style to merge with the default text style for
/// [NavigationRailDestination] labels, when the destination is not selected.
final TextStyle unselectedLabelTextStyle;
/// The style to merge with the default text style for
/// [NavigationRailDestination] labels, when the destination is selected.
final TextStyle selectedLabelTextStyle;
/// The theme to merge with the default icon theme for
/// [NavigationRailDestination] icons, when the destination is not selected.
final IconThemeData unselectedIconTheme;
/// The theme to merge with the default icon theme for
/// [NavigationRailDestination] icons, when the destination is selected.
final IconThemeData selectedIconTheme;
/// The alignment for the [NavigationRailDestination]s as they are positioned
/// within the [NavigationRail].
final double groupAlignment;
/// The type that defines the layout and behavior of the labels in the
/// [NavigationRail].
final NavigationRailLabelType labelType;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
NavigationRailThemeData copyWith({
Color backgroundColor,
double elevation,
TextStyle unselectedLabelTextStyle,
TextStyle selectedLabelTextStyle,
IconThemeData unselectedIconTheme,
IconThemeData selectedIconTheme,
double groupAlignment,
NavigationRailLabelType labelType,
}) {
return NavigationRailThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
elevation: elevation ?? this.elevation,
unselectedLabelTextStyle: unselectedLabelTextStyle ?? this.unselectedLabelTextStyle,
selectedLabelTextStyle: selectedLabelTextStyle ?? this.selectedLabelTextStyle,
unselectedIconTheme: unselectedIconTheme ?? this.unselectedIconTheme,
selectedIconTheme: selectedIconTheme ?? this.selectedIconTheme,
groupAlignment: groupAlignment ?? this.groupAlignment,
labelType: labelType ?? this.labelType,
);
}
/// Linearly interpolate between two navigation rail themes.
///
/// If both arguments are null then null is returned.
///
/// {@macro dart.ui.shadow.lerp}
static NavigationRailThemeData lerp(NavigationRailThemeData a, NavigationRailThemeData b, double t) {
assert(t != null);
if (a == null && b == null)
return null;
return NavigationRailThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
unselectedLabelTextStyle: TextStyle.lerp(a?.unselectedLabelTextStyle, b?.unselectedLabelTextStyle, t),
selectedLabelTextStyle: TextStyle.lerp(a?.selectedLabelTextStyle, b?.selectedLabelTextStyle, t),
unselectedIconTheme: IconThemeData.lerp(a?.unselectedIconTheme, b?.unselectedIconTheme, t),
selectedIconTheme: IconThemeData.lerp(a?.selectedIconTheme, b?.selectedIconTheme, t),
groupAlignment: lerpDouble(a?.groupAlignment, b?.groupAlignment, t),
labelType: t < 0.5 ? a?.labelType : b?.labelType,
);
}
@override
int get hashCode {
return hashValues(
backgroundColor,
elevation,
unselectedLabelTextStyle,
selectedLabelTextStyle,
unselectedIconTheme,
selectedIconTheme,
groupAlignment,
labelType,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is NavigationRailThemeData
&& other.backgroundColor == backgroundColor
&& other.elevation == elevation
&& other.unselectedLabelTextStyle == unselectedLabelTextStyle
&& other.selectedLabelTextStyle == selectedLabelTextStyle
&& other.unselectedIconTheme == unselectedIconTheme
&& other.selectedIconTheme == selectedIconTheme
&& other.groupAlignment == groupAlignment
&& other.labelType == labelType;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
const NavigationRailThemeData defaultData = NavigationRailThemeData();
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: defaultData.backgroundColor));
properties.add(DoubleProperty('elevation', elevation, defaultValue: defaultData.elevation));
properties.add(DiagnosticsProperty<TextStyle>('unselectedLabelTextStyle', unselectedLabelTextStyle, defaultValue: defaultData.unselectedLabelTextStyle));
properties.add(DiagnosticsProperty<TextStyle>('selectedLabelTextStyle', selectedLabelTextStyle, defaultValue: defaultData.selectedLabelTextStyle));
properties.add(DiagnosticsProperty<IconThemeData>('unselectedIconTheme', unselectedIconTheme, defaultValue: defaultData.unselectedIconTheme));
properties.add(DiagnosticsProperty<IconThemeData>('selectedIconTheme', selectedIconTheme, defaultValue: defaultData.selectedIconTheme));
properties.add(DoubleProperty('groupAlignment', groupAlignment, defaultValue: defaultData.groupAlignment));
properties.add(DiagnosticsProperty<NavigationRailLabelType>('labelType', labelType, defaultValue: defaultData.labelType));
}
}
/// An inherited widget that defines visual properties for [NavigationRail]s and
/// [NavigationRailDestination]s in this widget's subtree.
///
/// Values specified here are used for [NavigationRail] properties that are not
/// given an explicit non-null value.
class NavigationRailTheme extends InheritedTheme {
/// Creates a navigation rail theme that controls the
/// [NavigationRailThemeData] properties for a [NavigationRail].
///
/// The data argument must not be null.
const NavigationRailTheme({
Key key,
@required this.data,
Widget child,
}) : assert(data != null), super(key: key, child: child);
/// Specifies the background color, elevation, label text style, icon theme,
/// group alignment, and label type and border values for descendant
/// [NavigationRail] widgets.
final NavigationRailThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [NavigationRailTheme] widget, then
/// [ThemeData.navigationRailTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// NavigationRailTheme theme = NavigationRailTheme.of(context);
/// ```
static NavigationRailThemeData of(BuildContext context) {
final NavigationRailTheme navigationRailTheme = context.dependOnInheritedWidgetOfExactType<NavigationRailTheme>();
return navigationRailTheme?.data ?? Theme.of(context).navigationRailTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
final NavigationRailTheme ancestorTheme = context.findAncestorWidgetOfExactType<NavigationRailTheme>();
return identical(this, ancestorTheme) ? child : NavigationRailTheme(data: data, child: child);
}
@override
bool updateShouldNotify(NavigationRailTheme oldWidget) => data != oldWidget.data;
}
......@@ -25,6 +25,7 @@ import 'floating_action_button_theme.dart';
import 'ink_splash.dart';
import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart';
import 'navigation_rail_theme.dart';
import 'page_transitions_theme.dart';
import 'popup_menu_theme.dart';
import 'slider_theme.dart';
......@@ -256,6 +257,7 @@ class ThemeData with Diagnosticable {
ColorScheme colorScheme,
DialogTheme dialogTheme,
FloatingActionButtonThemeData floatingActionButtonTheme,
NavigationRailThemeData navigationRailTheme,
Typography typography,
CupertinoThemeData cupertinoOverrideTheme,
SnackBarThemeData snackBarTheme,
......@@ -364,6 +366,7 @@ class ThemeData with Diagnosticable {
);
dialogTheme ??= const DialogTheme();
floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
navigationRailTheme ??= const NavigationRailThemeData();
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
snackBarTheme ??= const SnackBarThemeData();
bottomSheetTheme ??= const BottomSheetThemeData();
......@@ -428,6 +431,7 @@ class ThemeData with Diagnosticable {
colorScheme: colorScheme,
dialogTheme: dialogTheme,
floatingActionButtonTheme: floatingActionButtonTheme,
navigationRailTheme: navigationRailTheme,
typography: typography,
cupertinoOverrideTheme: cupertinoOverrideTheme,
snackBarTheme: snackBarTheme,
......@@ -505,6 +509,7 @@ class ThemeData with Diagnosticable {
@required this.colorScheme,
@required this.dialogTheme,
@required this.floatingActionButtonTheme,
@required this.navigationRailTheme,
@required this.typography,
@required this.cupertinoOverrideTheme,
@required this.snackBarTheme,
......@@ -566,6 +571,7 @@ class ThemeData with Diagnosticable {
assert(colorScheme != null),
assert(dialogTheme != null),
assert(floatingActionButtonTheme != null),
assert(navigationRailTheme != null),
assert(typography != null),
assert(snackBarTheme != null),
assert(bottomSheetTheme != null),
......@@ -982,6 +988,10 @@ class ThemeData with Diagnosticable {
/// [FloatingActionButton].
final FloatingActionButtonThemeData floatingActionButtonTheme;
/// A theme for customizing the background color, elevation, text style, and
/// icon themes of a [NavigationRail].
final NavigationRailThemeData navigationRailTheme;
/// The color and geometry [TextTheme] values used to configure [textTheme],
/// [primaryTextTheme], and [accentTextTheme].
final Typography typography;
......@@ -1072,6 +1082,7 @@ class ThemeData with Diagnosticable {
ColorScheme colorScheme,
DialogTheme dialogTheme,
FloatingActionButtonThemeData floatingActionButtonTheme,
NavigationRailThemeData navigationRailTheme,
Typography typography,
CupertinoThemeData cupertinoOverrideTheme,
SnackBarThemeData snackBarTheme,
......@@ -1138,6 +1149,7 @@ class ThemeData with Diagnosticable {
colorScheme: colorScheme ?? this.colorScheme,
dialogTheme: dialogTheme ?? this.dialogTheme,
floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
typography: typography ?? this.typography,
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
......@@ -1282,6 +1294,7 @@ class ThemeData with Diagnosticable {
colorScheme: ColorScheme.lerp(a.colorScheme, b.colorScheme, t),
dialogTheme: DialogTheme.lerp(a.dialogTheme, b.dialogTheme, t),
floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t),
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t),
typography: Typography.lerp(a.typography, b.typography, t),
cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
snackBarTheme: SnackBarThemeData.lerp(a.snackBarTheme, b.snackBarTheme, t),
......@@ -1354,6 +1367,7 @@ class ThemeData with Diagnosticable {
&& other.colorScheme == colorScheme
&& other.dialogTheme == dialogTheme
&& other.floatingActionButtonTheme == floatingActionButtonTheme
&& other.navigationRailTheme == navigationRailTheme
&& other.typography == typography
&& other.cupertinoOverrideTheme == cupertinoOverrideTheme
&& other.snackBarTheme == snackBarTheme
......@@ -1425,6 +1439,7 @@ class ThemeData with Diagnosticable {
colorScheme,
dialogTheme,
floatingActionButtonTheme,
navigationRailTheme,
typography,
cupertinoOverrideTheme,
snackBarTheme,
......@@ -1492,6 +1507,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<ColorScheme>('colorScheme', colorScheme, defaultValue: defaultData.colorScheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DialogTheme>('dialogTheme', dialogTheme, defaultValue: defaultData.dialogTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonThemeData', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailThemeData', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<Typography>('typography', typography, defaultValue: defaultData.typography, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<CupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SnackBarThemeData>('snackBarTheme', snackBarTheme, defaultValue: defaultData.snackBarTheme, level: DiagnosticLevel.debug));
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
void main() {
testWidgets('Custom selected and unselected textStyles are honored', (WidgetTester tester) async {
const TextStyle selectedTextStyle = TextStyle(fontWeight: FontWeight.w300, fontSize: 17.0);
const TextStyle unselectedTextStyle = TextStyle(fontWeight: FontWeight.w800, fontSize: 11.0);
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
selectedLabelTextStyle: selectedTextStyle,
unselectedLabelTextStyle: unselectedTextStyle,
),
);
final TextStyle actualSelectedTextStyle = tester.renderObject<RenderParagraph>(find.text('Abc')).text.style;
final TextStyle actualUnselectedTextStyle = tester.renderObject<RenderParagraph>(find.text('Def')).text.style;
expect(actualSelectedTextStyle.fontSize, equals(selectedTextStyle.fontSize));
expect(actualSelectedTextStyle.fontWeight, equals(selectedTextStyle.fontWeight));
expect(actualUnselectedTextStyle.fontSize, equals(actualUnselectedTextStyle.fontSize));
expect(actualUnselectedTextStyle.fontWeight, equals(actualUnselectedTextStyle.fontWeight));
});
testWidgets('Custom selected and unselected iconThemes are honored', (WidgetTester tester) async {
const IconThemeData selectedIconTheme = IconThemeData(size: 36, color: Color(0x00000001));
const IconThemeData unselectedIconTheme = IconThemeData(size: 18, color: Color(0x00000002));
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
selectedIconTheme: selectedIconTheme,
unselectedIconTheme: unselectedIconTheme,
),
);
final TextStyle actualSelectedIconTheme = _iconStyle(tester, Icons.favorite);
final TextStyle actualUnselectedIconTheme = _iconStyle(tester, Icons.bookmark_border);
expect(actualSelectedIconTheme.color, equals(selectedIconTheme.color));
expect(actualSelectedIconTheme.fontSize, equals(selectedIconTheme.size));
expect(actualUnselectedIconTheme.color, equals(unselectedIconTheme.color));
expect(actualUnselectedIconTheme.fontSize, equals(unselectedIconTheme.size));
});
testWidgets('backgroundColor can be changed', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
),
);
expect(_railMaterial(tester).color, equals(Colors.white));
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
backgroundColor: Colors.green,
),
);
expect(_railMaterial(tester).color, equals(Colors.green));
});
testWidgets('elevation can be changed', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
),
);
expect(_railMaterial(tester).elevation, equals(0));
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
elevation: 7,
),
);
expect(_railMaterial(tester).elevation, equals(7));
});
testWidgets('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
});
testWidgets('Renders at the correct default width - [labelType]=selected', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
labelType: NavigationRailLabelType.selected,
destinations: _destinations(),
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
});
testWidgets('Renders at the correct default width - [labelType]=all', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
labelType: NavigationRailLabelType.all,
destinations: _destinations(),
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
});
testWidgets('Renders wider for a destination with a long label - [labelType]=all', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
labelType: NavigationRailLabelType.all,
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('Abc'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.bookmark),
label: Text('Longer Label'),
),
],
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
// Total padding is 16 (8 on each side).
expect(renderBox.size.width, _labelRenderBox(tester, 'Longer Label').size.width + 16.0);
});
testWidgets('Renders only icons - [labelType]=none (default)', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
),
);
expect(find.byIcon(Icons.favorite), findsOneWidget);
expect(find.byIcon(Icons.bookmark_border), findsOneWidget);
expect(find.byIcon(Icons.star_border), findsOneWidget);
expect(find.byIcon(Icons.hotel), findsOneWidget);
// When there are no labels, a 0 opacity label is still shown for semantics.
expect(_labelOpacity(tester, 'Abc'), 0);
expect(_labelOpacity(tester, 'Def'), 0);
expect(_labelOpacity(tester, 'Ghi'), 0);
expect(_labelOpacity(tester, 'Jkl'), 0);
});
testWidgets('Renders icons and labels - [labelType]=all', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
),
);
expect(find.byIcon(Icons.favorite), findsOneWidget);
expect(find.byIcon(Icons.bookmark_border), findsOneWidget);
expect(find.byIcon(Icons.star_border), findsOneWidget);
expect(find.byIcon(Icons.hotel), findsOneWidget);
expect(find.text('Abc'), findsOneWidget);
expect(find.text('Def'), findsOneWidget);
expect(find.text('Ghi'), findsOneWidget);
expect(find.text('Jkl'), findsOneWidget);
// When displaying all labels, there is no opacity.
expect(_opacityAboveLabel('Abc'), findsNothing);
expect(_opacityAboveLabel('Def'), findsNothing);
expect(_opacityAboveLabel('Ghi'), findsNothing);
expect(_opacityAboveLabel('Jkl'), findsNothing);
});
testWidgets('Renders icons and selected label - [labelType]=selected', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.selected,
),
);
expect(find.byIcon(Icons.favorite), findsOneWidget);
expect(find.byIcon(Icons.bookmark_border), findsOneWidget);
expect(find.byIcon(Icons.star_border), findsOneWidget);
expect(find.byIcon(Icons.hotel), findsOneWidget);
// Only the selected label is visible.
expect(_labelOpacity(tester, 'Abc'), 1);
expect(_labelOpacity(tester, 'Def'), 0);
expect(_labelOpacity(tester, 'Ghi'), 0);
expect(_labelOpacity(tester, 'Jkl'), 0);
});
testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0', (WidgetTester tester) async {
// Since the rail is icon only, its destinations should not be affected by
// textScaleFactor.
await _pumpNavigationRail(
tester,
textScaleFactor: 3.0,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75', (WidgetTester tester) async {
// Since the rail is icon only, its destinations should not be affected by
// textScaleFactor.
await _pumpNavigationRail(
tester,
textScaleFactor: 0.75,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.selected,
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0,
),
),
);
expect(
firstLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstLabelRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 + firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
textScaleFactor: 3.0,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.selected,
),
);
// The rail and destinations sizes grow to fit the larger text labels.
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 142.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
// The first label sits right below the first icon.
final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
expect(
firstLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - firstLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + firstIconRenderBox.size.height,
),
),
);
nextDestinationY += 16.0 + firstIconRenderBox.size.height + firstLabelRenderBox.size.height + 16.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + 24.0,
),
),
);
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + 24.0,
),
),
);
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + 24.0,
),
),
);
});
testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
textScaleFactor: 0.75,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.selected,
),
);
// A smaller textScaleFactor will not reduce the default width of the rail
// since there is a minWidth.
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0,
),
),
);
expect(
firstLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstLabelRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 + firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
)),
);
});
testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
firstLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + firstIconRenderBox.size.height,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
secondLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + secondIconRenderBox.size.height,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
thirdLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + thirdIconRenderBox.size.height,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
fourthLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + fourthIconRenderBox.size.height,
),
),
);
});
testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
textScaleFactor: 3.0,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
),
);
// The rail and destinations sizes grow to fit the larger text labels.
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 142.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
firstLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - firstLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + firstIconRenderBox.size.height,
),
),
);
nextDestinationY += 16.0 + firstIconRenderBox.size.height + firstLabelRenderBox.size.height + 16.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
secondLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - secondLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + secondIconRenderBox.size.height,
),
),
);
nextDestinationY += 16.0 + secondIconRenderBox.size.height + secondLabelRenderBox.size.height + 16.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
thirdLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - thirdLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + thirdIconRenderBox.size.height,
),
),
);
nextDestinationY += 16.0 + thirdIconRenderBox.size.height + thirdLabelRenderBox.size.height + 16.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
fourthLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(142.0 - fourthLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + fourthIconRenderBox.size.height,
),
),
);
});
testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
textScaleFactor: 0.75,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
labelType: NavigationRailLabelType.all,
),
);
// A smaller textScaleFactor will not reduce the default size of the rail.
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 72.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
firstLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + firstIconRenderBox.size.height,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
secondLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + secondIconRenderBox.size.height,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
thirdLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + thirdIconRenderBox.size.height,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + 16.0,
),
),
);
expect(
fourthLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthLabelRenderBox.size.width) / 2.0,
nextDestinationY + 16.0 + fourthIconRenderBox.size.height,
),
),
);
});
testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
minWidth: 56.0,
destinations: _destinations(),
),
);
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 56.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 56 below the first destination.
nextDestinationY += 56.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 56 below the second destination.
nextDestinationY += 56.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 56 below the third destination.
nextDestinationY += 56.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
textScaleFactor: 3.0,
navigationRail: NavigationRail(
selectedIndex: 0,
minWidth: 56.0,
destinations: _destinations(),
),
);
// Since the rail is icon only, its preferred width should not be affected
// by textScaleFactor.
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 56.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 56 below the first destination.
nextDestinationY += 56.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 56 below the second destination.
nextDestinationY += 56.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 56 below the third destination.
nextDestinationY += 56.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
textScaleFactor: 3.0,
navigationRail: NavigationRail(
selectedIndex: 0,
minWidth: 56.0,
destinations: _destinations(),
),
);
// Since the rail is icon only, its preferred width should not be affected
// by textScaleFactor.
final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail));
expect(renderBox.size.width, 56.0);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 56 below the first destination.
nextDestinationY += 56.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 56 below the second destination.
nextDestinationY += 56.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 56 below the third destination.
nextDestinationY += 56.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(56.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Group alignment works - [groupAlignment]=-1.0 (default)', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
),
);
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
groupAlignment: 0.0,
destinations: _destinations(),
),
);
double nextDestinationY = 160.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
groupAlignment: 1.0,
destinations: _destinations(),
),
);
double nextDestinationY = 312.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Leading and trailing appear in the correct places', (WidgetTester tester) async {
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
leading: FloatingActionButton(onPressed: () { }),
trailing: FloatingActionButton(onPressed: () { }),
destinations: _destinations(),
),
);
final RenderBox leading = tester.renderObject<RenderBox>(find.byType(FloatingActionButton).at(0));
final RenderBox trailing = tester.renderObject<RenderBox>(find.byType(FloatingActionButton).at(1));
expect(leading.localToGlobal(Offset.zero), const Offset(0.0, 8.0));
expect(trailing.localToGlobal(Offset.zero), const Offset(0.0, 360.0));
});
testWidgets('Extended rail animates the width and labels appear - [textDirection]=LTR', (WidgetTester tester) async {
bool extended = false;
StateSetter stateSetter;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
stateSetter = setState;
return Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
extended: extended,
),
const Expanded(
child: Text('body'),
),
],
),
);
},
),
),
);
final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
expect(rail.size.width, equals(72.0));
stateSetter(() {
extended = true;
});
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(rail.size.width, equals(164.0));
await tester.pumpAndSettle();
expect(rail.size.width, equals(256.0));
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
expect(
firstLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
72.0,
nextDestinationY + (72.0 - firstLabelRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
expect(
secondLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
72.0,
nextDestinationY + (72.0 - secondLabelRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
expect(
thirdLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
72.0,
nextDestinationY + (72.0 - thirdLabelRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
(72.0 - fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
expect(
fourthLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
72.0,
nextDestinationY + (72.0 - fourthLabelRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Extended rail animates the width and labels appear - [textDirection]=RTL', (WidgetTester tester) async {
bool extended = false;
StateSetter stateSetter;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
stateSetter = setState;
return Directionality(
textDirection: TextDirection.rtl,
child: Scaffold(
body: Row(
textDirection: TextDirection.rtl,
children: <Widget>[
NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
extended: extended,
),
const Expanded(
child: Text('body'),
),
],
),
),
);
},
),
),
);
final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
expect(rail.size.width, equals(72.0));
expect(rail.localToGlobal(Offset.zero), equals(const Offset(728.0, 0.0)));
stateSetter(() {
extended = true;
});
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(rail.size.width, equals(164.0));
expect(rail.localToGlobal(Offset.zero), equals(const Offset(636.0, 0.0)));
await tester.pumpAndSettle();
expect(rail.size.width, equals(256.0));
expect(rail.localToGlobal(Offset.zero), equals(const Offset(544.0, 0.0)));
// The first destination is 8 from the top because of the default vertical
// padding at the to of the rail.
double nextDestinationY = 8.0;
final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite);
final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc');
expect(
firstIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
800.0 - (72.0 + firstIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0,
),
),
);
expect(
firstLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
800.0 - 72.0 - firstLabelRenderBox.size.width,
nextDestinationY + (72.0 - firstLabelRenderBox.size.height) / 2.0,
),
),
);
// The second destination is 72 below the first destination.
nextDestinationY += 72.0;
final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border);
final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def');
expect(
secondIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
800.0 - (72.0 + secondIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0,
),
),
);
expect(
secondLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
800.0 - 72.0 - secondLabelRenderBox.size.width,
nextDestinationY + (72.0 - secondLabelRenderBox.size.height) / 2.0,
),
),
);
// The third destination is 72 below the second destination.
nextDestinationY += 72.0;
final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border);
final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi');
expect(
thirdIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
800.0 - (72.0 + thirdIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0,
),
),
);
expect(
thirdLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
800.0 - 72.0 - thirdLabelRenderBox.size.width,
nextDestinationY + (72.0 - thirdLabelRenderBox.size.height) / 2.0,
),
),
);
// The fourth destination is 72 below the third destination.
nextDestinationY += 72.0;
final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel);
final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl');
expect(
fourthIconRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
800.0 - (72.0 + fourthIconRenderBox.size.width) / 2.0,
nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0,
),
),
);
expect(
fourthLabelRenderBox.localToGlobal(Offset.zero),
equals(
Offset(
800.0 - 72.0 - fourthLabelRenderBox.size.width,
nextDestinationY + (72.0 - fourthLabelRenderBox.size.height) / 2.0,
),
),
);
});
testWidgets('Extended rail gets wider with longer labels are larger text scale', (WidgetTester tester) async {
bool extended = false;
StateSetter stateSetter;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
stateSetter = setState;
return Scaffold(
body: Row(
children: <Widget>[
MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0),
child: NavigationRail(
selectedIndex: 0,
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('Abc'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.bookmark),
label: Text('Longer Label'),
),
],
extended: extended,
),
),
const Expanded(
child: Text('body'),
),
],
),
);
},
),
),
);
final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
expect(rail.size.width, equals(72.0));
stateSetter(() {
extended = true;
});
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(rail.size.width, equals(332.0));
await tester.pumpAndSettle();
expect(rail.size.width, equals(584.0));
});
testWidgets('Extended rail final width can be changed', (WidgetTester tester) async {
bool extended = false;
StateSetter stateSetter;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
stateSetter = setState;
return Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
selectedIndex: 0,
minExtendedWidth: 300,
destinations: _destinations(),
extended: extended,
),
const Expanded(
child: Text('body'),
),
],
),
);
},
),
),
);
final RenderBox rail = tester.firstRenderObject<RenderBox>(find.byType(NavigationRail));
expect(rail.size.width, equals(72.0));
stateSetter(() {
extended = true;
});
await tester.pumpAndSettle();
expect(rail.size.width, equals(300.0));
});
testWidgets('Extended rail animation can be consumed', (WidgetTester tester) async {
bool extended = false;
Animation<double> animation;
StateSetter stateSetter;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
stateSetter = setState;
return Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
selectedIndex: 0,
leading: Builder(
builder: (BuildContext context) {
animation = NavigationRail.extendedAnimation(context);
return FloatingActionButton(onPressed: () { },);
},
),
destinations: _destinations(),
extended: extended,
),
const Expanded(
child: Text('body'),
),
],
),
);
},
),
),
);
expect(animation.isDismissed, isTrue);
stateSetter(() {
extended = true;
});
await tester.pumpAndSettle();
expect(animation.isCompleted, isTrue);
});
testWidgets('onDestinationSelected is called', (WidgetTester tester) async {
int selectedIndex;
await _pumpNavigationRail(
tester,
navigationRail: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
onDestinationSelected: (int index) {
selectedIndex = index;
},
labelType: NavigationRailLabelType.all,
),
);
await tester.tap(find.text('Def'));
expect(selectedIndex, 1);
await tester.tap(find.text('Ghi'));
expect(selectedIndex, 2);
});
testWidgets('Changing destinations animate when [labelType]=selected', (WidgetTester tester) async {
int selectedIndex = 0;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
destinations: _destinations(),
selectedIndex: selectedIndex,
labelType: NavigationRailLabelType.selected,
onDestinationSelected: (int index) {
setState(() {
selectedIndex = index;
});
},
),
const Expanded(
child: Text('body'),
),
],
),
);
},
),
),
);
// Tap the second destination.
await tester.tap(find.byIcon(Icons.bookmark_border));
expect(selectedIndex, 1);
// The second destination animates in.
expect(_labelOpacity(tester, 'Def'), equals(0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(_labelOpacity(tester, 'Def'), equals(0.5));
await tester.pumpAndSettle();
expect(_labelOpacity(tester, 'Def'), equals(1.0));
// Tap the third destination.
await tester.tap(find.byIcon(Icons.star_border));
expect(selectedIndex, 2);
// The second destination animates out quickly and the third destination
// animates in.
expect(_labelOpacity(tester, 'Ghi'), equals(0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 25));
expect(_labelOpacity(tester, 'Def'), equals(0.5));
expect(_labelOpacity(tester, 'Ghi'), equals(0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 25));
expect(_labelOpacity(tester, 'Def'), equals(0.0));
expect(_labelOpacity(tester, 'Ghi'), equals(0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(_labelOpacity(tester, 'Ghi'), equals(0.5));
await tester.pumpAndSettle();
expect(_labelOpacity(tester, 'Ghi'), equals(1.0));
});
testWidgets('Semantics - labelType=[none]', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.none);
expect(semantics, hasSemantics(_expectedSemantics(), ignoreId: true, ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('Semantics - labelType=[selected]', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.selected);
expect(semantics, hasSemantics(_expectedSemantics(), ignoreId: true, ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('Semantics - labelType=[all]', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await _pumpLocalizedTestRail(tester, labelType: NavigationRailLabelType.all);
expect(semantics, hasSemantics(_expectedSemantics(), ignoreId: true, ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('Semantics - extended', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await _pumpLocalizedTestRail(tester, extended: true);
expect(semantics, hasSemantics(_expectedSemantics(), ignoreId: true, ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
}
TestSemantics _expectedSemantics() {
return TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isSelected,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Abc\nTab 1 of 4',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Def\nTab 2 of 4',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Ghi\nTab 3 of 4',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Jkl\nTab 4 of 4',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'body',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
);
}
List<NavigationRailDestination> _destinations() {
return const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('Abc'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.bookmark),
label: Text('Def'),
),
NavigationRailDestination(
icon: Icon(Icons.star_border),
selectedIcon: Icon(Icons.star),
label: Text('Ghi'),
),
NavigationRailDestination(
icon: Icon(Icons.hotel),
selectedIcon: Icon(Icons.home),
label: Text('Jkl'),
),
];
}
Future<void> _pumpNavigationRail(
WidgetTester tester, {
double textScaleFactor = 1.0,
NavigationRail navigationRail,
}) async {
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (BuildContext context) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor),
child: Scaffold(
body: Row(
children: <Widget>[
navigationRail,
const Expanded(
child: Text('body'),
),
],
),
),
);
},
),
),
);
}
Future<void> _pumpLocalizedTestRail(WidgetTester tester, { NavigationRailLabelType labelType, bool extended = false }) async {
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MaterialApp(
home: Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
selectedIndex: 0,
extended: extended,
destinations: _destinations(),
labelType: labelType,
),
const Expanded(
child: Text('body'),
),
],
),
),
),
),
);
}
RenderBox _iconRenderBox(WidgetTester tester, IconData iconData) {
return tester.firstRenderObject<RenderBox>(
find.descendant(
of: find.byIcon(iconData),
matching: find.byType(RichText),
),
);
}
RenderBox _labelRenderBox(WidgetTester tester, String text) {
return tester.firstRenderObject<RenderBox>(
find.descendant(
of: find.text(text),
matching: find.byType(RichText),
),
);
}
TextStyle _iconStyle(WidgetTester tester, IconData icon) {
return tester.widget<RichText>(
find.descendant(
of: find.byIcon(icon),
matching: find.byType(RichText),
),
).text.style;
}
Finder _opacityAboveLabel(String text) {
return find.ancestor(
of: find.text(text),
matching: find.byType(Opacity),
);
}
// Only valid when labelType != all.
double _labelOpacity(WidgetTester tester, String text) {
final Opacity opacityWidget = tester.widget<Opacity>(
find.ancestor(
of: find.text(text),
matching: find.byType(Opacity),
),
);
return opacityWidget.opacity;
}
Material _railMaterial(WidgetTester tester) {
// The first material is for the rail, and the rest are for the destinations.
return tester.firstWidget<Material>(
find.descendant(
of: find.byType(NavigationRail),
matching: find.byType(Material),
),
);
}
\ No newline at end of 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 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('copyWith, ==, hashCode basics', () {
expect(const NavigationRailThemeData(), const NavigationRailThemeData().copyWith());
expect(const NavigationRailThemeData().hashCode, const NavigationRailThemeData().copyWith().hashCode);
});
testWidgets('Default values are used when no NavigationRail or NavigationRailThemeData properties are specified', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
),
),
),
);
expect(_railMaterial(tester).color, ThemeData().colorScheme.surface);
expect(_railMaterial(tester).elevation, 0);
expect(_selectedIconTheme(tester).size, 24.0);
expect(_selectedIconTheme(tester).color, ThemeData().colorScheme.primary);
expect(_selectedIconTheme(tester).opacity, 0.64);
expect(_unselectedIconTheme(tester).size, 24.0);
expect(_unselectedIconTheme(tester).color, ThemeData().colorScheme.onSurface);
expect(_unselectedIconTheme(tester).opacity, 1.0);
expect(_selectedLabelStyle(tester).fontSize, 14.0);
expect(_unselectedLabelStyle(tester).fontSize, 14.0);
expect(_destinationsAlign(tester).alignment, Alignment.topCenter);
expect(_labelType(tester), NavigationRailLabelType.none);
});
testWidgets('NavigationRailThemeData values are used when no NavigationRail properties are specified', (WidgetTester tester) async {
const Color backgroundColor = Color(0x00000001);
const double elevation = 7.0;
const double selectedIconSize = 25.0;
const double unselectedIconSize = 23.0;
const Color selectedIconColor = Color(0x00000002);
const Color unselectedIconColor = Color(0x00000003);
const double selectedIconOpacity = 0.99;
const double unselectedIconOpacity = 0.98;
const double selectedLabelFontSize = 13.0;
const double unselectedLabelFontSize = 11.0;
const double groupAlignment = 0.0;
const NavigationRailLabelType labelType = NavigationRailLabelType.all;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NavigationRailTheme(
data: const NavigationRailThemeData(
backgroundColor: backgroundColor,
elevation: elevation,
selectedIconTheme: IconThemeData(
size: selectedIconSize,
color: selectedIconColor,
opacity: selectedIconOpacity,
),
unselectedIconTheme: IconThemeData(
size: unselectedIconSize,
color: unselectedIconColor,
opacity: unselectedIconOpacity,
),
selectedLabelTextStyle: TextStyle(fontSize: selectedLabelFontSize),
unselectedLabelTextStyle: TextStyle(fontSize: unselectedLabelFontSize),
groupAlignment: groupAlignment,
labelType: labelType,
),
child: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
),
),
),
),
);
expect(_railMaterial(tester).color, backgroundColor);
expect(_railMaterial(tester).elevation, elevation);
expect(_selectedIconTheme(tester).size, selectedIconSize);
expect(_selectedIconTheme(tester).color, selectedIconColor);
expect(_selectedIconTheme(tester).opacity, selectedIconOpacity);
expect(_unselectedIconTheme(tester).size, unselectedIconSize);
expect(_unselectedIconTheme(tester).color, unselectedIconColor);
expect(_unselectedIconTheme(tester).opacity, unselectedIconOpacity);
expect(_selectedLabelStyle(tester).fontSize, selectedLabelFontSize);
expect(_unselectedLabelStyle(tester).fontSize, unselectedLabelFontSize);
expect(_destinationsAlign(tester).alignment, Alignment.center);
expect(_labelType(tester), labelType);
});
testWidgets('NavigationRail values take priority over NavigationRailThemeData values when both properties are specified', (WidgetTester tester) async {
const Color backgroundColor = Color(0x00000001);
const double elevation = 7.0;
const double selectedIconSize = 25.0;
const double unselectedIconSize = 23.0;
const Color selectedIconColor = Color(0x00000002);
const Color unselectedIconColor = Color(0x00000003);
const double selectedIconOpacity = 0.99;
const double unselectedIconOpacity = 0.98;
const double selectedLabelFontSize = 13.0;
const double unselectedLabelFontSize = 11.0;
const double groupAlignment = 0.0;
const NavigationRailLabelType labelType = NavigationRailLabelType.all;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NavigationRailTheme(
data: const NavigationRailThemeData(
backgroundColor: Color(0x00000099),
elevation: 5,
selectedIconTheme: IconThemeData(
size: 31.0,
color: Color(0x00000098),
opacity: 0.81,
),
unselectedIconTheme: IconThemeData(
size: 37.0,
color: Color(0x00000097),
opacity: 0.82,
),
selectedLabelTextStyle: TextStyle(fontSize: 9.0),
unselectedLabelTextStyle: TextStyle(fontSize: 7.0),
groupAlignment: 1.0,
labelType: NavigationRailLabelType.selected,
),
child: NavigationRail(
selectedIndex: 0,
destinations: _destinations(),
backgroundColor: backgroundColor,
elevation: elevation,
selectedIconTheme: const IconThemeData(
size: selectedIconSize,
color: selectedIconColor,
opacity: selectedIconOpacity,
),
unselectedIconTheme: const IconThemeData(
size: unselectedIconSize,
color: unselectedIconColor,
opacity: unselectedIconOpacity,
),
selectedLabelTextStyle: const TextStyle(fontSize: selectedLabelFontSize),
unselectedLabelTextStyle: const TextStyle(fontSize: unselectedLabelFontSize),
groupAlignment: groupAlignment,
labelType: labelType,
),
),
),
),
);
expect(_railMaterial(tester).color, backgroundColor);
expect(_railMaterial(tester).elevation, elevation);
expect(_selectedIconTheme(tester).size, selectedIconSize);
expect(_selectedIconTheme(tester).color, selectedIconColor);
expect(_selectedIconTheme(tester).opacity, selectedIconOpacity);
expect(_unselectedIconTheme(tester).size, unselectedIconSize);
expect(_unselectedIconTheme(tester).color, unselectedIconColor);
expect(_unselectedIconTheme(tester).opacity, unselectedIconOpacity);
expect(_selectedLabelStyle(tester).fontSize, selectedLabelFontSize);
expect(_unselectedLabelStyle(tester).fontSize, unselectedLabelFontSize);
expect(_destinationsAlign(tester).alignment, Alignment.center);
expect(_labelType(tester), labelType);
});
testWidgets('Default debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const NavigationRailThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('Custom debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const NavigationRailThemeData(
backgroundColor: Color(0x00000099),
elevation: 5,
selectedIconTheme: IconThemeData(color: Color(0x00000098)),
unselectedIconTheme: IconThemeData(color: Color(0x00000097)),
selectedLabelTextStyle: TextStyle(fontSize: 9.0),
unselectedLabelTextStyle: TextStyle(fontSize: 7.0),
groupAlignment: 1.0,
labelType: NavigationRailLabelType.selected,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description[0], 'backgroundColor: Color(0x00000099)');
expect(description[1], 'elevation: 5.0');
expect(description[2], 'unselectedLabelTextStyle: TextStyle(inherit: true, size: 7.0)');
expect(description[3], 'selectedLabelTextStyle: TextStyle(inherit: true, size: 9.0)');
// Ignore instance address for IconThemeData.
expect(description[4].contains('unselectedIconTheme: IconThemeData'), isTrue);
expect(description[4].contains('(color: Color(0x00000097))'), isTrue);
expect(description[5].contains('selectedIconTheme: IconThemeData'), isTrue);
expect(description[5].contains('(color: Color(0x00000098))'), isTrue);
expect(description[6], 'groupAlignment: 1.0');
expect(description[7], 'labelType: NavigationRailLabelType.selected');
});
}
List<NavigationRailDestination> _destinations() {
return const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('Abc'),
),
NavigationRailDestination(
icon: Icon(Icons.star_border),
selectedIcon: Icon(Icons.star),
label: Text('Def'),
),
];
}
Material _railMaterial(WidgetTester tester) {
// The first material is for the rail, and the rest are for the destinations.
return tester.firstWidget<Material>(
find.descendant(
of: find.byType(NavigationRail),
matching: find.byType(Material),
),
);
}
IconThemeData _selectedIconTheme(WidgetTester tester) {
return _iconTheme(tester, Icons.favorite);
}
IconThemeData _unselectedIconTheme(WidgetTester tester) {
return _iconTheme(tester, Icons.star_border);
}
IconThemeData _iconTheme(WidgetTester tester, IconData icon) {
// The first IconTheme is the one added by the navigation rail.
return tester.firstWidget<IconTheme>(
find.ancestor(
of: find.byIcon(icon),
matching: find.byType(IconTheme),
),
).data;
}
TextStyle _selectedLabelStyle(WidgetTester tester) {
return tester.widget<RichText>(
find.descendant(
of: find.text('Abc'),
matching: find.byType(RichText),
),
).text.style;
}
TextStyle _unselectedLabelStyle(WidgetTester tester) {
return tester.widget<RichText>(
find.descendant(
of: find.text('Def'),
matching: find.byType(RichText),
),
).text.style;
}
Align _destinationsAlign(WidgetTester tester) {
// The first Expanded widget is the one within the main Column for the rail
// content.
return tester.firstWidget<Align>(
find.descendant(
of: find.byType(Expanded),
matching: find.byType(Align),
),
);
}
NavigationRailLabelType _labelType(WidgetTester tester) {
if (_opacityAboveLabel('Abc').evaluate().isNotEmpty && _opacityAboveLabel('Def').evaluate().isNotEmpty) {
return _labelOpacity(tester, 'Abc') == 1 ? NavigationRailLabelType.selected : NavigationRailLabelType.none;
} else {
return NavigationRailLabelType.all;
}
}
Finder _opacityAboveLabel(String text) {
return find.ancestor(
of: find.text(text),
matching: find.byType(Opacity),
);
}
// Only valid when labelType != all.
double _labelOpacity(WidgetTester tester, String text) {
final Opacity opacityWidget = tester.widget<Opacity>(
find.ancestor(
of: find.text(text),
matching: find.byType(Opacity),
),
);
return opacityWidget.opacity;
}
......@@ -256,6 +256,7 @@ void main() {
colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.black),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
typography: Typography.material2018(platform: TargetPlatform.android),
cupertinoOverrideTheme: null,
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.black),
......@@ -335,6 +336,7 @@ void main() {
colorScheme: const ColorScheme.light(),
dialogTheme: const DialogTheme(backgroundColor: Colors.white),
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
typography: Typography.material2018(platform: TargetPlatform.iOS),
cupertinoOverrideTheme: ThemeData.light().cupertinoOverrideTheme,
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.white),
......@@ -400,6 +402,7 @@ void main() {
colorScheme: otherTheme.colorScheme,
dialogTheme: otherTheme.dialogTheme,
floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
navigationRailTheme: otherTheme.navigationRailTheme,
typography: otherTheme.typography,
cupertinoOverrideTheme: otherTheme.cupertinoOverrideTheme,
snackBarTheme: otherTheme.snackBarTheme,
......@@ -466,6 +469,7 @@ void main() {
expect(themeDataCopy.colorScheme, equals(otherTheme.colorScheme));
expect(themeDataCopy.dialogTheme, equals(otherTheme.dialogTheme));
expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme));
expect(themeDataCopy.typography, equals(otherTheme.typography));
expect(themeDataCopy.cupertinoOverrideTheme, equals(otherTheme.cupertinoOverrideTheme));
expect(themeDataCopy.snackBarTheme, equals(otherTheme.snackBarTheme));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment