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>
Christian Mürtz <teraarts@t-online.de>
Lukasz Piliszczuk <lukasz@intheloup.io>
Felix Schmidt <felix.free@gmx.de>
Artur Rymarz <artur.rymarz@gmail.com>
......@@ -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: <Widget> [
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<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.
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<int> 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,
);
}
}
......@@ -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<BottomNavigationBarItem> 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<BottomNavigationBar> 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<BottomNavigationBar> 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();
}
......
......@@ -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].
......
......@@ -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<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() {
expect(_backgroundColor, 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 }) {
......
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