Unverified Commit 5412ef07 authored by MH Johnson's avatar MH Johnson Committed by GitHub

[Material] Update TabController to support dynamic Tabs (#30884)

* Update TabController to support dynamic tabs.

* Added test for single Tab showing correct color.
parent 8e66c53f
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
...@@ -85,12 +87,40 @@ class TabController extends ChangeNotifier { ...@@ -85,12 +87,40 @@ class TabController extends ChangeNotifier {
assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)), assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
_index = initialIndex, _index = initialIndex,
_previousIndex = initialIndex, _previousIndex = initialIndex,
_animationController = length < 2 ? null : AnimationController( _animationController = AnimationController.unbounded(
value: initialIndex.toDouble(), value: initialIndex.toDouble(),
upperBound: (length - 1).toDouble(),
vsync: vsync, vsync: vsync,
); );
// Private constructor used by `_copyWith`. This allows a new TabController to
// be created without having to create a new animationController.
TabController._({
int index,
int previousIndex,
AnimationController animationController,
@required this.length,
}) : _index = index,
_previousIndex = previousIndex,
_animationController = animationController;
/// Creates a new [TabController] with `index`, `previousIndex`, and `length`
/// if they are non-null.
///
/// This will reuse the existing [_animationController].
///
/// This is useful for [DefaultTabController], for example when
/// [DefaultTabController.length] is updated, this method is called so that a
/// new [TabController] is created without having to create a new [AnimationController].
TabController _copyWith({ int index, int length, int previousIndex }) {
return TabController._(
index: index ?? _index,
length: length ?? this.length,
animationController: _animationController,
previousIndex: previousIndex ?? _previousIndex,
);
}
/// An animation whose value represents the current position of the [TabBar]'s /// An animation whose value represents the current position of the [TabBar]'s
/// selected tab indicator as well as the scrollOffsets of the [TabBar] /// selected tab indicator as well as the scrollOffsets of the [TabBar]
/// and [TabBarView]. /// and [TabBarView].
...@@ -178,9 +208,8 @@ class TabController extends ChangeNotifier { ...@@ -178,9 +208,8 @@ 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 => length > 1 ? _animationController.value - _index.toDouble() : 0.0; double get offset => _animationController.value - _index.toDouble();
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);
...@@ -262,6 +291,8 @@ class DefaultTabController extends StatefulWidget { ...@@ -262,6 +291,8 @@ class DefaultTabController extends StatefulWidget {
this.initialIndex = 0, this.initialIndex = 0,
@required this.child, @required this.child,
}) : assert(initialIndex != null), }) : assert(initialIndex != null),
assert(length >= 0),
assert(initialIndex >= 0 && initialIndex < length),
super(key: key); super(key: key);
/// The total number of tabs. Typically greater than one. Must match /// The total number of tabs. Typically greater than one. Must match
...@@ -323,4 +354,24 @@ class _DefaultTabControllerState extends State<DefaultTabController> with Single ...@@ -323,4 +354,24 @@ class _DefaultTabControllerState extends State<DefaultTabController> with Single
child: widget.child, child: widget.child,
); );
} }
@override
void didUpdateWidget(DefaultTabController oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.length != widget.length) {
// If the length is shortened while the last tab is selected, we should
// automatically update the index of the controller to be the new last tab.
int newIndex;
int previousIndex = _controller.previousIndex;
if (_controller.index >= widget.length) {
newIndex = math.max(0, widget.length - 1);
previousIndex = _controller.index;
}
_controller = _controller._copyWith(
length: widget.length,
index: newIndex,
previousIndex: previousIndex,
);
}
}
} }
...@@ -783,16 +783,6 @@ class _TabBarState extends State<TabBar> { ...@@ -783,16 +783,6 @@ class _TabBarState extends State<TabBar> {
return true; return true;
}()); }());
assert(() {
if (newController.length != widget.tabs.length) {
throw FlutterError(
'Controller\'s length property (${newController.length}) does not match the \n'
'number of tab elements (${widget.tabs.length}) present in TabBar\'s tabs property.'
);
}
return true;
}());
if (newController == _controller) if (newController == _controller)
return; return;
...@@ -960,6 +950,15 @@ class _TabBarState extends State<TabBar> { ...@@ -960,6 +950,15 @@ class _TabBarState extends State<TabBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
assert(() {
if (_controller.length != widget.tabs.length) {
throw FlutterError(
'Controller\'s length property (${_controller.length}) does not match the \n'
'number of tabs (${widget.tabs.length}) present in TabBar\'s tabs property.'
);
}
return true;
}());
final MaterialLocalizations localizations = MaterialLocalizations.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context);
if (_controller.length == 0) { if (_controller.length == 0) {
return Container( return Container(
...@@ -1144,16 +1143,6 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1144,16 +1143,6 @@ class _TabBarViewState extends State<TabBarView> {
return true; return true;
}()); }());
assert(() {
if (newController.length != widget.children.length) {
throw FlutterError(
'Controller\'s length property (${newController.length}) does not match the \n'
'number of elements (${widget.children.length}) present in TabBarView\'s children property.'
);
}
return true;
}());
if (newController == _controller) if (newController == _controller)
return; return;
...@@ -1268,6 +1257,15 @@ class _TabBarViewState extends State<TabBarView> { ...@@ -1268,6 +1257,15 @@ class _TabBarViewState extends State<TabBarView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(() {
if (_controller.length != widget.children.length) {
throw FlutterError(
'Controller\'s length property (${_controller.length}) does not match the \n'
'number of tabs (${widget.children.length}) present in TabBar\'s tabs property.'
);
}
return true;
}());
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification, onNotification: _handleScrollNotification,
child: PageView( child: PageView(
......
...@@ -641,7 +641,7 @@ void main() { ...@@ -641,7 +641,7 @@ void main() {
expect(tabController.previousIndex, 1); expect(tabController.previousIndex, 1);
expect(tabController.indexIsChanging, false); expect(tabController.indexIsChanging, false);
expect(tabController.animation.value, 1.0); expect(tabController.animation.value, 1.0);
expect(tabController.animation.status, AnimationStatus.completed); expect(tabController.animation.status, AnimationStatus.forward);
tabController.index = 0; tabController.index = 0;
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
...@@ -2100,4 +2100,71 @@ void main() { ...@@ -2100,4 +2100,71 @@ void main() {
expect(tester.hasRunningAnimations, isFalse); expect(tester.hasRunningAnimations, isFalse);
expect(await tester.pumpAndSettle(), 1); // no more frames are scheduled. expect(await tester.pumpAndSettle(), 1); // no more frames are scheduled.
}); });
// Regression test for https://github.com/flutter/flutter/issues/20292.
testWidgets('Number of tabs can be updated dynamically', (WidgetTester tester) async {
final List<String> threeTabs = <String>['A', 'B', 'C'];
final List<String> twoTabs = <String>['A', 'B'];
final List<String> oneTab = <String>['A'];
final Key key = UniqueKey();
Widget buildTabs(List<String> tabs) {
return boilerplate(
child: DefaultTabController(
key: key,
length: tabs.length,
child: TabBar(
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
),
),
);
}
TabController getController() => DefaultTabController.of(tester.element(find.text('A')));
await tester.pumpWidget(buildTabs(threeTabs));
await tester.tap(find.text('B'));
await tester.pump();
TabController controller = getController();
expect(controller.previousIndex, 0);
expect(controller.index, 1);
expect(controller.length, 3);
await tester.pumpWidget(buildTabs(twoTabs));
controller = getController();
expect(controller.previousIndex, 0);
expect(controller.index, 1);
expect(controller.length, 2);
await tester.pumpWidget(buildTabs(oneTab));
controller = getController();
expect(controller.previousIndex, 1);
expect(controller.index, 0);
expect(controller.length, 1);
await tester.pumpWidget(buildTabs(twoTabs));
controller = getController();
expect(controller.previousIndex, 1);
expect(controller.index, 0);
expect(controller.length, 2);
});
// Regression test for https://github.com/flutter/flutter/issues/15008.
testWidgets('TabBar with one tab has correct color', (WidgetTester tester) async {
const Tab tab = Tab(text: 'A');
const Color selectedTabColor = Color(1);
const Color unselectedTabColor = Color(2);
await tester.pumpWidget(boilerplate(
child: const DefaultTabController(
length: 1,
child: TabBar(
tabs: <Tab>[tab],
labelColor: selectedTabColor,
unselectedLabelColor: unselectedTabColor,
),
),
));
final IconThemeData iconTheme = IconTheme.of(tester.element(find.text('A')));
expect(iconTheme.color, equals(selectedTabColor));
});
} }
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