Add animationDuration
property to TabController (#91987)
This commit is contained in:
parent
0d0b2dbae5
commit
a4b5123395
@ -101,11 +101,12 @@ class TabController extends ChangeNotifier {
|
||||
///
|
||||
/// The `initialIndex` must be valid given [length] and must not 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, Duration? animationDuration, required this.length, required TickerProvider vsync})
|
||||
: assert(length != null && length >= 0),
|
||||
assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
|
||||
_index = initialIndex,
|
||||
_previousIndex = initialIndex,
|
||||
_animationDuration = animationDuration ?? kTabScrollDuration,
|
||||
_animationController = AnimationController.unbounded(
|
||||
value: initialIndex.toDouble(),
|
||||
vsync: vsync,
|
||||
@ -117,14 +118,16 @@ class TabController extends ChangeNotifier {
|
||||
required int index,
|
||||
required int previousIndex,
|
||||
required AnimationController? animationController,
|
||||
required Duration animationDuration,
|
||||
required this.length,
|
||||
}) : _index = index,
|
||||
_previousIndex = previousIndex,
|
||||
_animationController = animationController;
|
||||
_animationController = animationController,
|
||||
_animationDuration = animationDuration;
|
||||
|
||||
|
||||
/// Creates a new [TabController] with `index`, `previousIndex`, and `length`
|
||||
/// if they are non-null.
|
||||
/// Creates a new [TabController] with `index`, `previousIndex`, `length`, and
|
||||
/// `animationDuration` if they are non-null.
|
||||
///
|
||||
/// This method is used by [DefaultTabController].
|
||||
///
|
||||
@ -134,6 +137,7 @@ class TabController extends ChangeNotifier {
|
||||
required int? index,
|
||||
required int? length,
|
||||
required int? previousIndex,
|
||||
required Duration? animationDuration,
|
||||
}) {
|
||||
if (index != null) {
|
||||
_animationController!.value = index.toDouble();
|
||||
@ -143,6 +147,7 @@ class TabController extends ChangeNotifier {
|
||||
length: length ?? this.length,
|
||||
animationController: _animationController,
|
||||
previousIndex: previousIndex ?? _previousIndex,
|
||||
animationDuration: animationDuration ?? _animationDuration,
|
||||
);
|
||||
}
|
||||
|
||||
@ -159,6 +164,12 @@ class TabController extends ChangeNotifier {
|
||||
Animation<double>? get animation => _animationController?.view;
|
||||
AnimationController? _animationController;
|
||||
|
||||
/// Controls the duration of TabController and TabBarView animations.
|
||||
///
|
||||
/// Defaults to kTabScrollDuration.
|
||||
Duration get animationDuration => _animationDuration;
|
||||
final Duration _animationDuration;
|
||||
|
||||
/// The total number of tabs.
|
||||
///
|
||||
/// Typically greater than one. Must match [TabBar.tabs]'s and
|
||||
@ -174,7 +185,7 @@ class TabController extends ChangeNotifier {
|
||||
return;
|
||||
_previousIndex = index;
|
||||
_index = value;
|
||||
if (duration != null) {
|
||||
if (duration != null && duration > Duration.zero) {
|
||||
_indexIsChangingCount += 1;
|
||||
notifyListeners(); // Because the value of indexIsChanging may have changed.
|
||||
_animationController!
|
||||
@ -228,8 +239,8 @@ class TabController extends ChangeNotifier {
|
||||
///
|
||||
/// While the animation is running [indexIsChanging] is true. When the
|
||||
/// animation completes [offset] will be 0.0.
|
||||
void animateTo(int value, { Duration duration = kTabScrollDuration, Curve curve = Curves.ease }) {
|
||||
_changeIndex(value, duration: duration, curve: curve);
|
||||
void animateTo(int value, { Duration? duration, Curve curve = Curves.ease }) {
|
||||
_changeIndex(value, duration: duration ?? _animationDuration, curve: curve);
|
||||
}
|
||||
|
||||
/// The difference between the [animation]'s value and [index].
|
||||
@ -333,6 +344,7 @@ class DefaultTabController extends StatefulWidget {
|
||||
required this.length,
|
||||
this.initialIndex = 0,
|
||||
required this.child,
|
||||
this.animationDuration,
|
||||
}) : assert(initialIndex != null),
|
||||
assert(length >= 0),
|
||||
assert(length == 0 || (initialIndex >= 0 && initialIndex < length)),
|
||||
@ -349,6 +361,11 @@ class DefaultTabController extends StatefulWidget {
|
||||
/// Defaults to zero.
|
||||
final int initialIndex;
|
||||
|
||||
/// Controls the duration of DefaultTabController and TabBarView animations.
|
||||
///
|
||||
/// Defaults to kTabScrollDuration.
|
||||
final Duration? animationDuration;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// Typically a [Scaffold] whose [AppBar] includes a [TabBar].
|
||||
@ -384,6 +401,7 @@ class _DefaultTabControllerState extends State<DefaultTabController> with Single
|
||||
vsync: this,
|
||||
length: widget.length,
|
||||
initialIndex: widget.initialIndex,
|
||||
animationDuration: widget.animationDuration,
|
||||
);
|
||||
}
|
||||
|
||||
@ -416,9 +434,19 @@ class _DefaultTabControllerState extends State<DefaultTabController> with Single
|
||||
}
|
||||
_controller = _controller._copyWith(
|
||||
length: widget.length,
|
||||
animationDuration: widget.animationDuration,
|
||||
index: newIndex,
|
||||
previousIndex: previousIndex,
|
||||
);
|
||||
}
|
||||
|
||||
if (oldWidget.animationDuration != widget.animationDuration) {
|
||||
_controller = _controller._copyWith(
|
||||
length: widget.length,
|
||||
animationDuration: widget.animationDuration,
|
||||
index: _controller.index,
|
||||
previousIndex: _controller.previousIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1392,10 +1392,18 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
if (_pageController.page == _currentIndex!.toDouble())
|
||||
return Future<void>.value();
|
||||
|
||||
final Duration duration = _controller!.animationDuration;
|
||||
|
||||
if (duration == Duration.zero) {
|
||||
_pageController.jumpToPage(_currentIndex!);
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
final int previousIndex = _controller!.previousIndex;
|
||||
|
||||
if ((_currentIndex! - previousIndex).abs() == 1) {
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease);
|
||||
await _pageController.animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
|
||||
_warpUnderwayCount -= 1;
|
||||
return Future<void>.value();
|
||||
}
|
||||
@ -1415,7 +1423,7 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
});
|
||||
_pageController.jumpToPage(initialPage);
|
||||
|
||||
await _pageController.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease);
|
||||
await _pageController.animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
|
||||
if (!mounted)
|
||||
return Future<void>.value();
|
||||
setState(() {
|
||||
|
@ -109,9 +109,11 @@ Widget buildFrame({
|
||||
required String value,
|
||||
bool isScrollable = false,
|
||||
Color? indicatorColor,
|
||||
Duration? animationDuration,
|
||||
}) {
|
||||
return boilerplate(
|
||||
child: DefaultTabController(
|
||||
animationDuration: animationDuration,
|
||||
initialIndex: tabs.indexOf(value),
|
||||
length: tabs.length,
|
||||
child: TabBar(
|
||||
@ -937,6 +939,262 @@ void main() {
|
||||
expect(find.text('Second'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('TabBar animationDuration sets indicator animation duration', (WidgetTester tester) async {
|
||||
const Duration animationDuration = Duration(milliseconds: 100);
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: animationDuration));
|
||||
final TabController controller = DefaultTabController.of(tester.element(find.text('A')))!;
|
||||
|
||||
await tester.tap(find.text('A'));
|
||||
await tester.pump();
|
||||
expect(controller.indexIsChanging, true);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.pump(animationDuration);
|
||||
expect(controller.index, 0);
|
||||
expect(controller.previousIndex, 1);
|
||||
expect(controller.indexIsChanging, false);
|
||||
|
||||
//Test when index diff is greater than 1
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: animationDuration));
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pump();
|
||||
expect(controller.indexIsChanging, true);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.pump(animationDuration);
|
||||
expect(controller.index, 2);
|
||||
expect(controller.previousIndex, 0);
|
||||
expect(controller.indexIsChanging, false);
|
||||
});
|
||||
|
||||
testWidgets('TabBarView controller sets animation duration', (WidgetTester tester) async {
|
||||
const Duration animationDuration = Duration(milliseconds: 100);
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
final TabController tabController = TabController(
|
||||
vsync: const TestVSync(),
|
||||
initialIndex: 1,
|
||||
length: tabs.length,
|
||||
animationDuration: animationDuration,
|
||||
);
|
||||
await tester.pumpWidget(boilerplate(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TabBar(
|
||||
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||
controller: tabController,
|
||||
),
|
||||
SizedBox(
|
||||
width: 400.0,
|
||||
height: 400.0,
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: const <Widget>[
|
||||
Center(child: Text('0')),
|
||||
Center(child: Text('1')),
|
||||
Center(child: Text('2')),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(tabController.index, 1);
|
||||
|
||||
final PageView pageView = tester.widget(find.byType(PageView));
|
||||
final PageController pageController = pageView.controller;
|
||||
final ScrollPosition position = pageController.position;
|
||||
|
||||
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
|
||||
// page 1 is at 400.0, page 2 is at 800.0.
|
||||
expect(position.pixels, 400);
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pump();
|
||||
expect(position.pixels, 400);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.pump(animationDuration);
|
||||
expect(position.pixels, 800);
|
||||
});
|
||||
|
||||
testWidgets('TabBar tap skips indicator animation when disabled in controller', (WidgetTester tester) async {
|
||||
final List<String> tabs = <String>['A', 'B'];
|
||||
|
||||
const Color indicatorColor = Color(0xFFFF0000);
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'A', indicatorColor: indicatorColor, animationDuration: Duration.zero));
|
||||
|
||||
final RenderBox box = tester.renderObject(find.byType(TabBar));
|
||||
final TabIndicatorRecordingCanvas canvas = TabIndicatorRecordingCanvas(indicatorColor);
|
||||
final TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas);
|
||||
|
||||
box.paint(context, Offset.zero);
|
||||
final Rect indicatorRect0 = canvas.indicatorRect;
|
||||
expect(indicatorRect0.left, 0.0);
|
||||
expect(indicatorRect0.width, 400.0);
|
||||
expect(indicatorRect0.height, 2.0);
|
||||
|
||||
await tester.tap(find.text('B'));
|
||||
await tester.pump();
|
||||
box.paint(context, Offset.zero);
|
||||
final Rect indicatorRect2 = canvas.indicatorRect;
|
||||
expect(indicatorRect2.left, 400.0);
|
||||
expect(indicatorRect2.width, 400.0);
|
||||
expect(indicatorRect2.height, 2.0);
|
||||
});
|
||||
|
||||
testWidgets('TabBar tap changes index instantly when animation is disabled in controller', (WidgetTester tester) async {
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: Duration.zero));
|
||||
final TabController controller = DefaultTabController.of(tester.element(find.text('A')))!;
|
||||
|
||||
await tester.tap(find.text('A'));
|
||||
await tester.pump();
|
||||
expect(controller.index, 0);
|
||||
expect(controller.previousIndex, 1);
|
||||
expect(controller.indexIsChanging, false);
|
||||
|
||||
//Test when index diff is greater than 1
|
||||
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: Duration.zero));
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pump();
|
||||
expect(controller.index, 2);
|
||||
expect(controller.previousIndex, 0);
|
||||
expect(controller.indexIsChanging, false);
|
||||
});
|
||||
|
||||
testWidgets('TabBarView skips animation when disabled in controller', (WidgetTester tester) async {
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
final TabController tabController = TabController(
|
||||
vsync: const TestVSync(),
|
||||
initialIndex: 1,
|
||||
length: tabs.length,
|
||||
animationDuration: Duration.zero,
|
||||
);
|
||||
await tester.pumpWidget(boilerplate(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TabBar(
|
||||
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||
controller: tabController,
|
||||
),
|
||||
SizedBox(
|
||||
width: 400.0,
|
||||
height: 400.0,
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: const <Widget>[
|
||||
Center(child: Text('0')),
|
||||
Center(child: Text('1')),
|
||||
Center(child: Text('2')),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(tabController.index, 1);
|
||||
|
||||
final PageView pageView = tester.widget(find.byType(PageView));
|
||||
final PageController pageController = pageView.controller;
|
||||
final ScrollPosition position = pageController.position;
|
||||
|
||||
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
|
||||
// page 1 is at 400.0, page 2 is at 800.0.
|
||||
expect(position.pixels, 400);
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pump();
|
||||
expect(position.pixels, 800);
|
||||
});
|
||||
|
||||
testWidgets('TabBarView skips animation when disabled in controller - skip tabs', (WidgetTester tester) async {
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
final TabController tabController = TabController(
|
||||
vsync: const TestVSync(),
|
||||
length: tabs.length,
|
||||
animationDuration: Duration.zero,
|
||||
);
|
||||
await tester.pumpWidget(boilerplate(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TabBar(
|
||||
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||
controller: tabController,
|
||||
),
|
||||
SizedBox(
|
||||
width: 400.0,
|
||||
height: 400.0,
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: const <Widget>[
|
||||
Center(child: Text('0')),
|
||||
Center(child: Text('1')),
|
||||
Center(child: Text('2')),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(tabController.index, 0);
|
||||
|
||||
final PageView pageView = tester.widget(find.byType(PageView));
|
||||
final PageController pageController = pageView.controller;
|
||||
final ScrollPosition position = pageController.position;
|
||||
|
||||
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
|
||||
// page 1 is at 400.0, page 2 is at 800.0.
|
||||
expect(position.pixels, 0);
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pump();
|
||||
expect(position.pixels, 800);
|
||||
});
|
||||
|
||||
testWidgets('TabBarView skips animation when disabled in controller - two tabs', (WidgetTester tester) async {
|
||||
final List<String> tabs = <String>['A', 'B'];
|
||||
final TabController tabController = TabController(
|
||||
vsync: const TestVSync(),
|
||||
length: tabs.length,
|
||||
animationDuration: Duration.zero,
|
||||
);
|
||||
await tester.pumpWidget(boilerplate(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
TabBar(
|
||||
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||
controller: tabController,
|
||||
),
|
||||
SizedBox(
|
||||
width: 400.0,
|
||||
height: 400.0,
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: const <Widget>[
|
||||
Center(child: Text('0')),
|
||||
Center(child: Text('1')),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
expect(tabController.index, 0);
|
||||
|
||||
final PageView pageView = tester.widget(find.byType(PageView));
|
||||
final PageController pageController = pageView.controller;
|
||||
final ScrollPosition position = pageController.position;
|
||||
|
||||
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
|
||||
// page 1 is at 400.0, page 2 is at 800.0.
|
||||
expect(position.pixels, 0);
|
||||
await tester.tap(find.text('B'));
|
||||
await tester.pump();
|
||||
expect(position.pixels, 400);
|
||||
});
|
||||
|
||||
testWidgets('TabBar tap animates the selection indicator', (WidgetTester tester) async {
|
||||
// This is a regression test for https://github.com/flutter/flutter/issues/7479
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user