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'; ...@@ -16,6 +16,8 @@ import 'constants.dart';
/// A stateful widget that builds a [TabBar] or a [TabBarView] can create /// A stateful widget that builds a [TabBar] or a [TabBarView] can create
/// a TabController and share it directly. /// a TabController and share it directly.
/// ///
/// ## Sample code
///
/// ```dart /// ```dart
/// class _MyDemoState extends State<MyDemo> with SingleTickerProviderStateMixin { /// class _MyDemoState extends State<MyDemo> with SingleTickerProviderStateMixin {
/// final List<Tab> myTabs = <Tab>[ /// final List<Tab> myTabs = <Tab>[
...@@ -62,12 +64,18 @@ import 'constants.dart'; ...@@ -62,12 +64,18 @@ import 'constants.dart';
/// inherited widget. /// inherited widget.
class TabController extends ChangeNotifier { class TabController extends ChangeNotifier {
/// Creates an object that manages the state required by [TabBar] and a [TabBarView]. /// 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 }) TabController({ int initialIndex: 0, @required this.length, @required TickerProvider vsync })
: assert(length != null && length > 1), : assert(length != null && length >= 0),
assert(initialIndex != null && initialIndex >= 0 && initialIndex < length), assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
_index = initialIndex, _index = initialIndex,
_previousIndex = initialIndex, _previousIndex = initialIndex,
_animationController = new AnimationController( _animationController = length < 2 ? null : new AnimationController(
value: initialIndex.toDouble(), value: initialIndex.toDouble(),
upperBound: (length - 1).toDouble(), upperBound: (length - 1).toDouble(),
vsync: vsync vsync: vsync
...@@ -81,18 +89,21 @@ class TabController extends ChangeNotifier { ...@@ -81,18 +89,21 @@ class TabController extends ChangeNotifier {
/// selected tab is changed, the animation's value equals [index]. The /// selected tab is changed, the animation's value equals [index]. The
/// animation's value can be [offset] by +/- 1.0 to reflect [TabBarView] /// animation's value can be [offset] by +/- 1.0 to reflect [TabBarView]
/// drag scrolling. /// 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; 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; final int length;
void _changeIndex(int value, { Duration duration, Curve curve }) { void _changeIndex(int value, { Duration duration, Curve curve }) {
assert(value != null); assert(value != null);
assert(value >= 0 && value < length); assert(value >= 0 && (value < length || length == 0));
assert(duration == null ? curve == null : true); assert(duration == null ? curve == null : true);
assert(_indexIsChangingCount >= 0); assert(_indexIsChangingCount >= 0);
if (value == _index) if (value == _index || length < 2)
return; return;
_previousIndex = index; _previousIndex = index;
_index = value; _index = value;
...@@ -118,6 +129,9 @@ class TabController extends ChangeNotifier { ...@@ -118,6 +129,9 @@ class TabController extends ChangeNotifier {
/// [indexIsChanging] to false, and notifies listeners. /// [indexIsChanging] to false, and notifies listeners.
/// ///
/// To change the currently selected tab and play the [animation] use [animateTo]. /// 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 get index => _index;
int _index; int _index;
set index(int value) { set index(int value) {
...@@ -148,8 +162,9 @@ class TabController extends ChangeNotifier { ...@@ -148,8 +162,9 @@ class TabController extends ChangeNotifier {
/// drags left or right. A value between -1.0 and 0.0 implies that the /// 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 /// 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. /// 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) { set offset(double value) {
assert(length > 1);
assert(value != null); assert(value != null);
assert(value >= -1.0 && value <= 1.0); assert(value >= -1.0 && value <= 1.0);
assert(!indexIsChanging); assert(!indexIsChanging);
...@@ -160,7 +175,7 @@ class TabController extends ChangeNotifier { ...@@ -160,7 +175,7 @@ class TabController extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_animationController.dispose(); _animationController?.dispose();
super.dispose(); super.dispose();
} }
} }
...@@ -220,7 +235,7 @@ class _TabControllerScope extends InheritedWidget { ...@@ -220,7 +235,7 @@ class _TabControllerScope extends InheritedWidget {
class DefaultTabController extends StatefulWidget { class DefaultTabController extends StatefulWidget {
/// Creates a default tab controller for the given [child] widget. /// 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. /// The [initialIndex] argument must not be null.
const DefaultTabController({ const DefaultTabController({
...@@ -231,7 +246,7 @@ class DefaultTabController extends StatefulWidget { ...@@ -231,7 +246,7 @@ class DefaultTabController extends StatefulWidget {
}) : assert(initialIndex != null), }) : assert(initialIndex != null),
super(key: key); 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; final int length;
/// The initial index of the selected tab. /// The initial index of the selected tab.
......
...@@ -389,22 +389,23 @@ class _TabBarScrollController extends ScrollController { ...@@ -389,22 +389,23 @@ class _TabBarScrollController extends ScrollController {
/// A material design widget that displays a horizontal row of tabs. /// A material design widget that displays a horizontal row of tabs.
/// ///
/// Typically created as part of an [AppBar] and in conjuction with a /// Typically created as the [AppBar.bottom] part of an [AppBar] and in
/// [TabBarView]. /// conjuction with a [TabBarView].
/// ///
/// If a [TabController] is not provided, then there must be a /// 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. /// Requires one of its ancestors to be a [Material] widget.
/// ///
/// See also: /// See also:
/// ///
/// * [TabBarView], which displays the contents that the tab bar is selecting /// * [TabBarView], which displays page views that correspond to each tab.
/// between.
class TabBar extends StatefulWidget implements PreferredSizeWidget { class TabBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a material design tab bar. /// 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 /// If a [TabController] is not provided, then there must be a
/// [DefaultTabController] ancestor. /// [DefaultTabController] ancestor.
...@@ -424,13 +425,15 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -424,13 +425,15 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
this.labelStyle, this.labelStyle,
this.unselectedLabelColor, this.unselectedLabelColor,
this.unselectedLabelStyle, this.unselectedLabelStyle,
}) : assert(tabs != null && tabs.length > 1), }) : assert(tabs != null),
assert(isScrollable != null), assert(isScrollable != null),
assert(indicatorWeight != null && indicatorWeight > 0.0), assert(indicatorWeight != null && indicatorWeight > 0.0),
assert(indicatorPadding != null), assert(indicatorPadding != null),
super(key: key); 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; final List<Widget> tabs;
/// This widget's selection and animation state. /// This widget's selection and animation state.
...@@ -667,6 +670,12 @@ class _TabBarState extends State<TabBar> { ...@@ -667,6 +670,12 @@ class _TabBarState extends State<TabBar> {
@override @override
Widget build(BuildContext context) { 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); final List<Widget> wrappedTabs = new List<Widget>.from(widget.tabs, growable: false);
// If the controller was provided by DefaultTabController and we're part // If the controller was provided by DefaultTabController and we're part
...@@ -774,8 +783,7 @@ class TabBarView extends StatefulWidget { ...@@ -774,8 +783,7 @@ class TabBarView extends StatefulWidget {
Key key, Key key,
@required this.children, @required this.children,
this.controller, this.controller,
}) : assert(children != null && children.length > 1), }) : assert(children != null), super(key: key);
super(key: key);
/// This widget's selection and animation state. /// This widget's selection and animation state.
/// ///
......
...@@ -900,4 +900,99 @@ void main() { ...@@ -900,4 +900,99 @@ void main() {
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight) 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