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 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';
|
|||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
/// [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 {
|
|||||||
/// 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 {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_animationController.dispose();
|
_animationController?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 {
|
|||||||
}) : 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 {
|
|||||||
|
|
||||||
/// 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 {
|
|||||||
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> {
|
|||||||
|
|
||||||
@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 {
|
|||||||
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() {
|
|||||||
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);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user