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>
.toList(),
currentIndex: _currentIndex,
type: _type,
//iconSize: 4.0,
onTap: (int index) {
setState(() {
_navigationViews[_currentIndex].controller.reverse();
......
......@@ -17,11 +17,6 @@ import 'material_localizations.dart';
import 'text_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].
///
/// See also:
......@@ -30,18 +25,16 @@ const double _kBottomMargin = 8.0;
/// * [BottomNavigationBarItem]
/// * <https://material.io/design/components/bottom-navigation.html#specs>
enum BottomNavigationBarType {
/// The [BottomNavigationBar]'s [BottomNavigationBarItem]s have fixed width, always
/// display their text labels, and do not shift when tapped.
/// The [BottomNavigationBar]'s [BottomNavigationBarItem]s have fixed width.
fixed,
/// The location and size of the [BottomNavigationBar] [BottomNavigationBarItem]s
/// animate and labels fade in when they are tapped. Only the selected item
/// displays its text label.
/// animate and labels fade in when they are tapped.
shifting,
}
/// A material widget displayed at the bottom of an app for selecting among a
/// small number of views, typically between three and five.
/// A material widget that's displayed at the bottom of an app for selecting
/// among a small number of views, typically between three and five.
///
/// 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
......@@ -52,18 +45,19 @@ enum BottomNavigationBarType {
/// where it is provided as the [Scaffold.bottomNavigationBar] argument.
///
/// The bottom navigation bar's [type] changes how its [items] are displayed.
/// If not specified it's automatically set to [BottomNavigationBarType.fixed]
/// when there are less than four items, [BottomNavigationBarType.shifting]
/// otherwise.
/// If not specified, then it's automatically set to
/// [BottomNavigationBarType.fixed] when there are less than four items, and
/// [BottomNavigationBarType.shifting] otherwise.
///
/// * [BottomNavigationBarType.fixed], the default when there are less than
/// four [items]. The selected item is rendered with [fixedColor] if it's
/// non-null, otherwise the theme's [ThemeData.primaryColor] is used. The
/// navigation bar's background color is the default [Material] background
/// four [items]. The selected item is rendered with the
/// [selectedItemColor] if it's non-null, otherwise the theme's
/// [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).
/// * [BottomNavigationBarType.shifting], the default when there are four
/// or more [items]. All items are rendered in white and the navigation bar's
/// background color is the same as the
/// or more [items]. If [selectedItemColor] is null, all items are rendered
/// in white. The navigation bar's background color is the same as the
/// [BottomNavigationBarItem.backgroundColor] of the selected item. In this
/// case it's assumed that each item will have a different background color
/// and that background color will contrast well with white.
......@@ -71,10 +65,9 @@ enum BottomNavigationBarType {
/// {@tool snippet --template=stateful_widget_material}
/// This example shows a [BottomNavigationBar] as it is used within a [Scaffold]
/// widget. The [BottomNavigationBar] has three [BottomNavigationBarItem]
/// widgets and the [currentIndex] is set to index 1. The color of the selected
/// item is set to a purple color. A function is called whenever any item is
/// tapped and the function helps display the appropriate [Text] in the body of
/// the [Scaffold].
/// widgets and the [currentIndex] is set to index 1. The selected item is
/// purple. The `_onItemTapped` function changes the selected item's index
/// and displays a corresponding message in the center of the [Scaffold].
///
/// ```dart
/// int _selectedIndex = 1;
......@@ -106,7 +99,7 @@ enum BottomNavigationBarType {
/// BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
/// ],
/// currentIndex: _selectedIndex,
/// fixedColor: Colors.deepPurple,
/// selectedItemColor: Colors.deepPurple,
/// onTap: _onItemTapped,
/// ),
/// );
......@@ -120,25 +113,43 @@ enum BottomNavigationBarType {
/// * [Scaffold]
/// * <https://material.io/design/components/bottom-navigation.html>
class BottomNavigationBar extends StatefulWidget {
/// Creates a bottom navigation bar, typically used in a [Scaffold] where it
/// is provided as the [Scaffold.bottomNavigationBar] argument.
/// Creates a bottom navigation bar which is typically used as a
/// [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
/// are two or three [items], [BottomNavigationBarType.shifting] otherwise.
///
/// If [fixedColor] is null then the theme's primary color,
/// [ThemeData.primaryColor], is used. However if [BottomNavigationBar.type] is
/// [BottomNavigationBarType.shifting] then [fixedColor] is ignored.
/// The [iconSize], [selectedFontSize], [unselectedFontSize], and [elevation]
/// arguments must be non-null and non-negative.
///
/// 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({
Key key,
@required this.items,
this.onTap,
this.currentIndex = 0,
this.elevation = 8.0,
BottomNavigationBarType type,
this.fixedColor,
Color fixedColor,
this.backgroundColor,
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.length >= 2),
assert(
......@@ -146,42 +157,125 @@ class BottomNavigationBar extends StatefulWidget {
'Every item must have a non-null title',
),
assert(0 <= currentIndex && currentIndex < items.length),
assert(iconSize != null),
type = type ?? (items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting),
assert(elevation != null && elevation >= 0.0),
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);
/// 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;
/// 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
/// current index and call `setState` to rebuild it with the newly provided
/// index.
/// The stateful widget that creates the bottom navigation bar needs to keep
/// track of the index of the selected [BottomNavigationBarItem] and call
/// `setState` to rebuild the bottom navigation bar with the new [currentIndex].
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;
/// 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].
///
/// See documentation for [BottomNavigationBarType] for information on the meaning
/// of different types.
/// See documentation for [BottomNavigationBarType] for information on the
/// meaning of different types.
final BottomNavigationBarType type;
/// The color of the selected item when bottom navigation bar is
/// [BottomNavigationBarType.fixed].
/// The value of [selectedItemColor].
///
/// 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,
/// [ThemeData.primaryColor], is used. However if [BottomNavigationBar.type] is
/// [BottomNavigationBarType.shifting] then [fixedColor] is ignored.
final Color fixedColor;
/// If [type] is [BottomNavigationBarType.shifting] and the
/// [items]s, have [BottomNavigationBarItem.backgroundColor] set, the [item]'s
/// backgroundColor will splash and overwrite this color.
final Color backgroundColor;
/// The size of all of the [BottomNavigationBarItem] icons.
///
/// See [BottomNavigationBarItem.icon] for more information.
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
_BottomNavigationBarState createState() => _BottomNavigationBarState();
}
......@@ -198,8 +292,17 @@ class _BottomNavigationTile extends StatelessWidget {
this.colorTween,
this.flex,
this.selected = false,
@required this.selectedFontSize,
@required this.unselectedFontSize,
this.showSelectedLabels,
this.showUnselectedLabels,
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 BottomNavigationBarItem item;
......@@ -209,7 +312,11 @@ class _BottomNavigationTile extends StatelessWidget {
final ColorTween colorTween;
final double flex;
final bool selected;
final double selectedFontSize;
final double unselectedFontSize;
final String indexLabel;
final bool showSelectedLabels;
final bool showUnselectedLabels;
@override
Widget build(BuildContext context) {
......@@ -218,16 +325,50 @@ class _BottomNavigationTile extends StatelessWidget {
// produce smooth animation. We do this by multiplying the flex value
// (which is an integer) by a large number.
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) {
case BottomNavigationBarType.fixed:
size = 1;
label = _FixedLabel(colorTween: colorTween, animation: animation, item: item);
break;
case BottomNavigationBarType.shifting:
size = (flex * 1000.0).round();
label = _ShiftingLabel(animation: animation, item: item);
break;
}
......@@ -241,21 +382,31 @@ class _BottomNavigationTile extends StatelessWidget {
children: <Widget>[
InkResponse(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_TileIcon(
type: type,
colorTween: colorTween,
animation: animation,
iconSize: iconSize,
selected: selected,
item: item,
),
label,
],
child: Padding(
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_TileIcon(
colorTween: colorTween,
animation: animation,
iconSize: iconSize,
selected: selected,
item: item,
),
_Label(
colorTween: colorTween,
animation: animation,
item: item,
selectedFontSize: selectedFontSize,
unselectedFontSize: unselectedFontSize,
showSelectedLabels: showSelectedLabels,
showUnselectedLabels: showUnselectedLabels,
),
],
),
),
),
Semantics(
......@@ -272,15 +423,15 @@ class _BottomNavigationTile extends StatelessWidget {
class _TileIcon extends StatelessWidget {
const _TileIcon({
Key key,
@required this.type,
@required this.colorTween,
@required this.animation,
@required this.iconSize,
@required this.selected,
@required this.item,
}) : super(key: key);
}) : assert(selected != null),
assert(item != null),
super(key: key);
final BottomNavigationBarType type;
final ColorTween colorTween;
final Animation<double> animation;
final double iconSize;
......@@ -289,28 +440,11 @@ class _TileIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
double tweenStart;
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;
}
final Color iconColor = colorTween.evaluate(animation);
return Align(
alignment: Alignment.topCenter,
heightFactor: 1.0,
child: Container(
margin: EdgeInsets.only(
top: Tween<double>(
begin: tweenStart,
end: _kTopMargin,
).evaluate(animation),
),
child: IconTheme(
data: IconThemeData(
color: iconColor,
......@@ -323,89 +457,84 @@ class _TileIcon extends StatelessWidget {
}
}
class _FixedLabel extends StatelessWidget {
const _FixedLabel({
class _Label extends StatelessWidget {
const _Label({
Key key,
@required this.colorTween,
@required this.animation,
@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 Animation<double> animation;
final BottomNavigationBarItem item;
final double selectedFontSize;
final double unselectedFontSize;
final bool showSelectedLabels;
final bool showUnselectedLabels;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
heightFactor: 1.0,
child: Container(
margin: const EdgeInsets.only(bottom: _kBottomMargin),
child: DefaultTextStyle.merge(
style: TextStyle(
fontSize: _kActiveFontSize,
color: colorTween.evaluate(animation),
),
// The font size should grow here when active, but because of the way
// font rendering works, it doesn't grow smoothly if we just animate
// the font size, so we use a transform instead.
child: Transform(
transform: Matrix4.diagonal3(
Vector3.all(
Tween<double>(
begin: _kInactiveFontSize / _kActiveFontSize,
end: 1.0,
).evaluate(animation),
),
),
alignment: Alignment.bottomCenter,
child: item.title,
Widget text = DefaultTextStyle.merge(
style: TextStyle(
fontSize: selectedFontSize,
color: colorTween.evaluate(animation),
),
// The font size should grow here when active, but because of the way
// font rendering works, it doesn't grow smoothly if we just animate
// the font size, so we use a transform instead.
child: Transform(
transform: Matrix4.diagonal3(
Vector3.all(
Tween<double>(
begin: unselectedFontSize / selectedFontSize,
end: 1.0,
).evaluate(animation),
),
),
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;
final BottomNavigationBarItem item;
if (!showUnselectedLabels && !showSelectedLabels) {
// 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(
alignment: Alignment.bottomCenter,
heightFactor: 1.0,
child: Container(
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,
),
),
),
child: Container(child: text),
);
}
}
......@@ -529,63 +658,57 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
List<Widget> _createTiles() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
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) {
case BottomNavigationBarType.fixed:
final ThemeData themeData = Theme.of(context);
final TextTheme textTheme = themeData.textTheme;
Color 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,
colorTween = ColorTween(
begin: widget.unselectedItemColor ?? themeData.textTheme.caption.color,
end: widget.selectedItemColor ?? 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;
case BottomNavigationBarType.shifting:
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);
},
flex: _evaluateFlex(_animations[i]),
selected: i == widget.currentIndex,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
),
);
}
colorTween = ColorTween(
begin: widget.unselectedItemColor ?? Colors.white,
end: widget.selectedItemColor ?? Colors.white,
);
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) {
......@@ -602,12 +725,14 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
assert(debugCheckHasMaterialLocalizations(context));
assert(debugCheckHasMediaQuery(context));
// 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;
switch (widget.type) {
case BottomNavigationBarType.fixed:
backgroundColor = widget.backgroundColor;
break;
case BottomNavigationBarType.shifting:
backgroundColor = _backgroundColor;
......@@ -616,7 +741,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
return Semantics(
explicitChildNodes: true,
child: Material(
elevation: 8.0,
elevation: widget.elevation,
color: backgroundColor,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: kBottomNavigationBarHeight + additionalBottomPadding),
......
......@@ -9,8 +9,8 @@ import 'framework.dart';
/// An interactive button within either material's [BottomNavigationBar]
/// or the iOS themed [CupertinoTabBar] with an icon and title.
///
/// This class is rarely used in isolation. Commonly embedded in one of the
/// bottom navigation widgets above.
/// This class is rarely used in isolation. It is typically embedded in one of
/// the bottom navigation widgets above.
///
/// See also:
///
......@@ -67,7 +67,7 @@ class BottomNavigationBarItem {
///
/// If the navigation bar's type is [BottomNavigationBarType.shifting], then
/// 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
/// via [CupertinoTabBar.backgroundColor].
......
......@@ -6,7 +6,9 @@ import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
......@@ -68,6 +70,325 @@ void main() {
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 {
await tester.pumpWidget(
MaterialApp(
......@@ -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 expectedHeight = kBottomNavigationBarHeight + additionalPadding;
expect(tester.getSize(find.byType(BottomNavigationBar)).height, expectedHeight);
......@@ -393,7 +714,7 @@ void main() {
);
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 {
......@@ -827,7 +1148,7 @@ void main() {
await tester.pump(const Duration(milliseconds: 30));
await expectLater(
find.byType(BottomNavigationBar),
matchesGoldenFile('bottom_navigation_bar.shifting_transition.$pump.png'),
matchesGoldenFile('bottom_navigation_bar.shifting_transition.2.$pump.png'),
skip: !Platform.isLinux,
);
}
......@@ -851,6 +1172,191 @@ void main() {
])));
}, 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 }) {
......@@ -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