mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Fix TabBarView and TabBar animations are not synchronized (#122021)
This commit is contained in:
parent
27aa83b48b
commit
5d10cc28ca
@ -1529,10 +1529,10 @@ class TabBarView extends StatefulWidget {
|
||||
class _TabBarViewState extends State<TabBarView> {
|
||||
TabController? _controller;
|
||||
late PageController _pageController;
|
||||
late List<Widget> _children;
|
||||
late List<Widget> _childrenWithKey;
|
||||
int? _currentIndex;
|
||||
int _warpUnderwayCount = 0;
|
||||
int _scrollUnderwayCount = 0;
|
||||
bool _debugHasScheduledValidChildrenCountCheck = false;
|
||||
|
||||
// If the TabBarView is rebuilt with a new tab controller, the caller should
|
||||
@ -1568,6 +1568,22 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
}
|
||||
}
|
||||
|
||||
void _jumpToPage(int page) {
|
||||
_warpUnderwayCount += 1;
|
||||
_pageController.jumpToPage(page);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
Future<void> _animateToPage(
|
||||
int page, {
|
||||
required Duration duration,
|
||||
required Curve curve,
|
||||
}) async {
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController.animateToPage(page, duration: duration, curve: curve);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -1591,10 +1607,10 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_updateTabController();
|
||||
_currentIndex = _controller!.index;
|
||||
_warpUnderwayCount += 1;
|
||||
_pageController.jumpToPage(_currentIndex!);
|
||||
_warpUnderwayCount -= 1;
|
||||
_jumpToPage(_currentIndex!);
|
||||
}
|
||||
// While a warp is under way, we stop updating the tab page contents.
|
||||
// This is tracked in https://github.com/flutter/flutter/issues/31269.
|
||||
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) {
|
||||
_updateChildren();
|
||||
}
|
||||
@ -1611,12 +1627,11 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
}
|
||||
|
||||
void _updateChildren() {
|
||||
_children = widget.children;
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
if (_warpUnderwayCount > 0 || !_controller!.indexIsChanging) {
|
||||
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
|
||||
return;
|
||||
} // This widget is driving the controller's animation.
|
||||
|
||||
@ -1626,71 +1641,73 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _warpToCurrentIndex() async {
|
||||
if (!mounted) {
|
||||
return Future<void>.value();
|
||||
void _warpToCurrentIndex() {
|
||||
if (!mounted || _pageController.page == _currentIndex!.toDouble()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pageController.page == _currentIndex!.toDouble()) {
|
||||
return Future<void>.value();
|
||||
final bool adjacentDestination = (_currentIndex! - _controller!.previousIndex).abs() == 1;
|
||||
if (adjacentDestination) {
|
||||
_warpToAdjacentTab(_controller!.animationDuration);
|
||||
} else {
|
||||
_warpToNonAdjacentTab(_controller!.animationDuration);
|
||||
}
|
||||
}
|
||||
|
||||
final Duration duration = _controller!.animationDuration;
|
||||
Future<void> _warpToAdjacentTab(Duration duration) async {
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() { _updateChildren(); });
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
Future<void> _warpToNonAdjacentTab(Duration duration) async {
|
||||
final int previousIndex = _controller!.previousIndex;
|
||||
|
||||
if ((_currentIndex! - previousIndex).abs() == 1) {
|
||||
if (duration == Duration.zero) {
|
||||
_pageController.jumpToPage(_currentIndex!);
|
||||
return Future<void>.value();
|
||||
}
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController.animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
|
||||
_warpUnderwayCount -= 1;
|
||||
|
||||
if (mounted && widget.children != _children) {
|
||||
setState(() { _updateChildren(); });
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
assert((_currentIndex! - previousIndex).abs() > 1);
|
||||
|
||||
// initialPage defines which page is shown when starting the animation.
|
||||
// This page is adjacent to the destination page.
|
||||
final int initialPage = _currentIndex! > previousIndex
|
||||
? _currentIndex! - 1
|
||||
: _currentIndex! + 1;
|
||||
final List<Widget> originalChildren = _childrenWithKey;
|
||||
setState(() {
|
||||
_warpUnderwayCount += 1;
|
||||
|
||||
setState(() {
|
||||
// Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children.
|
||||
// For motivation, see https://github.com/flutter/flutter/pull/29188 and
|
||||
// https://github.com/flutter/flutter/issues/27010#issuecomment-486475152.
|
||||
_childrenWithKey = List<Widget>.of(_childrenWithKey, growable: false);
|
||||
final Widget temp = _childrenWithKey[initialPage];
|
||||
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
|
||||
_childrenWithKey[previousIndex] = temp;
|
||||
});
|
||||
_pageController.jumpToPage(initialPage);
|
||||
|
||||
// Make a first jump to the adjacent page.
|
||||
_jumpToPage(initialPage);
|
||||
|
||||
// Jump or animate to the destination page.
|
||||
if (duration == Duration.zero) {
|
||||
_pageController.jumpToPage(_currentIndex!);
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _pageController.animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
|
||||
|
||||
if (!mounted) {
|
||||
return Future<void>.value();
|
||||
}
|
||||
await _animateToPage(_currentIndex!, duration: duration, curve: Curves.ease);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_warpUnderwayCount -= 1;
|
||||
if (widget.children != _children) {
|
||||
_updateChildren();
|
||||
} else {
|
||||
_childrenWithKey = originalChildren;
|
||||
}
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() { _updateChildren(); });
|
||||
}
|
||||
}
|
||||
|
||||
void _syncControllerOffset() {
|
||||
_controller!.offset = clampDouble(_pageController.page! - _controller!.index, -1.0, 1.0);
|
||||
}
|
||||
|
||||
// Called when the PageView scrolls
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (_warpUnderwayCount > 0) {
|
||||
if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1698,21 +1715,22 @@ class _TabBarViewState extends State<TabBarView> {
|
||||
return false;
|
||||
}
|
||||
|
||||
_warpUnderwayCount += 1;
|
||||
_scrollUnderwayCount += 1;
|
||||
if (notification is ScrollUpdateNotification && !_controller!.indexIsChanging) {
|
||||
if ((_pageController.page! - _controller!.index).abs() > 1.0) {
|
||||
final bool pageChanged = (_pageController.page! - _controller!.index).abs() > 1.0;
|
||||
if (pageChanged) {
|
||||
_controller!.index = _pageController.page!.round();
|
||||
_currentIndex =_controller!.index;
|
||||
}
|
||||
_controller!.offset = clampDouble(_pageController.page! - _controller!.index, -1.0, 1.0);
|
||||
_syncControllerOffset();
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_controller!.index = _pageController.page!.round();
|
||||
_currentIndex = _controller!.index;
|
||||
if (!_controller!.indexIsChanging) {
|
||||
_controller!.offset = clampDouble(_pageController.page! - _controller!.index, -1.0, 1.0);
|
||||
_syncControllerOffset();
|
||||
}
|
||||
}
|
||||
_warpUnderwayCount -= 1;
|
||||
_scrollUnderwayCount -= 1;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1120,6 +1120,65 @@ void main() {
|
||||
expect(position.pixels, 800);
|
||||
});
|
||||
|
||||
testWidgets('TabBarView animation can be interrupted', (WidgetTester tester) async {
|
||||
const Duration animationDuration = Duration(seconds: 2);
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
final TabController tabController = TabController(
|
||||
vsync: const TestVSync(),
|
||||
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, 0);
|
||||
|
||||
final PageView pageView = tester.widget<PageView>(find.byType(PageView));
|
||||
final PageController pageController = pageView.controller;
|
||||
final ScrollPosition position = pageController.position;
|
||||
|
||||
expect(position.pixels, 0.0);
|
||||
|
||||
await tester.tap(find.text('C'));
|
||||
await tester.pump(const Duration(milliseconds: 10)); // TODO(bleroux): find why this is needed.
|
||||
|
||||
// Runs the animation for half of the animation duration.
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// The position should be between page 1 and page 2.
|
||||
expect(position.pixels, greaterThan(400.0));
|
||||
expect(position.pixels, lessThan(800.0));
|
||||
|
||||
// Switch to another tab before the end of the animation.
|
||||
await tester.tap(find.text('A'));
|
||||
await tester.pump(const Duration(milliseconds: 10)); // TODO(bleroux): find why this is needed.
|
||||
await tester.pump(animationDuration);
|
||||
expect(position.pixels, 0.0);
|
||||
|
||||
await tester.pumpAndSettle(); // Finish the animation.
|
||||
});
|
||||
|
||||
testWidgets('TabBarView viewportFraction sets PageView viewport fraction', (WidgetTester tester) async {
|
||||
const Duration animationDuration = Duration(milliseconds: 100);
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user