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>
|
||||
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);
|
||||
});
|
||||
}
|
@ -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 }) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user