Commit a3670a2f authored by Artur Rymarz's avatar Artur Rymarz Committed by xster

Add parameters to allow hiding icons and border of the Cupertino TabBar (#22804)

parent 490d369a
...@@ -27,3 +27,4 @@ Victor Choueiri <victor@ctrlanddev.com> ...@@ -27,3 +27,4 @@ Victor Choueiri <victor@ctrlanddev.com>
Christian Mürtz <teraarts@t-online.de> Christian Mürtz <teraarts@t-online.de>
Lukasz Piliszczuk <lukasz@intheloup.io> Lukasz Piliszczuk <lukasz@intheloup.io>
Felix Schmidt <felix.free@gmx.de> Felix Schmidt <felix.free@gmx.de>
Artur Rymarz <artur.rymarz@gmail.com>
...@@ -44,6 +44,13 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { ...@@ -44,6 +44,13 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
this.activeColor = CupertinoColors.activeBlue, this.activeColor = CupertinoColors.activeBlue,
this.inactiveColor = CupertinoColors.inactiveGray, this.inactiveColor = CupertinoColors.inactiveGray,
this.iconSize = 30.0, 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 != null),
assert(items.length >= 2), assert(items.length >= 2),
assert(currentIndex != null), assert(currentIndex != null),
...@@ -90,6 +97,11 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { ...@@ -90,6 +97,11 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
/// Must not be null. /// Must not be null.
final double iconSize; 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. /// True if the tab bar's background color has no transparency.
bool get opaque => backgroundColor.alpha == 0xFF; bool get opaque => backgroundColor.alpha == 0xFF;
...@@ -101,16 +113,9 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { ...@@ -101,16 +113,9 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
final double bottomPadding = MediaQuery.of(context).padding.bottom; final double bottomPadding = MediaQuery.of(context).padding.bottom;
Widget result = DecoratedBox( Widget result = DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
border: const Border( border: border,
top: BorderSide(
color: _kDefaultTabBarBorderColor,
width: 0.0, // One physical pixel.
style: BorderStyle.solid,
),
),
color: backgroundColor, color: backgroundColor,
), ),
// TODO(xster): allow icons-only versions of the tab bar too.
child: SizedBox( child: SizedBox(
height: _kTabBarHeight + bottomPadding, height: _kTabBarHeight + bottomPadding,
child: IconTheme.merge( // Default with the inactive state. child: IconTheme.merge( // Default with the inactive state.
...@@ -171,15 +176,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { ...@@ -171,15 +176,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
padding: const EdgeInsets.only(bottom: 4.0), padding: const EdgeInsets.only(bottom: 4.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: <Widget> [ children: _buildSingleTabItem(items[index], active),
Expanded(child:
Center(child: active
? items[index].activeIcon
: items[index].icon
),
),
items[index].title,
],
), ),
), ),
), ),
...@@ -193,6 +190,20 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { ...@@ -193,6 +190,20 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
return result; return result;
} }
List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
final List<Widget> components = <Widget>[
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. /// Change the active tab item's icon and title colors to active.
Widget _wrapActiveItem(Widget item, { @required bool active }) { Widget _wrapActiveItem(Widget item, { @required bool active }) {
if (!active) if (!active)
...@@ -216,18 +227,20 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { ...@@ -216,18 +227,20 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
Color activeColor, Color activeColor,
Color inactiveColor, Color inactiveColor,
Size iconSize, Size iconSize,
Border border,
int currentIndex, int currentIndex,
ValueChanged<int> onTap, ValueChanged<int> onTap,
}) { }) {
return CupertinoTabBar( return CupertinoTabBar(
key: key ?? this.key, key: key ?? this.key,
items: items ?? this.items, items: items ?? this.items,
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
activeColor: activeColor ?? this.activeColor, activeColor: activeColor ?? this.activeColor,
inactiveColor: inactiveColor ?? this.inactiveColor, inactiveColor: inactiveColor ?? this.inactiveColor,
iconSize: iconSize ?? this.iconSize, iconSize: iconSize ?? this.iconSize,
currentIndex: currentIndex ?? this.currentIndex, border: border ?? this.border,
onTap: onTap ?? this.onTap, currentIndex: currentIndex ?? this.currentIndex,
onTap: onTap ?? this.onTap,
); );
} }
} }
...@@ -77,7 +77,7 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -77,7 +77,7 @@ class BottomNavigationBar extends StatefulWidget {
/// Creates a bottom navigation bar, typically used in a [Scaffold] where it /// Creates a bottom navigation bar, typically used in a [Scaffold] where it
/// is provided as the [Scaffold.bottomNavigationBar] argument. /// 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 /// 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.
...@@ -95,12 +95,16 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -95,12 +95,16 @@ class BottomNavigationBar extends StatefulWidget {
this.iconSize = 24.0, this.iconSize = 24.0,
}) : assert(items != null), }) : assert(items != null),
assert(items.length >= 2), 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(0 <= currentIndex && currentIndex < items.length),
assert(iconSize != null), assert(iconSize != null),
type = type ?? (items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting), type = type ?? (items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting),
super(key: key); 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<BottomNavigationBarItem> items; final List<BottomNavigationBarItem> items;
/// The callback that is called when a item is tapped. /// The callback that is called when a item is tapped.
...@@ -149,8 +153,7 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -149,8 +153,7 @@ class _BottomNavigationTile extends StatelessWidget {
this.flex, this.flex,
this.selected = false, this.selected = false,
this.indexLabel, this.indexLabel,
} }) : assert(selected != null);
): assert(selected != null);
final BottomNavigationBarType type; final BottomNavigationBarType type;
final BottomNavigationBarItem item; final BottomNavigationBarItem item;
...@@ -335,7 +338,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -335,7 +338,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
return CurvedAnimation( return CurvedAnimation(
parent: _controllers[index], parent: _controllers[index],
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
reverseCurve: Curves.fastOutSlowIn.flipped reverseCurve: Curves.fastOutSlowIn.flipped,
); );
}); });
_controllers[widget.currentIndex].value = 1.0; _controllers[widget.currentIndex].value = 1.0;
...@@ -475,7 +478,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -475,7 +478,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
flex: _evaluateFlex(_animations[i]), flex: _evaluateFlex(_animations[i]),
selected: i == widget.currentIndex, selected: i == widget.currentIndex,
indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length), indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
) ),
); );
} }
break; break;
...@@ -567,7 +570,7 @@ class _Circle { ...@@ -567,7 +570,7 @@ class _Circle {
); );
animation = CurvedAnimation( animation = CurvedAnimation(
parent: controller, parent: controller,
curve: Curves.fastOutSlowIn curve: Curves.fastOutSlowIn,
); );
controller.forward(); controller.forward();
} }
......
...@@ -21,15 +21,14 @@ import 'framework.dart'; ...@@ -21,15 +21,14 @@ import 'framework.dart';
class BottomNavigationBarItem { class BottomNavigationBarItem {
/// Creates an item that is used with [BottomNavigationBar.items]. /// 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({ const BottomNavigationBarItem({
@required this.icon, @required this.icon,
@required this.title, this.title,
Widget activeIcon, Widget activeIcon,
this.backgroundColor, this.backgroundColor,
}) : activeIcon = activeIcon ?? icon, }) : activeIcon = activeIcon ?? icon,
assert(icon != null), assert(icon != null);
assert(title != null);
/// The icon of the item. /// The icon of the item.
/// ///
...@@ -61,7 +60,7 @@ class BottomNavigationBarItem { ...@@ -61,7 +60,7 @@ class BottomNavigationBarItem {
/// * [BottomNavigationBarItem.icon], for a description of how to pair icons. /// * [BottomNavigationBarItem.icon], for a description of how to pair icons.
final Widget activeIcon; 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; final Widget title;
/// The color of the background radial animation for material [BottomNavigationBar]. /// The color of the background radial animation for material [BottomNavigationBar].
......
...@@ -238,4 +238,98 @@ void main() { ...@@ -238,4 +238,98 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
}
testWidgets('Title of items should be nullable', (WidgetTester tester) async {
const TestImageProvider iconProvider = TestImageProvider(16, 16);
final List<int> itemsTapped = <int>[];
await pumpWidgetWithBoilerplate(
tester,
MediaQuery(
data: const MediaQueryData(),
child: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
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, <int>[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>[
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>[
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
...@@ -781,6 +781,25 @@ void main() { ...@@ -781,6 +781,25 @@ void main() {
expect(_backgroundColor, Colors.green); expect(_backgroundColor, Colors.green);
expect(tester.widget<Material>(backgroundMaterial).color, Colors.green); expect(tester.widget<Material>(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>[
BottomNavigationBarItem(
icon: Icon(Icons.ac_unit),
title: Text('AC'),
),
BottomNavigationBarItem(
icon: Icon(Icons.access_alarm),
)
])));
}, throwsA(isInstanceOf<AssertionError>()));
});
} }
Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) { Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) {
......
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