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);