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