Unverified Commit 083ac65c authored by Bruno Leroux's avatar Bruno Leroux Committed by GitHub

Fix TabBarView desynchronized after animation interruption (#132748)

parent e9beaea0
...@@ -63,6 +63,8 @@ abstract class ScrollActivity { ...@@ -63,6 +63,8 @@ abstract class ScrollActivity {
ScrollActivityDelegate get delegate => _delegate; ScrollActivityDelegate get delegate => _delegate;
ScrollActivityDelegate _delegate; ScrollActivityDelegate _delegate;
bool _isDisposed = false;
/// Updates the activity's link to the [ScrollActivityDelegate]. /// Updates the activity's link to the [ScrollActivityDelegate].
/// ///
/// This should only be called when an activity is being moved from a defunct /// This should only be called when an activity is being moved from a defunct
...@@ -134,7 +136,9 @@ abstract class ScrollActivity { ...@@ -134,7 +136,9 @@ abstract class ScrollActivity {
/// Called when the scroll view stops performing this activity. /// Called when the scroll view stops performing this activity.
@mustCallSuper @mustCallSuper
void dispose() { } void dispose() {
_isDisposed = true;
}
@override @override
String toString() => describeIdentity(this); String toString() => describeIdentity(this);
...@@ -535,7 +539,7 @@ class BallisticScrollActivity extends ScrollActivity { ...@@ -535,7 +539,7 @@ class BallisticScrollActivity extends ScrollActivity {
) )
..addListener(_tick) ..addListener(_tick)
..animateWith(simulation) ..animateWith(simulation)
.whenComplete(_end); // won't trigger if we dispose _controller first .whenComplete(_end); // won't trigger if we dispose _controller before it completes.
} }
late AnimationController _controller; late AnimationController _controller;
...@@ -569,8 +573,12 @@ class BallisticScrollActivity extends ScrollActivity { ...@@ -569,8 +573,12 @@ class BallisticScrollActivity extends ScrollActivity {
} }
void _end() { void _end() {
// Check if the activity was disposed before going ballistic because _end might be called
// if _controller is disposed just after completion.
if (!_isDisposed) {
delegate.goBallistic(0.0); delegate.goBallistic(0.0);
} }
}
@override @override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) { void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
...@@ -628,7 +636,7 @@ class DrivenScrollActivity extends ScrollActivity { ...@@ -628,7 +636,7 @@ class DrivenScrollActivity extends ScrollActivity {
) )
..addListener(_tick) ..addListener(_tick)
..animateTo(to, duration: duration, curve: curve) ..animateTo(to, duration: duration, curve: curve)
.whenComplete(_end); // won't trigger if we dispose _controller first .whenComplete(_end); // won't trigger if we dispose _controller before it completes.
} }
late final Completer<void> _completer; late final Completer<void> _completer;
...@@ -648,8 +656,12 @@ class DrivenScrollActivity extends ScrollActivity { ...@@ -648,8 +656,12 @@ class DrivenScrollActivity extends ScrollActivity {
} }
void _end() { void _end() {
// Check if the activity was disposed before going ballistic because _end might be called
// if _controller is disposed just after completion.
if (!_isDisposed) {
delegate.goBallistic(velocity); delegate.goBallistic(velocity);
} }
}
@override @override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) { void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
......
...@@ -2224,6 +2224,57 @@ void main() { ...@@ -2224,6 +2224,57 @@ void main() {
expect(tabController.index, 0); expect(tabController.index, 0);
}); });
testWidgets('On going TabBarView animation can be interrupted by a new animation', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/132293.
final List<String> tabs = <String>['A', 'B', 'C'];
final TabController tabController = TabController(
vsync: const TestVSync(),
length: tabs.length,
);
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')),
],
),
),
],
),
));
// First page is visible.
expect(tabController.index, 0);
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Animate to the second page.
tabController.animateTo(1);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
// Animate back to the first page before the previous animation ends.
tabController.animateTo(0);
await tester.pumpAndSettle();
// First page should be visible.
expect(tabController.index, 0);
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
});
testWidgets('Can switch to non-neighboring tab in nested TabBarView without crashing', (WidgetTester tester) async { testWidgets('Can switch to non-neighboring tab in nested TabBarView without crashing', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/18756 // This is a regression test for https://github.com/flutter/flutter/issues/18756
final TabController mainTabController = _tabController(length: 4, vsync: const TestVSync()); final TabController mainTabController = _tabController(length: 4, vsync: const TestVSync());
......
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