From 65dca5b578e70f0d262c6066ccae932cf2ca56e2 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Wed, 16 Dec 2015 14:33:30 -0800 Subject: [PATCH] Correct the TabBarView swipe selection change animation The TabBarSelection change animation needs to start where the fling's drag gesture ended rather than from zero. The intial vlaue of progress for the TabBarSelection's performance is now converted from the range used during an interactive drag, to the range used when animating from the previously selected tab to the new one. TabBarSelection now requires a maxIndex parameter. --- .../material_gallery/lib/demo/tabs_demo.dart | 2 +- examples/stocks/lib/stock_home.dart | 2 +- packages/flutter/lib/src/material/tabs.dart | 82 +++++++++++++------ packages/flutter/test/widget/tabs_test.dart | 4 +- 4 files changed, 60 insertions(+), 30 deletions(-) diff --git a/examples/material_gallery/lib/demo/tabs_demo.dart b/examples/material_gallery/lib/demo/tabs_demo.dart index ea09f1d092..3d2e3ff2dd 100644 --- a/examples/material_gallery/lib/demo/tabs_demo.dart +++ b/examples/material_gallery/lib/demo/tabs_demo.dart @@ -6,8 +6,8 @@ import 'package:flutter/material.dart'; import 'widget_demo.dart'; -final TabBarSelection _selection = new TabBarSelection(); final List _iconNames = ["event", "home", "android", "alarm", "face", "language"]; +final TabBarSelection _selection = new TabBarSelection(maxIndex: _iconNames.length - 1); Widget buildTabBar(_) { return new TabBar( diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 81fcd26147..4975aacc58 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -30,7 +30,7 @@ class StockHomeState extends State { super.initState(); _tabBarSelection = PageStorage.of(context)?.readState(context); if (_tabBarSelection == null) { - _tabBarSelection = new TabBarSelection(); + _tabBarSelection = new TabBarSelection(maxIndex: 1); PageStorage.of(context)?.writeState(context, _tabBarSelection); } } diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 64f886247b..a10970c79a 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -382,9 +382,14 @@ class _TabsScrollBehavior extends BoundedBehavior { } class TabBarSelection { - TabBarSelection({ int index: 0, this.onChanged }) : _index = index; + TabBarSelection({ int index: 0, this.maxIndex, this.onChanged }) : _index = index { + assert(maxIndex != null); + assert(index != null); + assert(_index >= 0 && _index <= maxIndex); + } final VoidCallback onChanged; + final int maxIndex; PerformanceView get performance => _performance.view; final _performance = new Performance(duration: _kTabBarScroll, progress: 1.0); @@ -400,9 +405,33 @@ class TabBarSelection { _previousIndex = _index; _index = value; _indexIsChanging = true; + + // If the selected index change was triggered by a drag gesture, the current + // value of _performance.progress will reflect where the gesture ended. While + // the drag was underway progress indicates where the indicator and TabBarView + // scrollPosition are vis the indices of the two tabs adjacent to the selected + // one. So 0.5 means the drag didn't move at all, 0.0 means the drag extended + // to the beginning of the tab on the left and 1.0 likewise for the tab on the + // right. That is unless the selected index was 0 or maxIndex. In those cases + // progress just moves between the selected tab and the adjacent one. + // Convert progress to reflect the fact that we're now moving between (just) + // the previous and current selection index. + + double progress; + if (_performance.status == PerformanceStatus.completed) + progress = 0.0; + else if (_previousIndex == 0) + progress = _performance.progress; + else if (_previousIndex == maxIndex) + progress = 1.0 - _performance.progress; + else if (_previousIndex < _index) + progress = (_performance.progress - 0.5) * 2.0; + else + progress = 1.0 - _performance.progress * 2.0; + _performance - ..progress = 0.0 - ..play().then((_) { + ..progress = progress + ..forward().then((_) { if (onChanged != null) onChanged(); _indexIsChanging = false; @@ -425,7 +454,9 @@ class TabBar extends Scrollable { this.isScrollable: false }) : super(key: key, scrollDirection: ScrollDirection.horizontal) { assert(labels != null); + assert(labels.length > 1); assert(selection != null); + assert(selection.maxIndex == labels.length - 1); } final Iterable labels; @@ -648,7 +679,10 @@ class TabBarView extends PageableList { itemBuilder: itemBuilder, itemsWrap: false ) { + assert(items != null); + assert(items.length > 1); assert(selection != null); + assert(selection.maxIndex == items.length - 1); } final TabBarSelection selection; @@ -690,44 +724,40 @@ class _TabBarViewState extends PageableListState> { super.initState(); _initItemIndicesAndScrollPosition(); _performance - ..addStatusListener(_handleStatusChange) ..addListener(_handleProgressChange); } void dispose() { _performance - ..removeStatusListener(_handleStatusChange) ..removeListener(_handleProgressChange) ..stop(); super.dispose(); } - void _handleStatusChange(PerformanceStatus status) { - if (!config.selection.indexIsChanging) - return; - // The TabBar is driving the TabBarSelection performance. - - final int selectedIndex = config.selection.index; - final int previousSelectedIndex = config.selection.previousIndex; - - if (status == PerformanceStatus.forward) { - if (selectedIndex < previousSelectedIndex) { - _itemIndices = [selectedIndex, previousSelectedIndex]; - _scrollDirection = AnimationDirection.reverse; - } else { - _itemIndices = [previousSelectedIndex, selectedIndex]; - _scrollDirection = AnimationDirection.forward; - } - } else if (status == PerformanceStatus.completed) { - _initItemIndicesAndScrollPosition(); - } - } - void _handleProgressChange() { if (!config.selection.indexIsChanging) return; // The TabBar is driving the TabBarSelection performance. + if (_performance.status == PerformanceStatus.completed) { + _initItemIndicesAndScrollPosition(); + return; + } + + if (_performance.status != PerformanceStatus.forward) + return; + + final int selectedIndex = config.selection.index; + final int previousSelectedIndex = config.selection.previousIndex; + + if (selectedIndex < previousSelectedIndex) { + _itemIndices = [selectedIndex, previousSelectedIndex]; + _scrollDirection = AnimationDirection.reverse; + } else { + _itemIndices = [previousSelectedIndex, selectedIndex]; + _scrollDirection = AnimationDirection.forward; + } + if (_scrollDirection == AnimationDirection.forward) scrollTo(_performance.progress); else diff --git a/packages/flutter/test/widget/tabs_test.dart b/packages/flutter/test/widget/tabs_test.dart index 0f639a11d6..1844b39062 100644 --- a/packages/flutter/test/widget/tabs_test.dart +++ b/packages/flutter/test/widget/tabs_test.dart @@ -23,7 +23,7 @@ void main() { test('TabBar tap selects tab', () { testWidgets((WidgetTester tester) { List tabs = ['A', 'B', 'C']; - selection = new TabBarSelection(index: 2); + selection = new TabBarSelection(index: 2, maxIndex: tabs.length - 1); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: false)); expect(tester.findText('A'), isNotNull); @@ -51,7 +51,7 @@ void main() { test('Scrollable TabBar tap selects tab', () { testWidgets((WidgetTester tester) { List tabs = ['A', 'B', 'C']; - selection = new TabBarSelection(index: 2); + selection = new TabBarSelection(index: 2, maxIndex: tabs.length - 1); tester.pumpWidget(buildFrame(tabs: tabs, isScrollable: true)); expect(tester.findText('A'), isNotNull);