diff --git a/AUTHORS b/AUTHORS index d598d66524..35932ff7d2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,3 +27,4 @@ Victor Choueiri Christian Mürtz Lukasz Piliszczuk Felix Schmidt +Artur Rymarz diff --git a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart index 5cab1d78a8..f7d02abfc7 100644 --- a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart +++ b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart @@ -44,6 +44,13 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { this.activeColor = CupertinoColors.activeBlue, this.inactiveColor = CupertinoColors.inactiveGray, this.iconSize = 30.0, + this.border = const Border( + top: BorderSide( + color: _kDefaultTabBarBorderColor, + width: 0.0, // One physical pixel. + style: BorderStyle.solid, + ), + ), }) : assert(items != null), assert(items.length >= 2), assert(currentIndex != null), @@ -90,6 +97,11 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { /// Must not be null. final double iconSize; + /// The border of the [CupertinoTabBar]. + /// + /// The default value is a one physical pixel top border with grey color. + final Border border; + /// True if the tab bar's background color has no transparency. bool get opaque => backgroundColor.alpha == 0xFF; @@ -101,16 +113,9 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { final double bottomPadding = MediaQuery.of(context).padding.bottom; Widget result = DecoratedBox( decoration: BoxDecoration( - border: const Border( - top: BorderSide( - color: _kDefaultTabBarBorderColor, - width: 0.0, // One physical pixel. - style: BorderStyle.solid, - ), - ), + border: border, color: backgroundColor, ), - // TODO(xster): allow icons-only versions of the tab bar too. child: SizedBox( height: _kTabBarHeight + bottomPadding, child: IconTheme.merge( // Default with the inactive state. @@ -171,15 +176,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { padding: const EdgeInsets.only(bottom: 4.0), child: Column( mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded(child: - Center(child: active - ? items[index].activeIcon - : items[index].icon - ), - ), - items[index].title, - ], + children: _buildSingleTabItem(items[index], active), ), ), ), @@ -193,6 +190,20 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { return result; } + List _buildSingleTabItem(BottomNavigationBarItem item, bool active) { + final List components = [ + Expanded( + child: Center(child: active ? item.activeIcon : item.icon), + ) + ]; + + if (item.title != null) { + components.add(item.title); + } + + return components; + } + /// Change the active tab item's icon and title colors to active. Widget _wrapActiveItem(Widget item, { @required bool active }) { if (!active) @@ -216,18 +227,20 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { Color activeColor, Color inactiveColor, Size iconSize, + Border border, int currentIndex, ValueChanged onTap, }) { return CupertinoTabBar( - key: key ?? this.key, - items: items ?? this.items, - backgroundColor: backgroundColor ?? this.backgroundColor, - activeColor: activeColor ?? this.activeColor, - inactiveColor: inactiveColor ?? this.inactiveColor, - iconSize: iconSize ?? this.iconSize, - currentIndex: currentIndex ?? this.currentIndex, - onTap: onTap ?? this.onTap, + key: key ?? this.key, + items: items ?? this.items, + backgroundColor: backgroundColor ?? this.backgroundColor, + activeColor: activeColor ?? this.activeColor, + inactiveColor: inactiveColor ?? this.inactiveColor, + iconSize: iconSize ?? this.iconSize, + border: border ?? this.border, + currentIndex: currentIndex ?? this.currentIndex, + onTap: onTap ?? this.onTap, ); } } diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart index 9024b5328f..15124943ae 100644 --- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart +++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart @@ -77,7 +77,7 @@ class BottomNavigationBar extends StatefulWidget { /// Creates a bottom navigation bar, typically used in a [Scaffold] where it /// is provided as the [Scaffold.bottomNavigationBar] argument. /// - /// The length of [items] must be at least two. + /// The length of [items] must be at least two and each item's icon and title must be not null. /// /// If [type] is null then [BottomNavigationBarType.fixed] is used when there /// are two or three [items], [BottomNavigationBarType.shifting] otherwise. @@ -95,12 +95,16 @@ class BottomNavigationBar extends StatefulWidget { this.iconSize = 24.0, }) : assert(items != null), assert(items.length >= 2), + assert( + items.every((BottomNavigationBarItem item) => item.title != null) == true, + '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), super(key: key); - /// The interactive items laid out within the bottom navigation bar. + /// The interactive items laid out within the bottom navigation bar where each item has an icon and title. final List items; /// The callback that is called when a item is tapped. @@ -149,8 +153,7 @@ class _BottomNavigationTile extends StatelessWidget { this.flex, this.selected = false, this.indexLabel, - } - ): assert(selected != null); + }) : assert(selected != null); final BottomNavigationBarType type; final BottomNavigationBarItem item; @@ -335,7 +338,7 @@ class _BottomNavigationBarState extends State with TickerPr return CurvedAnimation( parent: _controllers[index], curve: Curves.fastOutSlowIn, - reverseCurve: Curves.fastOutSlowIn.flipped + reverseCurve: Curves.fastOutSlowIn.flipped, ); }); _controllers[widget.currentIndex].value = 1.0; @@ -475,7 +478,7 @@ class _BottomNavigationBarState extends State with TickerPr flex: _evaluateFlex(_animations[i]), selected: i == widget.currentIndex, indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length), - ) + ), ); } break; @@ -567,7 +570,7 @@ class _Circle { ); animation = CurvedAnimation( parent: controller, - curve: Curves.fastOutSlowIn + curve: Curves.fastOutSlowIn, ); controller.forward(); } diff --git a/packages/flutter/lib/src/widgets/bottom_navigation_bar_item.dart b/packages/flutter/lib/src/widgets/bottom_navigation_bar_item.dart index c7d743681b..4ee16b1206 100644 --- a/packages/flutter/lib/src/widgets/bottom_navigation_bar_item.dart +++ b/packages/flutter/lib/src/widgets/bottom_navigation_bar_item.dart @@ -21,15 +21,14 @@ import 'framework.dart'; class BottomNavigationBarItem { /// Creates an item that is used with [BottomNavigationBar.items]. /// - /// The arguments [icon] and [title] should not be null. + /// The argument [icon] should not be null and the argument [title] should not be null when used in a Material Design's [BottomNavigationBar]. const BottomNavigationBarItem({ @required this.icon, - @required this.title, + this.title, Widget activeIcon, this.backgroundColor, }) : activeIcon = activeIcon ?? icon, - assert(icon != null), - assert(title != null); + assert(icon != null); /// The icon of the item. /// @@ -61,7 +60,7 @@ class BottomNavigationBarItem { /// * [BottomNavigationBarItem.icon], for a description of how to pair icons. final Widget activeIcon; - /// The title of the item. + /// The title of the item. If the title is not provided only the icon will be shown when not used in a Material Design [BottomNavigationBar]. final Widget title; /// The color of the background radial animation for material [BottomNavigationBar]. diff --git a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart index 6bdfcb6062..54dc0b9166 100644 --- a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart +++ b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart @@ -238,4 +238,98 @@ void main() { semantics.dispose(); }); -} + + testWidgets('Title of items should be nullable', (WidgetTester tester) async { + const TestImageProvider iconProvider = TestImageProvider(16, 16); + final List itemsTapped = []; + + await pumpWidgetWithBoilerplate( + tester, + MediaQuery( + data: const MediaQueryData(), + child: CupertinoTabBar( + items: const [ + BottomNavigationBarItem( + icon: ImageIcon( + TestImageProvider(24, 24), + ), + title: Text('Tab 1'), + ), + BottomNavigationBarItem( + icon: ImageIcon( + iconProvider, + ), + ), + ], + onTap: (int index) => itemsTapped.add(index), + ), + )); + + expect(find.text('Tab 1'), findsOneWidget); + + final Finder finder = find.byWidgetPredicate( + (Widget widget) => widget is Image && widget.image == iconProvider); + + await tester.tap(finder); + expect(itemsTapped, [1]); + }); + + testWidgets('Hide border hides the top border of the tabBar', + (WidgetTester tester) async { + await pumpWidgetWithBoilerplate( + tester, + MediaQuery( + data: const MediaQueryData(), + child: CupertinoTabBar( + items: const [ + BottomNavigationBarItem( + icon: ImageIcon( + TestImageProvider(24, 24), + ), + title: Text('Tab 1'), + ), + BottomNavigationBarItem( + icon: ImageIcon( + TestImageProvider(24, 24), + ), + title: Text('Tab 2'), + ), + ], + ), + )); + + final DecoratedBox decoratedBox = tester.widget(find.byType(DecoratedBox)); + final BoxDecoration boxDecoration = decoratedBox.decoration; + expect(boxDecoration.border, isNotNull); + + await pumpWidgetWithBoilerplate( + tester, + MediaQuery( + data: const MediaQueryData(), + child: CupertinoTabBar( + items: const [ + BottomNavigationBarItem( + icon: ImageIcon( + TestImageProvider(24, 24), + ), + title: Text('Tab 1'), + ), + BottomNavigationBarItem( + icon: ImageIcon( + TestImageProvider(24, 24), + ), + title: Text('Tab 2'), + ), + ], + backgroundColor: const Color(0xFFFFFFFF), // Opaque white. + border: null, + ), + )); + + final DecoratedBox decoratedBoxHiddenBorder = + tester.widget(find.byType(DecoratedBox)); + final BoxDecoration boxDecorationHiddenBorder = + decoratedBoxHiddenBorder.decoration; + expect(boxDecorationHiddenBorder.border, isNull); + }); +} \ No newline at end of file diff --git a/packages/flutter/test/material/bottom_navigation_bar_test.dart b/packages/flutter/test/material/bottom_navigation_bar_test.dart index 9acc86319d..d499dfc80d 100644 --- a/packages/flutter/test/material/bottom_navigation_bar_test.dart +++ b/packages/flutter/test/material/bottom_navigation_bar_test.dart @@ -781,6 +781,25 @@ void main() { expect(_backgroundColor, Colors.green); expect(tester.widget(backgroundMaterial).color, Colors.green); }); + + testWidgets('BottomNavigationBar item title should not be nullable', + (WidgetTester tester) async { + expect(() { + MaterialApp( + home: Scaffold( + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.shifting, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.ac_unit), + title: Text('AC'), + ), + BottomNavigationBarItem( + icon: Icon(Icons.access_alarm), + ) + ]))); + }, throwsA(isInstanceOf())); + }); } Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) {