Add parameters to allow hiding icons and border of the Cupertino TabBar (#22804)
This commit is contained in:
parent
490d369a7f
commit
a3670a2f85
1
AUTHORS
1
AUTHORS
@ -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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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
|
|||||||
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
|
|||||||
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 {
|
|||||||
);
|
);
|
||||||
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';
|
|||||||
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 {
|
|||||||
/// * [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() {
|
|||||||
|
|
||||||
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);
|
||||||
|
});
|
||||||
}
|
}
|
@ -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 }) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user