Allow TabBars, TabBarViews, TabControllers, with zero or one tabs (#10608)
This commit is contained in:
parent
a84877222c
commit
123e9e013d
@ -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);
|
||||
});
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user