Unverified Commit 617ca627 authored by Hans Muller's avatar Hans Muller Committed by GitHub

[Material] Expand BottomNavigationBar API (reprise) (#28159)

parent b96ae03b
...@@ -189,6 +189,7 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo> ...@@ -189,6 +189,7 @@ class _BottomNavigationDemoState extends State<BottomNavigationDemo>
.toList(), .toList(),
currentIndex: _currentIndex, currentIndex: _currentIndex,
type: _type, type: _type,
//iconSize: 4.0,
onTap: (int index) { onTap: (int index) {
setState(() { setState(() {
_navigationViews[_currentIndex].controller.reverse(); _navigationViews[_currentIndex].controller.reverse();
......
...@@ -17,11 +17,6 @@ import 'material_localizations.dart'; ...@@ -17,11 +17,6 @@ import 'material_localizations.dart';
import 'text_theme.dart'; import 'text_theme.dart';
import 'theme.dart'; import 'theme.dart';
const double _kActiveFontSize = 14.0;
const double _kInactiveFontSize = 12.0;
const double _kTopMargin = 6.0;
const double _kBottomMargin = 8.0;
/// Defines the layout and behavior of a [BottomNavigationBar]. /// Defines the layout and behavior of a [BottomNavigationBar].
/// ///
/// See also: /// See also:
...@@ -30,18 +25,16 @@ const double _kBottomMargin = 8.0; ...@@ -30,18 +25,16 @@ const double _kBottomMargin = 8.0;
/// * [BottomNavigationBarItem] /// * [BottomNavigationBarItem]
/// * <https://material.io/design/components/bottom-navigation.html#specs> /// * <https://material.io/design/components/bottom-navigation.html#specs>
enum BottomNavigationBarType { enum BottomNavigationBarType {
/// The [BottomNavigationBar]'s [BottomNavigationBarItem]s have fixed width, always /// The [BottomNavigationBar]'s [BottomNavigationBarItem]s have fixed width.
/// display their text labels, and do not shift when tapped.
fixed, fixed,
/// The location and size of the [BottomNavigationBar] [BottomNavigationBarItem]s /// The location and size of the [BottomNavigationBar] [BottomNavigationBarItem]s
/// animate and labels fade in when they are tapped. Only the selected item /// animate and labels fade in when they are tapped.
/// displays its text label.
shifting, shifting,
} }
/// A material widget displayed at the bottom of an app for selecting among a /// A material widget that's displayed at the bottom of an app for selecting
/// small number of views, typically between three and five. /// among a small number of views, typically between three and five.
/// ///
/// The bottom navigation bar consists of multiple items in the form of /// The bottom navigation bar consists of multiple items in the form of
/// text labels, icons, or both, laid out on top of a piece of material. It /// text labels, icons, or both, laid out on top of a piece of material. It
...@@ -52,18 +45,19 @@ enum BottomNavigationBarType { ...@@ -52,18 +45,19 @@ enum BottomNavigationBarType {
/// where it is provided as the [Scaffold.bottomNavigationBar] argument. /// where it is provided as the [Scaffold.bottomNavigationBar] argument.
/// ///
/// The bottom navigation bar's [type] changes how its [items] are displayed. /// The bottom navigation bar's [type] changes how its [items] are displayed.
/// If not specified it's automatically set to [BottomNavigationBarType.fixed] /// If not specified, then it's automatically set to
/// when there are less than four items, [BottomNavigationBarType.shifting] /// [BottomNavigationBarType.fixed] when there are less than four items, and
/// otherwise. /// [BottomNavigationBarType.shifting] otherwise.
/// ///
/// * [BottomNavigationBarType.fixed], the default when there are less than /// * [BottomNavigationBarType.fixed], the default when there are less than
/// four [items]. The selected item is rendered with [fixedColor] if it's /// four [items]. The selected item is rendered with the
/// non-null, otherwise the theme's [ThemeData.primaryColor] is used. The /// [selectedItemColor] if it's non-null, otherwise the theme's
/// navigation bar's background color is the default [Material] background /// [ThemeData.primaryColor] is used. If [backgroundColor] is null, The
/// navigation bar's background color defaults to the [Material] background
/// color, [ThemeData.canvasColor] (essentially opaque white). /// color, [ThemeData.canvasColor] (essentially opaque white).
/// * [BottomNavigationBarType.shifting], the default when there are four /// * [BottomNavigationBarType.shifting], the default when there are four
/// or more [items]. All items are rendered in white and the navigation bar's /// or more [items]. If [selectedItemColor] is null, all items are rendered
/// background color is the same as the /// in white. The navigation bar's background color is the same as the
/// [BottomNavigationBarItem.backgroundColor] of the selected item. In this /// [BottomNavigationBarItem.backgroundColor] of the selected item. In this
/// case it's assumed that each item will have a different background color /// case it's assumed that each item will have a different background color
/// and that background color will contrast well with white. /// and that background color will contrast well with white.
...@@ -71,10 +65,9 @@ enum BottomNavigationBarType { ...@@ -71,10 +65,9 @@ enum BottomNavigationBarType {
/// {@tool snippet --template=stateful_widget_material} /// {@tool snippet --template=stateful_widget_material}
/// This example shows a [BottomNavigationBar] as it is used within a [Scaffold] /// This example shows a [BottomNavigationBar] as it is used within a [Scaffold]
/// widget. The [BottomNavigationBar] has three [BottomNavigationBarItem] /// widget. The [BottomNavigationBar] has three [BottomNavigationBarItem]
/// widgets and the [currentIndex] is set to index 1. The color of the selected /// widgets and the [currentIndex] is set to index 1. The selected item is
/// item is set to a purple color. A function is called whenever any item is /// purple. The `_onItemTapped` function changes the selected item's index
/// tapped and the function helps display the appropriate [Text] in the body of /// and displays a corresponding message in the center of the [Scaffold].
/// the [Scaffold].
/// ///
/// ```dart /// ```dart
/// int _selectedIndex = 1; /// int _selectedIndex = 1;
...@@ -106,7 +99,7 @@ enum BottomNavigationBarType { ...@@ -106,7 +99,7 @@ enum BottomNavigationBarType {
/// BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')), /// BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
/// ], /// ],
/// currentIndex: _selectedIndex, /// currentIndex: _selectedIndex,
/// fixedColor: Colors.deepPurple, /// selectedItemColor: Colors.deepPurple,
/// onTap: _onItemTapped, /// onTap: _onItemTapped,
/// ), /// ),
/// ); /// );
...@@ -120,25 +113,43 @@ enum BottomNavigationBarType { ...@@ -120,25 +113,43 @@ enum BottomNavigationBarType {
/// * [Scaffold] /// * [Scaffold]
/// * <https://material.io/design/components/bottom-navigation.html> /// * <https://material.io/design/components/bottom-navigation.html>
class BottomNavigationBar extends StatefulWidget { class BottomNavigationBar extends StatefulWidget {
/// Creates a bottom navigation bar, typically used in a [Scaffold] where it /// Creates a bottom navigation bar which is typically used as a
/// is provided as the [Scaffold.bottomNavigationBar] argument. /// [Scaffold]'s [Scaffold.bottomNavigationBar] argument.
/// ///
/// The length of [items] must be at least two and each item's icon and title must be not null. /// The length of [items] must be at least two and each item's icon and title
/// must not be null.
/// ///
/// If [type] is null then [BottomNavigationBarType.fixed] is used when there /// If [type] is null then [BottomNavigationBarType.fixed] is used when there
/// are two or three [items], [BottomNavigationBarType.shifting] otherwise. /// are two or three [items], [BottomNavigationBarType.shifting] otherwise.
/// ///
/// If [fixedColor] is null then the theme's primary color, /// The [iconSize], [selectedFontSize], [unselectedFontSize], and [elevation]
/// [ThemeData.primaryColor], is used. However if [BottomNavigationBar.type] is /// arguments must be non-null and non-negative.
/// [BottomNavigationBarType.shifting] then [fixedColor] is ignored. ///
/// Only one of [selectedItemColor] and [fixedColor] can be specified. The
/// former is preferred, [fixedColor] only exists for the sake of
/// backwards compatibility.
///
/// The [showSelectedLabels] argument must not be non-null.
///
/// The [showUnselectedLabels] argument defaults to `true` if [type] is
/// [BottomNavigationBarType.fixed] and `false` if [type] is
/// [BottomNavigationBarType.shifting].
BottomNavigationBar({ BottomNavigationBar({
Key key, Key key,
@required this.items, @required this.items,
this.onTap, this.onTap,
this.currentIndex = 0, this.currentIndex = 0,
this.elevation = 8.0,
BottomNavigationBarType type, BottomNavigationBarType type,
this.fixedColor, Color fixedColor,
this.backgroundColor,
this.iconSize = 24.0, this.iconSize = 24.0,
Color selectedItemColor,
this.unselectedItemColor,
this.selectedFontSize = 14.0,
this.unselectedFontSize = 12.0,
this.showSelectedLabels = true,
bool showUnselectedLabels,
}) : assert(items != null), }) : assert(items != null),
assert(items.length >= 2), assert(items.length >= 2),
assert( assert(
...@@ -146,42 +157,125 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -146,42 +157,125 @@ class BottomNavigationBar extends StatefulWidget {
'Every item must have a non-null title', 'Every item must have a non-null title',
), ),
assert(0 <= currentIndex && currentIndex < items.length), assert(0 <= currentIndex && currentIndex < items.length),
assert(iconSize != null), assert(elevation != null && elevation >= 0.0),
type = type ?? (items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting), assert(iconSize != null && iconSize >= 0.0),
assert(
selectedItemColor != null ? fixedColor == null : true,
'Either selectedItemColor or fixedColor can be specified, but not both'
),
assert(selectedFontSize != null && selectedFontSize >= 0.0),
assert(unselectedFontSize != null && unselectedFontSize >= 0.0),
assert(showSelectedLabels != null),
type = _type(type, items),
selectedItemColor = selectedItemColor ?? fixedColor,
showUnselectedLabels = showUnselectedLabels ?? _defaultShowUnselected(_type(type, items)),
super(key: key); super(key: key);
/// The interactive items laid out within the bottom navigation bar where each item has an icon and title. /// Defines the appearance of the button items that are arrayed within the
/// bottom navigation bar.
final List<BottomNavigationBarItem> items; final List<BottomNavigationBarItem> items;
/// The callback that is called when a item is tapped. /// Called when one of the [items] is tapped.
/// ///
/// The widget creating the bottom navigation bar needs to keep track of the /// The stateful widget that creates the bottom navigation bar needs to keep
/// current index and call `setState` to rebuild it with the newly provided /// track of the index of the selected [BottomNavigationBarItem] and call
/// index. /// `setState` to rebuild the bottom navigation bar with the new [currentIndex].
final ValueChanged<int> onTap; final ValueChanged<int> onTap;
/// The index into [items] of the current active item. /// The index into [items] for the current active [BottomNavigationBarItem].
final int currentIndex; final int currentIndex;
/// The z-coordinate of this [BottomNavigationBar].
///
/// If null, defaults to `8.0`.
///
/// {@macro flutter.material.material.elevation}
final double elevation;
/// Defines the layout and behavior of a [BottomNavigationBar]. /// Defines the layout and behavior of a [BottomNavigationBar].
/// ///
/// See documentation for [BottomNavigationBarType] for information on the meaning /// See documentation for [BottomNavigationBarType] for information on the
/// of different types. /// meaning of different types.
final BottomNavigationBarType type; final BottomNavigationBarType type;
/// The color of the selected item when bottom navigation bar is /// The value of [selectedItemColor].
/// [BottomNavigationBarType.fixed]. ///
/// This getter only exists for backwards compatibility, the
/// [selectedItemColor] property is preferred.
Color get fixedColor => selectedItemColor;
/// The color of the [BottomNavigationBar] itself.
/// ///
/// If [fixedColor] is null then the theme's primary color, /// If [type] is [BottomNavigationBarType.shifting] and the
/// [ThemeData.primaryColor], is used. However if [BottomNavigationBar.type] is /// [items]s, have [BottomNavigationBarItem.backgroundColor] set, the [item]'s
/// [BottomNavigationBarType.shifting] then [fixedColor] is ignored. /// backgroundColor will splash and overwrite this color.
final Color fixedColor; final Color backgroundColor;
/// The size of all of the [BottomNavigationBarItem] icons. /// The size of all of the [BottomNavigationBarItem] icons.
/// ///
/// See [BottomNavigationBarItem.icon] for more information. /// See [BottomNavigationBarItem.icon] for more information.
final double iconSize; final double iconSize;
/// The color of the selected [BottomNavigationBarItem.icon] and
/// [BottomNavigationBarItem.label].
///
/// If null then the [ThemeData.primaryColor] is used.
final Color selectedItemColor;
/// The color of the unselected [BottomNavigationBarItem.icon] and
/// [BottomNavigationBarItem.label]s.
///
/// If null then the [TextTheme.caption]'s color is used.
final Color unselectedItemColor;
/// The font size of the [BottomNavigationBarItem] labels when they are selected.
///
/// Defaults to `14.0`.
final double selectedFontSize;
/// The font size of the [BottomNavigationBarItem] labels when they are not
/// selected.
///
/// Defaults to `12.0`.
final double unselectedFontSize;
/// Whether the labels are shown for the selected [BottomNavigationBarItem].
final bool showUnselectedLabels;
/// Whether the labels are shown for the unselected [BottomNavigationBarItem]s.
final bool showSelectedLabels;
// Used by the [BottomNavigationBar] constructor to set the [type] parameter.
//
// If type is provided, it is returned. Otherwise,
// [BottomNavigationBarType.fixed] is used for 3 or fewer items, and
// [BottomNavigationBarType.shifting] is used for 4+ items.
static BottomNavigationBarType _type(
BottomNavigationBarType type,
List<BottomNavigationBarItem> items,
) {
if (type != null) {
return type;
}
return items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting;
}
// Used by the [BottomNavigationBar] constructor to set the [showUnselected]
// parameter.
//
// Unselected labels are shown by default for [BottomNavigationBarType.fixed],
// and hidden by default for [BottomNavigationBarType.shifting].
static bool _defaultShowUnselected(BottomNavigationBarType type) {
switch (type) {
case BottomNavigationBarType.shifting:
return false;
case BottomNavigationBarType.fixed:
return true;
}
assert(false);
return false;
}
@override @override
_BottomNavigationBarState createState() => _BottomNavigationBarState(); _BottomNavigationBarState createState() => _BottomNavigationBarState();
} }
...@@ -198,8 +292,17 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -198,8 +292,17 @@ class _BottomNavigationTile extends StatelessWidget {
this.colorTween, this.colorTween,
this.flex, this.flex,
this.selected = false, this.selected = false,
@required this.selectedFontSize,
@required this.unselectedFontSize,
this.showSelectedLabels,
this.showUnselectedLabels,
this.indexLabel, this.indexLabel,
}) : assert(selected != null); }) : assert(type != null),
assert(item != null),
assert(animation != null),
assert(selected != null),
assert(selectedFontSize != null && selectedFontSize >= 0),
assert(unselectedFontSize != null && unselectedFontSize >= 0);
final BottomNavigationBarType type; final BottomNavigationBarType type;
final BottomNavigationBarItem item; final BottomNavigationBarItem item;
...@@ -209,7 +312,11 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -209,7 +312,11 @@ class _BottomNavigationTile extends StatelessWidget {
final ColorTween colorTween; final ColorTween colorTween;
final double flex; final double flex;
final bool selected; final bool selected;
final double selectedFontSize;
final double unselectedFontSize;
final String indexLabel; final String indexLabel;
final bool showSelectedLabels;
final bool showUnselectedLabels;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -218,16 +325,50 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -218,16 +325,50 @@ class _BottomNavigationTile extends StatelessWidget {
// produce smooth animation. We do this by multiplying the flex value // produce smooth animation. We do this by multiplying the flex value
// (which is an integer) by a large number. // (which is an integer) by a large number.
int size; int size;
Widget label;
double bottomPadding = selectedFontSize / 2.0;
double topPadding = selectedFontSize / 2.0;
// Defines the padding for the animating icons + labels.
//
// The animations go from "Unselected":
// =======
// | <-- Padding equal to the text height.
// | ☆
// | text <-- Invisible text.
// =======
//
// To "Selected":
//
// =======
// | <-- Padding equal to 1/2 text height.
// | ☆
// | text
// | <-- Padding equal to 1/2 text height.
// =======
if (showSelectedLabels && !showUnselectedLabels) {
bottomPadding = Tween<double>(
begin: 0.0,
end: selectedFontSize / 2.0,
).evaluate(animation);
topPadding = Tween<double>(
begin: selectedFontSize,
end: selectedFontSize / 2.0,
).evaluate(animation);
}
// Center all icons if no labels are shown.
if (!showSelectedLabels && !showUnselectedLabels) {
bottomPadding = 0.0;
topPadding = selectedFontSize;
}
switch (type) { switch (type) {
case BottomNavigationBarType.fixed: case BottomNavigationBarType.fixed:
size = 1; size = 1;
label = _FixedLabel(colorTween: colorTween, animation: animation, item: item);
break; break;
case BottomNavigationBarType.shifting: case BottomNavigationBarType.shifting:
size = (flex * 1000.0).round(); size = (flex * 1000.0).round();
label = _ShiftingLabel(animation: animation, item: item);
break; break;
} }
...@@ -241,21 +382,31 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -241,21 +382,31 @@ class _BottomNavigationTile extends StatelessWidget {
children: <Widget>[ children: <Widget>[
InkResponse( InkResponse(
onTap: onTap, onTap: onTap,
child: Column( child: Padding(
crossAxisAlignment: CrossAxisAlignment.center, padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ mainAxisAlignment: MainAxisAlignment.spaceBetween,
_TileIcon( mainAxisSize: MainAxisSize.min,
type: type, children: <Widget>[
colorTween: colorTween, _TileIcon(
animation: animation, colorTween: colorTween,
iconSize: iconSize, animation: animation,
selected: selected, iconSize: iconSize,
item: item, selected: selected,
), item: item,
label, ),
], _Label(
colorTween: colorTween,
animation: animation,
item: item,
selectedFontSize: selectedFontSize,
unselectedFontSize: unselectedFontSize,
showSelectedLabels: showSelectedLabels,
showUnselectedLabels: showUnselectedLabels,
),
],
),
), ),
), ),
Semantics( Semantics(
...@@ -272,15 +423,15 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -272,15 +423,15 @@ class _BottomNavigationTile extends StatelessWidget {
class _TileIcon extends StatelessWidget { class _TileIcon extends StatelessWidget {
const _TileIcon({ const _TileIcon({
Key key, Key key,
@required this.type,
@required this.colorTween, @required this.colorTween,
@required this.animation, @required this.animation,
@required this.iconSize, @required this.iconSize,
@required this.selected, @required this.selected,
@required this.item, @required this.item,
}) : super(key: key); }) : assert(selected != null),
assert(item != null),
super(key: key);
final BottomNavigationBarType type;
final ColorTween colorTween; final ColorTween colorTween;
final Animation<double> animation; final Animation<double> animation;
final double iconSize; final double iconSize;
...@@ -289,28 +440,11 @@ class _TileIcon extends StatelessWidget { ...@@ -289,28 +440,11 @@ class _TileIcon extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double tweenStart; final Color iconColor = colorTween.evaluate(animation);
Color iconColor;
switch (type) {
case BottomNavigationBarType.fixed:
tweenStart = 8.0;
iconColor = colorTween.evaluate(animation);
break;
case BottomNavigationBarType.shifting:
tweenStart = 16.0;
iconColor = Colors.white;
break;
}
return Align( return Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
heightFactor: 1.0, heightFactor: 1.0,
child: Container( child: Container(
margin: EdgeInsets.only(
top: Tween<double>(
begin: tweenStart,
end: _kTopMargin,
).evaluate(animation),
),
child: IconTheme( child: IconTheme(
data: IconThemeData( data: IconThemeData(
color: iconColor, color: iconColor,
...@@ -323,89 +457,84 @@ class _TileIcon extends StatelessWidget { ...@@ -323,89 +457,84 @@ class _TileIcon extends StatelessWidget {
} }
} }
class _FixedLabel extends StatelessWidget { class _Label extends StatelessWidget {
const _FixedLabel({ const _Label({
Key key, Key key,
@required this.colorTween, @required this.colorTween,
@required this.animation, @required this.animation,
@required this.item, @required this.item,
}) : super(key: key); @required this.selectedFontSize,
@required this.unselectedFontSize,
@required this.showSelectedLabels,
@required this.showUnselectedLabels,
}) : assert(colorTween != null),
assert(animation != null),
assert(item != null),
assert(selectedFontSize != null),
assert(unselectedFontSize != null),
assert(showSelectedLabels != null),
assert(showUnselectedLabels != null),
super(key: key);
final ColorTween colorTween; final ColorTween colorTween;
final Animation<double> animation; final Animation<double> animation;
final BottomNavigationBarItem item; final BottomNavigationBarItem item;
final double selectedFontSize;
final double unselectedFontSize;
final bool showSelectedLabels;
final bool showUnselectedLabels;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Align( Widget text = DefaultTextStyle.merge(
alignment: Alignment.bottomCenter, style: TextStyle(
heightFactor: 1.0, fontSize: selectedFontSize,
child: Container( color: colorTween.evaluate(animation),
margin: const EdgeInsets.only(bottom: _kBottomMargin), ),
child: DefaultTextStyle.merge( // The font size should grow here when active, but because of the way
style: TextStyle( // font rendering works, it doesn't grow smoothly if we just animate
fontSize: _kActiveFontSize, // the font size, so we use a transform instead.
color: colorTween.evaluate(animation), child: Transform(
), transform: Matrix4.diagonal3(
// The font size should grow here when active, but because of the way Vector3.all(
// font rendering works, it doesn't grow smoothly if we just animate Tween<double>(
// the font size, so we use a transform instead. begin: unselectedFontSize / selectedFontSize,
child: Transform( end: 1.0,
transform: Matrix4.diagonal3( ).evaluate(animation),
Vector3.all(
Tween<double>(
begin: _kInactiveFontSize / _kActiveFontSize,
end: 1.0,
).evaluate(animation),
),
),
alignment: Alignment.bottomCenter,
child: item.title,
), ),
), ),
alignment: Alignment.bottomCenter,
child: item.title,
), ),
); );
}
}
class _ShiftingLabel extends StatelessWidget {
const _ShiftingLabel({
Key key,
@required this.animation,
@required this.item,
}) : super(key: key);
final Animation<double> animation; if (!showUnselectedLabels && !showSelectedLabels) {
final BottomNavigationBarItem item; // Never show any labels.
text = Opacity(
alwaysIncludeSemantics: true,
opacity: 0.0,
child: text,
);
} else if (!showUnselectedLabels) {
// Fade selected labels in.
text = FadeTransition(
alwaysIncludeSemantics: true,
opacity: animation,
child: text,
);
} else if (!showSelectedLabels) {
// Fade selected labels out.
text = FadeTransition(
alwaysIncludeSemantics: true,
opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
child: text,
);
}
@override
Widget build(BuildContext context) {
return Align( return Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
heightFactor: 1.0, heightFactor: 1.0,
child: Container( child: Container(child: text),
margin: EdgeInsets.only(
bottom: Tween<double>(
// In the spec, they just remove the label for inactive items and
// specify a 16dp bottom margin. We don't want to actually remove
// the label because we want to fade it in and out, so this modifies
// the bottom margin to take that into account.
begin: 2.0,
end: _kBottomMargin,
).evaluate(animation),
),
child: FadeTransition(
alwaysIncludeSemantics: true,
opacity: animation,
child: DefaultTextStyle.merge(
style: const TextStyle(
fontSize: _kActiveFontSize,
color: Colors.white,
),
child: item.title,
),
),
),
); );
} }
} }
...@@ -529,63 +658,57 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -529,63 +658,57 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
List<Widget> _createTiles() { List<Widget> _createTiles() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context);
assert(localizations != null); assert(localizations != null);
final List<Widget> children = <Widget>[];
final ThemeData themeData = Theme.of(context);
Color themeColor;
switch (themeData.brightness) {
case Brightness.light:
themeColor = themeData.primaryColor;
break;
case Brightness.dark:
themeColor = themeData.accentColor;
break;
}
ColorTween colorTween;
switch (widget.type) { switch (widget.type) {
case BottomNavigationBarType.fixed: case BottomNavigationBarType.fixed:
final ThemeData themeData = Theme.of(context); colorTween = ColorTween(
final TextTheme textTheme = themeData.textTheme; begin: widget.unselectedItemColor ?? themeData.textTheme.caption.color,
Color themeColor; end: widget.selectedItemColor ?? widget.fixedColor ?? themeColor,
switch (themeData.brightness) {
case Brightness.light:
themeColor = themeData.primaryColor;
break;
case Brightness.dark:
themeColor = themeData.accentColor;
break;
}
final ColorTween colorTween = ColorTween(
begin: textTheme.caption.color,
end: widget.fixedColor ?? themeColor,
); );
for (int i = 0; i < widget.items.length; i += 1) {
children.add(
_BottomNavigationTile(
widget.type,
widget.items[i],
_animations[i],
widget.iconSize,
onTap: () {
if (widget.onTap != null)
widget.onTap(i);
},
colorTween: colorTween,
selected: i == widget.currentIndex,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
),
);
}
break; break;
case BottomNavigationBarType.shifting: case BottomNavigationBarType.shifting:
for (int i = 0; i < widget.items.length; i += 1) { colorTween = ColorTween(
children.add( begin: widget.unselectedItemColor ?? Colors.white,
_BottomNavigationTile( end: widget.selectedItemColor ?? Colors.white,
widget.type, );
widget.items[i],
_animations[i],
widget.iconSize,
onTap: () {
if (widget.onTap != null)
widget.onTap(i);
},
flex: _evaluateFlex(_animations[i]),
selected: i == widget.currentIndex,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
),
);
}
break; break;
} }
return children;
final List<Widget> tiles = <Widget>[];
for (int i = 0; i < widget.items.length; i++) {
tiles.add(_BottomNavigationTile(
widget.type,
widget.items[i],
_animations[i],
widget.iconSize,
selectedFontSize: widget.selectedFontSize,
unselectedFontSize: widget.unselectedFontSize,
onTap: () {
if (widget.onTap != null)
widget.onTap(i);
},
colorTween: colorTween,
flex: _evaluateFlex(_animations[i]),
selected: i == widget.currentIndex,
showSelectedLabels: widget.showSelectedLabels,
showUnselectedLabels: widget.showUnselectedLabels,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
));
}
return tiles;
} }
Widget _createContainer(List<Widget> tiles) { Widget _createContainer(List<Widget> tiles) {
...@@ -602,12 +725,14 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -602,12 +725,14 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context)); assert(debugCheckHasDirectionality(context));
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
assert(debugCheckHasMediaQuery(context));
// Labels apply up to _bottomMargin padding. Remainder is media padding. // Labels apply up to _bottomMargin padding. Remainder is media padding.
final double additionalBottomPadding = math.max(MediaQuery.of(context).padding.bottom - _kBottomMargin, 0.0); final double additionalBottomPadding = math.max(MediaQuery.of(context).padding.bottom - widget.selectedFontSize / 2.0, 0.0);
Color backgroundColor; Color backgroundColor;
switch (widget.type) { switch (widget.type) {
case BottomNavigationBarType.fixed: case BottomNavigationBarType.fixed:
backgroundColor = widget.backgroundColor;
break; break;
case BottomNavigationBarType.shifting: case BottomNavigationBarType.shifting:
backgroundColor = _backgroundColor; backgroundColor = _backgroundColor;
...@@ -616,7 +741,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -616,7 +741,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
return Semantics( return Semantics(
explicitChildNodes: true, explicitChildNodes: true,
child: Material( child: Material(
elevation: 8.0, elevation: widget.elevation,
color: backgroundColor, color: backgroundColor,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding), constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding),
......
...@@ -9,8 +9,8 @@ import 'framework.dart'; ...@@ -9,8 +9,8 @@ import 'framework.dart';
/// An interactive button within either material's [BottomNavigationBar] /// An interactive button within either material's [BottomNavigationBar]
/// or the iOS themed [CupertinoTabBar] with an icon and title. /// or the iOS themed [CupertinoTabBar] with an icon and title.
/// ///
/// This class is rarely used in isolation. Commonly embedded in one of the /// This class is rarely used in isolation. It is typically embedded in one of
/// bottom navigation widgets above. /// the bottom navigation widgets above.
/// ///
/// See also: /// See also:
/// ///
...@@ -67,7 +67,7 @@ class BottomNavigationBarItem { ...@@ -67,7 +67,7 @@ class BottomNavigationBarItem {
/// ///
/// If the navigation bar's type is [BottomNavigationBarType.shifting], then /// If the navigation bar's type is [BottomNavigationBarType.shifting], then
/// the entire bar is flooded with the [backgroundColor] when this item is /// the entire bar is flooded with the [backgroundColor] when this item is
/// tapped. /// tapped. This will override [BottomNavigationBar.backgroundColor].
/// ///
/// Not used for [CupertinoTabBar]. Control the invariant bar color directly /// Not used for [CupertinoTabBar]. Control the invariant bar color directly
/// via [CupertinoTabBar.backgroundColor]. /// via [CupertinoTabBar.backgroundColor].
......
...@@ -6,7 +6,9 @@ import 'dart:io'; ...@@ -6,7 +6,9 @@ import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
...@@ -68,6 +70,325 @@ void main() { ...@@ -68,6 +70,325 @@ void main() {
expect(find.text('Alarm'), findsOneWidget); expect(find.text('Alarm'), findsOneWidget);
}); });
testWidgets('Fixed BottomNavigationBar defaults', (WidgetTester tester) async {
const Color primaryColor = Colors.black;
const Color captionColor = Colors.purple;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
primaryColor: primaryColor,
textTheme: const TextTheme(caption: TextStyle(color: captionColor)),
),
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
const double selectedFontSize = 14.0;
const double unselectedFontSize = 12.0;
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.fontSize, selectedFontSize);
// Unselected label has a font size of 14 but is scaled down to be font size 12.
expect(tester.renderObject<RenderParagraph>(find.text('Alarm')).text.style.fontSize, selectedFontSize);
expect(
tester.firstWidget<Transform>(find.ancestor(of: find.text('Alarm'), matching: find.byType(Transform))).transform,
equals(Matrix4.diagonal3(Vector3.all(unselectedFontSize / selectedFontSize))),
);
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.color, equals(primaryColor));
expect(tester.renderObject<RenderParagraph>(find.text('Alarm')).text.style.color, equals(captionColor));
expect(_getOpacity(tester, 'Alarm'), equals(1.0));
expect(_getMaterial(tester).elevation, equals(8.0));
});
testWidgets('Shifting BottomNavigationBar defaults', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.shifting,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
const double selectedFontSize = 14.0;
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.fontSize, selectedFontSize);
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.color, equals(Colors.white));
expect(_getOpacity(tester, 'Alarm'), equals(0.0));
expect(_getMaterial(tester).elevation, equals(8.0));
});
testWidgets('Fixed BottomNavigationBar custom font size, color', (WidgetTester tester) async {
const Color primaryColor = Colors.black;
const Color captionColor = Colors.purple;
const Color selectedColor = Colors.blue;
const Color unselectedColor = Colors.yellow;
const double selectedFontSize = 18.0;
const double unselectedFontSize = 14.0;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
primaryColor: primaryColor,
textTheme: const TextTheme(caption: TextStyle(color: captionColor)),
),
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
selectedFontSize: selectedFontSize,
unselectedFontSize: unselectedFontSize,
selectedItemColor: selectedColor,
unselectedItemColor: unselectedColor,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.fontSize, selectedFontSize);
// Unselected label has a font size of 18 but is scaled down to be font size 14.
expect(tester.renderObject<RenderParagraph>(find.text('Alarm')).text.style.fontSize, selectedFontSize);
expect(
tester.firstWidget<Transform>(find.ancestor(of: find.text('Alarm'), matching: find.byType(Transform))).transform,
equals(Matrix4.diagonal3(Vector3.all(unselectedFontSize / selectedFontSize))),
);
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.color, equals(selectedColor));
expect(tester.renderObject<RenderParagraph>(find.text('Alarm')).text.style.color, equals(unselectedColor));
expect(_getOpacity(tester, 'Alarm'), equals(1.0));
});
testWidgets('Shifting BottomNavigationBar custom font size, color', (WidgetTester tester) async {
const Color primaryColor = Colors.black;
const Color captionColor = Colors.purple;
const Color selectedColor = Colors.blue;
const Color unselectedColor = Colors.yellow;
const double selectedFontSize = 18.0;
const double unselectedFontSize = 14.0;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
primaryColor: primaryColor,
textTheme: const TextTheme(caption: TextStyle(color: captionColor)),
),
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.shifting,
selectedFontSize: selectedFontSize,
unselectedFontSize: unselectedFontSize,
selectedItemColor: selectedColor,
unselectedItemColor: unselectedColor,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.fontSize, selectedFontSize);
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.color, equals(selectedColor));
expect(_getOpacity(tester, 'Alarm'), equals(0.0));
});
testWidgets('Fixed BottomNavigationBar can hide unselected labels', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
showUnselectedLabels: false,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
expect(_getOpacity(tester, 'AC'), equals(1.0));
expect(_getOpacity(tester, 'Alarm'), equals(0.0));
});
testWidgets('Fixed BottomNavigationBar can update background color', (WidgetTester tester) async {
const Color color = Colors.yellow;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: color,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
expect(_getMaterial(tester).color, equals(color));
});
testWidgets('Shifting BottomNavigationBar background color is overriden by item color', (WidgetTester tester) async {
const Color itemColor = Colors.yellow;
const Color backgroundColor = Colors.blue;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.shifting,
backgroundColor: backgroundColor,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
backgroundColor: itemColor,
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
expect(_getMaterial(tester).color, equals(itemColor));
});
testWidgets('Specifying both selectedItemColor and fixedColor asserts', (WidgetTester tester) async {
expect(
() {
return BottomNavigationBar(
selectedItemColor: Colors.black,
fixedColor: Colors.black,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
],
);
},
throwsAssertionError,
);
});
testWidgets('Fixed BottomNavigationBar uses fixedColor when selectedItemColor not provided', (WidgetTester tester) async {
const Color fixedColor = Colors.black;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
fixedColor: fixedColor,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
expect(tester.renderObject<RenderParagraph>(find.text('AC')).text.style.color, equals(fixedColor));
});
testWidgets('setting selectedFontSize to zero hides all labels', (WidgetTester tester) async {
const double customElevation = 3.0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
elevation: customElevation,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Alarm'),
),
]
)
)
)
);
expect(_getMaterial(tester).elevation, equals(customElevation));
});
testWidgets('BottomNavigationBar adds bottom padding to height', (WidgetTester tester) async { testWidgets('BottomNavigationBar adds bottom padding to height', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -91,7 +412,7 @@ void main() { ...@@ -91,7 +412,7 @@ void main() {
) )
); );
const double labelBottomMargin = 8.0; // _kBottomMargin in implementation. const double labelBottomMargin = 7.0; // 7 == defaulted selectedFontSize / 2.0.
const double additionalPadding = 40.0 - labelBottomMargin; const double additionalPadding = 40.0 - labelBottomMargin;
const double expectedHeight = kBottomNavigationBarHeight + additionalPadding; const double expectedHeight = kBottomNavigationBarHeight + additionalPadding;
expect(tester.getSize(find.byType(BottomNavigationBar)).height, expectedHeight); expect(tester.getSize(find.byType(BottomNavigationBar)).height, expectedHeight);
...@@ -393,7 +714,7 @@ void main() { ...@@ -393,7 +714,7 @@ void main() {
); );
final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar)); final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar));
expect(box.size.height, equals(68.0)); expect(box.size.height, equals(66.0));
}); });
testWidgets('BottomNavigationBar limits width of tiles with long titles', (WidgetTester tester) async { testWidgets('BottomNavigationBar limits width of tiles with long titles', (WidgetTester tester) async {
...@@ -827,7 +1148,7 @@ void main() { ...@@ -827,7 +1148,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 30)); await tester.pump(const Duration(milliseconds: 30));
await expectLater( await expectLater(
find.byType(BottomNavigationBar), find.byType(BottomNavigationBar),
matchesGoldenFile('bottom_navigation_bar.shifting_transition.$pump.png'), matchesGoldenFile('bottom_navigation_bar.shifting_transition.2.$pump.png'),
skip: !Platform.isLinux, skip: !Platform.isLinux,
); );
} }
...@@ -851,6 +1172,191 @@ void main() { ...@@ -851,6 +1172,191 @@ void main() {
]))); ])));
}, throwsA(isInstanceOf<AssertionError>())); }, throwsA(isInstanceOf<AssertionError>()));
}); });
testWidgets('BottomNavigationBar [showSelectedLabels]=false and [showUnselectedLabels]=false '
'for shifting navbar, expect that there is no rendered text', (WidgetTester tester) async {
final Widget widget = MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
type: BottomNavigationBarType.shifting,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
title: Text('Red'),
backgroundColor: Colors.red,
icon: Icon(Icons.dashboard),
),
BottomNavigationBarItem(
title: Text('Green'),
backgroundColor: Colors.green,
icon: Icon(Icons.menu),
),
],
),
);
},
),
);
await tester.pumpWidget(widget);
expect(find.text('Red'), findsOneWidget);
expect(find.text('Green'), findsOneWidget);
expect(tester.widget<Opacity>(find.byType(Opacity).first).opacity, 0.0);
expect(tester.widget<Opacity>(find.byType(Opacity).last).opacity, 0.0);
});
testWidgets('BottomNavigationBar [showSelectedLabels]=false and [showUnselectedLabels]=false '
'for fixed navbar, expect that there is no rendered text', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
title: Text('Red'),
backgroundColor: Colors.red,
icon: Icon(Icons.dashboard),
),
BottomNavigationBarItem(
title: Text('Green'),
backgroundColor: Colors.green,
icon: Icon(Icons.menu),
),
],
),
);
},
),
),
);
expect(find.text('Red'), findsOneWidget);
expect(find.text('Green'), findsOneWidget);
expect(tester.widget<Opacity>(find.byType(Opacity).first).opacity, 0.0);
expect(tester.widget<Opacity>(find.byType(Opacity).last).opacity, 0.0);
});
testWidgets('BottomNavigationBar.fixed [showSelectedLabels]=false and [showUnselectedLabels]=false semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerplate(
textDirection: TextDirection.ltr,
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('Red'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Green'),
),
],
),
),
);
final TestSemantics expected = TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isSelected,
SemanticsFlag.isHeader,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Red\nTab 1 of 2',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHeader,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Green\nTab 2 of 2',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
);
expect(semantics, hasSemantics(expected, ignoreId: true, ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('BottomNavigationBar.shifting [showSelectedLabels]=false and [showUnselectedLabels]=false semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
boilerplate(
textDirection: TextDirection.ltr,
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
type: BottomNavigationBarType.shifting,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('Red'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
title: Text('Green'),
),
],
),
),
);
final TestSemantics expected = TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isSelected,
SemanticsFlag.isHeader,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Red\nTab 1 of 2',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isHeader,
],
actions: <SemanticsAction>[SemanticsAction.tap],
label: 'Green\nTab 2 of 2',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
);
expect(semantics, hasSemantics(expected, ignoreId: true, ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
} }
Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) { Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) {
...@@ -874,3 +1380,19 @@ Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDir ...@@ -874,3 +1380,19 @@ Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDir
), ),
); );
} }
double _getOpacity(WidgetTester tester, String textValue) {
final FadeTransition opacityWidget = tester.widget<FadeTransition>(
find.ancestor(
of: find.text(textValue),
matching: find.byType(FadeTransition),
).first
);
return opacityWidget.opacity.value;
}
Material _getMaterial(WidgetTester tester) {
return tester.firstWidget<Material>(
find.descendant(of: find.byType(BottomNavigationBar), matching: find.byType(Material)),
);
}
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