Commit 123e9e01 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Allow TabBars, TabBarViews, TabControllers, with zero or one tabs (#10608)

parent a8487722
......@@ -16,6 +16,8 @@ import 'constants.dart';
/// A stateful widget that builds a [TabBar] or a [TabBarView] can create
/// a TabController and share it directly.
///
/// ## Sample code
///
/// ```dart
/// class _MyDemoState extends State<MyDemo> with SingleTickerProviderStateMixin {
/// final List<Tab> myTabs = <Tab>[
......@@ -62,12 +64,18 @@ import 'constants.dart';
/// inherited widget.
class TabController extends ChangeNotifier {
/// Creates an object that manages the state required by [TabBar] and a [TabBarView].
///
/// The [length] cannot be null or negative. Typically its a value greater than one, i.e.
/// typically there are two or more tabs.
///
/// The `initialIndex` must be valid given [length] and cannot be null. If [length] is
/// zero, then `initialIndex` must be 0 (the default).
TabController({ int initialIndex: 0, @required this.length, @required TickerProvider vsync })
: assert(length != null && length > 1),
assert(initialIndex != null && initialIndex >= 0 && initialIndex < length),
: assert(length != null && length >= 0),
assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
_index = initialIndex,
_previousIndex = initialIndex,
_animationController = new AnimationController(
_animationController = length < 2 ? null : new AnimationController(
value: initialIndex.toDouble(),
upperBound: (length - 1).toDouble(),
vsync: vsync
......@@ -81,18 +89,21 @@ class TabController extends ChangeNotifier {
/// selected tab is changed, the animation's value equals [index]. The
/// animation's value can be [offset] by +/- 1.0 to reflect [TabBarView]
/// drag scrolling.
Animation<double> get animation => _animationController.view;
///
/// If length is zero or one, [index] animations don't happen and the value
/// of this property is [kAlwaysCompleteAnimation].
Animation<double> get animation => _animationController?.view ?? kAlwaysCompleteAnimation;
final AnimationController _animationController;
/// The total number of tabs. Must be greater than one.
/// The total number of tabs. Typically greater than one.
final int length;
void _changeIndex(int value, { Duration duration, Curve curve }) {
assert(value != null);
assert(value >= 0 && value < length);
assert(value >= 0 && (value < length || length == 0));
assert(duration == null ? curve == null : true);
assert(_indexIsChangingCount >= 0);
if (value == _index)
if (value == _index || length < 2)
return;
_previousIndex = index;
_index = value;
......@@ -118,6 +129,9 @@ class TabController extends ChangeNotifier {
/// [indexIsChanging] to false, and notifies listeners.
///
/// To change the currently selected tab and play the [animation] use [animateTo].
///
/// The value of [index] must be valid given [length]. If [length] is zero,
/// then [index] will also be zero.
int get index => _index;
int _index;
set index(int value) {
......@@ -148,8 +162,9 @@ class TabController extends ChangeNotifier {
/// drags left or right. A value between -1.0 and 0.0 implies that the
/// TabBarView has been dragged to the left. Similarly a value between
/// 0.0 and 1.0 implies that the TabBarView has been dragged to the right.
double get offset => _animationController.value - _index.toDouble();
double get offset => length > 1 ? _animationController.value - _index.toDouble() : 0.0;
set offset(double value) {
assert(length > 1);
assert(value != null);
assert(value >= -1.0 && value <= 1.0);
assert(!indexIsChanging);
......@@ -160,7 +175,7 @@ class TabController extends ChangeNotifier {
@override
void dispose() {
_animationController.dispose();
_animationController?.dispose();
super.dispose();
}
}
......@@ -220,7 +235,7 @@ class _TabControllerScope extends InheritedWidget {
class DefaultTabController extends StatefulWidget {
/// Creates a default tab controller for the given [child] widget.
///
/// The [length] argument must be great than one.
/// The [length] argument is typically greater than one.
///
/// The [initialIndex] argument must not be null.
const DefaultTabController({
......@@ -231,7 +246,7 @@ class DefaultTabController extends StatefulWidget {
}) : assert(initialIndex != null),
super(key: key);
/// The total number of tabs. Must be greater than one.
/// The total number of tabs. Typically greater than one.
final int length;
/// The initial index of the selected tab.
......
......@@ -389,22 +389,23 @@ class _TabBarScrollController extends ScrollController {
/// A material design widget that displays a horizontal row of tabs.
///
/// Typically created as part of an [AppBar] and in conjuction with a
/// [TabBarView].
/// Typically created as the [AppBar.bottom] part of an [AppBar] and in
/// conjuction with a [TabBarView].
///
/// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor.
/// [DefaultTabController] ancestor. The tab controller's [TabController.length]
/// must equal the length of the [tabs] list.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
/// * [TabBarView], which displays the contents that the tab bar is selecting
/// between.
/// * [TabBarView], which displays page views that correspond to each tab.
class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a material design tab bar.
///
/// The [tabs] argument must not be null and must have more than one widget.
/// The [tabs] argument cannot be null and its length must match the [controller]'s
/// [TabController.length].
///
/// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor.
......@@ -424,13 +425,15 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.labelStyle,
this.unselectedLabelColor,
this.unselectedLabelStyle,
}) : assert(tabs != null && tabs.length > 1),
}) : assert(tabs != null),
assert(isScrollable != null),
assert(indicatorWeight != null && indicatorWeight > 0.0),
assert(indicatorPadding != null),
super(key: key);
/// Typically a list of [Tab] widgets.
/// Typically a list of two or more [Tab] widgets.
///
/// The length of this list must match the [controller]'s [TabController.length].
final List<Widget> tabs;
/// This widget's selection and animation state.
......@@ -667,6 +670,12 @@ class _TabBarState extends State<TabBar> {
@override
Widget build(BuildContext context) {
if (_controller.length == 0) {
return new Container(
height: _kTabHeight + widget.indicatorWeight,
);
}
final List<Widget> wrappedTabs = new List<Widget>.from(widget.tabs, growable: false);
// If the controller was provided by DefaultTabController and we're part
......@@ -774,8 +783,7 @@ class TabBarView extends StatefulWidget {
Key key,
@required this.children,
this.controller,
}) : assert(children != null && children.length > 1),
super(key: key);
}) : assert(children != null), super(key: key);
/// This widget's selection and animation state.
///
......
......@@ -900,4 +900,99 @@ void main() {
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight)
));
});
testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async {
final TabController controller = new TabController(
vsync: const TestVSync(),
length: 0,
);
await tester.pumpWidget(
new Material(
child: new Column(
children: <Widget>[
new TabBar(
controller: controller,
tabs: const <Widget>[],
),
new Flexible(
child: new TabBarView(
controller: controller,
children: const <Widget>[],
),
),
],
),
),
);
expect(controller.index, 0);
expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0));
expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0));
// A fling in the TabBar or TabBarView, shouldn't do anything.
await(tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0));
await(tester.pumpAndSettle());
await(tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0));
await(tester.pumpAndSettle());
expect(controller.index, 0);
});
testWidgets('TabBar etc with one tab', (WidgetTester tester) async {
final TabController controller = new TabController(
vsync: const TestVSync(),
length: 1,
);
await tester.pumpWidget(
new Material(
child: new Column(
children: <Widget>[
new TabBar(
controller: controller,
tabs: const <Widget>[const Tab(text: 'TAB')],
),
new Flexible(
child: new TabBarView(
controller: controller,
children: const <Widget>[const Text('PAGE')],
),
),
],
),
),
);
expect(controller.index, 0);
expect(find.text('TAB'), findsOneWidget);
expect(find.text('PAGE'), findsOneWidget);
expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0));
expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0));
// The one tab spans the app's width
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);
// A fling in the TabBar or TabBarView, shouldn't move the tab.
await(tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0));
await(tester.pump(const Duration(milliseconds: 50)));
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);
await(tester.pumpAndSettle());
await(tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0));
await(tester.pump(const Duration(milliseconds: 50)));
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);
await(tester.pumpAndSettle());
expect(controller.index, 0);
expect(find.text('TAB'), findsOneWidget);
expect(find.text('PAGE'), findsOneWidget);
});
}
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