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 {
ScrollActivityDelegate get delegate => _delegate;
ScrollActivityDelegate _delegate;
bool _isDisposed = false;
/// Updates the activity's link to the [ScrollActivityDelegate].
///
/// This should only be called when an activity is being moved from a defunct
......@@ -134,7 +136,9 @@ abstract class ScrollActivity {
/// Called when the scroll view stops performing this activity.
@mustCallSuper
void dispose() { }
void dispose() {
_isDisposed = true;
}
@override
String toString() => describeIdentity(this);
......@@ -535,7 +539,7 @@ class BallisticScrollActivity extends ScrollActivity {
)
..addListener(_tick)
..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;
......@@ -569,8 +573,12 @@ class BallisticScrollActivity extends ScrollActivity {
}
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);
}
}
@override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
......@@ -628,7 +636,7 @@ class DrivenScrollActivity extends ScrollActivity {
)
..addListener(_tick)
..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;
......@@ -648,8 +656,12 @@ class DrivenScrollActivity extends ScrollActivity {
}
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);
}
}
@override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
......
......@@ -2224,6 +2224,57 @@ void main() {
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 {
// This is a regression test for https://github.com/flutter/flutter/issues/18756
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