Scrollable TabBar shows initialIndex (#9402)
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index c8b0e82..6291752 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart
@@ -335,6 +335,54 @@ } } +// This class, and TabBarScrollController, only exist to handle the the case +// where a scrollable TabBar has a non-zero initialIndex. In that case we can +// only compute the scroll position's initial scroll offset (the "correct" +// pixels value) after the TabBar viewport width and scroll limits are known. +class _TabBarScrollPosition extends ScrollPosition { + _TabBarScrollPosition({ + ScrollPhysics physics, + AbstractScrollState state, + ScrollPosition oldPosition, + this.tabBar, + }) : super( + physics: physics, + state: state, + initialPixels: null, + oldPosition: oldPosition, + ); + + final _TabBarState tabBar; + + @override + bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { + bool result = true; + if (pixels == null) { + correctPixels(tabBar._initialScrollOffset(viewportDimension, minScrollExtent, maxScrollExtent)); + result = false; + } + return super.applyContentDimensions(minScrollExtent, maxScrollExtent) && result; + } +} + +// This class, and TabBarScrollPosition, only exist to handle the the case +// where a scrollable TabBar has a non-zero initialIndex. +class _TabBarScrollController extends ScrollController { + _TabBarScrollController(this.tabBar); + + final _TabBarState tabBar; + + @override + ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) { + return new _TabBarScrollPosition( + physics: physics, + state: state, + oldPosition: oldPosition, + tabBar: tabBar, + ); + } +} + /// A material design widget that displays a horizontal row of tabs. /// /// Typically created as part of an [AppBar] and in conjuction with a @@ -440,7 +488,7 @@ } class _TabBarState extends State<TabBar> { - final ScrollController _scrollController = new ScrollController(); + ScrollController _scrollController; TabController _controller; _IndicatorPainter _indicatorPainter; @@ -504,14 +552,22 @@ // tabOffsets[tabOffsets.length] is the right edge of the last tab. int get maxTabIndex => _indicatorPainter.tabOffsets.length - 2; - double _tabCenteredScrollOffset(int tabIndex) { + double _tabScrollOffset(int index, double viewportWidth, double minExtent, double maxExtent) { + if (!widget.isScrollable) + return 0.0; final List<double> tabOffsets = _indicatorPainter.tabOffsets; - assert(tabOffsets != null && tabIndex >= 0 && tabIndex <= maxTabIndex); + assert(tabOffsets != null && index >= 0 && index <= maxTabIndex); + final double tabCenter = (tabOffsets[index] + tabOffsets[index + 1]) / 2.0; + return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent); + } + double _tabCenteredScrollOffset(int index) { final ScrollPosition position = _scrollController.position; - final double tabCenter = (tabOffsets[tabIndex] + tabOffsets[tabIndex + 1]) / 2.0; - return (tabCenter - position.viewportDimension / 2.0) - .clamp(position.minScrollExtent, position.maxScrollExtent); + return _tabScrollOffset(index, position.viewportDimension, position.minScrollExtent, position.maxScrollExtent); + } + + double _initialScrollOffset(double viewportWidth, double minExtent, double maxExtent) { + return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent); } void _scrollToCurrentIndex() { @@ -557,6 +613,7 @@ }); } + // Called each time layout completes. void _saveTabOffsets(List<double> tabOffsets) { _indicatorPainter?.tabOffsets = tabOffsets; } @@ -662,6 +719,7 @@ ); if (widget.isScrollable) { + _scrollController ??= new _TabBarScrollController(this); tabBar = new SingleChildScrollView( scrollDirection: Axis.horizontal, controller: _scrollController,
diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index 61e927c..d09ce96 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart
@@ -806,6 +806,33 @@ // Close enough to switch to page 2 pageController.jumpTo(800.0 - 0.75 * position.physics.tolerance.distance); expect(tabController.index, 2); + }); + testWidgets('Scrollable TabBar with a non-zero TabController initialIndex', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/9374 + + final List<Tab> tabs = new List<Tab>.generate(20, (int index) { + return new Tab(text: 'TAB #$index'); + }); + + final TabController controller = new TabController( + vsync: const TestVSync(), + length: tabs.length, + initialIndex: tabs.length - 1, + ); + + await tester.pumpWidget( + new Material( + child: new TabBar( + isScrollable: true, + controller: controller, + tabs: tabs, + ), + ), + ); + + // The initialIndex tab should be visible and right justified + expect(find.text('TAB #19'), findsOneWidget); + expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0); }); }