[Material] Update TabController to support dynamic Tabs (#30884)
* Update TabController to support dynamic tabs. * Added test for single Tab showing correct color.
This commit is contained in:
parent
8e66c53f3d
commit
5412ef07f2
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
@ -85,12 +87,40 @@ class TabController extends ChangeNotifier {
|
||||
assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
|
||||
_index = initialIndex,
|
||||
_previousIndex = initialIndex,
|
||||
_animationController = length < 2 ? null : AnimationController(
|
||||
_animationController = AnimationController.unbounded(
|
||||
value: initialIndex.toDouble(),
|
||||
upperBound: (length - 1).toDouble(),
|
||||
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
|
||||
/// selected tab indicator as well as the scrollOffsets of the [TabBar]
|
||||
/// and [TabBarView].
|
||||
@ -178,9 +208,8 @@ 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 => length > 1 ? _animationController.value - _index.toDouble() : 0.0;
|
||||
double get offset => _animationController.value - _index.toDouble();
|
||||
set offset(double value) {
|
||||
assert(length > 1);
|
||||
assert(value != null);
|
||||
assert(value >= -1.0 && value <= 1.0);
|
||||
assert(!indexIsChanging);
|
||||
@ -262,6 +291,8 @@ class DefaultTabController extends StatefulWidget {
|
||||
this.initialIndex = 0,
|
||||
@required this.child,
|
||||
}) : assert(initialIndex != null),
|
||||
assert(length >= 0),
|
||||
assert(initialIndex >= 0 && initialIndex < length),
|
||||
super(key: key);
|
||||
|
||||
/// The total number of tabs. Typically greater than one. Must match
|
||||
@ -323,4 +354,24 @@ class _DefaultTabControllerState extends State<DefaultTabController> with Single
|
||||
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> {
|
||||
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)
|
||||
return;
|
||||
|
||||
@ -960,6 +950,15 @@ class _TabBarState extends State<TabBar> {
|
||||
@override
|
||||
Widget build(BuildContext 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);
|
||||
if (_controller.length == 0) {
|
||||
return Container(
|
||||
@ -1144,16 +1143,6 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
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)
|
||||
return;
|
||||
|
||||
@ -1268,6 +1257,15 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
|
||||
@override
|
||||
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>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: PageView(
|
||||
|
@ -641,7 +641,7 @@ void main() {
|
||||
expect(tabController.previousIndex, 1);
|
||||
expect(tabController.indexIsChanging, false);
|
||||
expect(tabController.animation.value, 1.0);
|
||||
expect(tabController.animation.status, AnimationStatus.completed);
|
||||
expect(tabController.animation.status, AnimationStatus.forward);
|
||||
|
||||
tabController.index = 0;
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
@ -2100,4 +2100,71 @@ void main() {
|
||||
expect(tester.hasRunningAnimations, isFalse);
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user