From a9f554ac393584a46afd3094514857c33f0f293e Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Tue, 18 Jun 2024 11:30:21 +0300 Subject: [PATCH] Fix scrollable `TabBar` jittering (#150041) fixes [TabBar with isScrollable set to true is broken](https://github.com/flutter/flutter/issues/150000) This regressed due to a tiny mistake in https://github.com/flutter/flutter/pull/146293 ### Code sample
expand to view the code sample ```dart import 'dart:ui'; import 'package:flutter/material.dart'; /// Flutter code sample for [TabBar]. void main() => runApp(const TabBarApp()); class TabBarApp extends StatelessWidget { const TabBarApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: TabBarExample(), ); } } class TabBarExample extends StatelessWidget { const TabBarExample({super.key}); @override Widget build(BuildContext context) { final List tabs = List.generate(20, (int index) => Tab(text: 'Tab $index')); return ScrollConfiguration( behavior: ScrollConfiguration.of(context) .copyWith(dragDevices: { PointerDeviceKind.touch, PointerDeviceKind.mouse, }), child: DefaultTabController( length: tabs.length, child: Scaffold( appBar: AppBar( title: const Text('TabBar Sample'), bottom: TabBar( isScrollable: true, tabs: tabs, tabAlignment: TabAlignment.start, ), ), body: TabBarView( children: [ for (int i = 0; i < tabs.length; i++) Center( child: Text('Page $i'), ), ], ), ), ), ); } } ```
### Before https://github.com/flutter/flutter/assets/48603081/b7aa98a2-a6a5-431e-8327-859a11efa129 ### After https://github.com/flutter/flutter/assets/48603081/0435719f-03d4-4d76-8b5a-532894fcf4a3 --- packages/flutter/lib/src/material/tabs.dart | 11 +-- packages/flutter/test/material/tabs_test.dart | 92 +++++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index e20ab579d27a..7ed44d0e9d0c 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -1549,12 +1549,11 @@ class _TabBarState extends State { final double index = _controller!.index.toDouble(); final double value = _controller!.animation!.value; final double offset = switch (value - index) { - -1.0 || 1.0 => leadingPosition ?? middlePosition, - 0 => middlePosition, - < 0 when leadingPosition == null => middlePosition, - > 0 when trailingPosition == null => middlePosition, - < 0 => lerpDouble(middlePosition, leadingPosition, index - value)!, - _ => lerpDouble(middlePosition, trailingPosition, value - index)!, + -1.0 => leadingPosition ?? middlePosition, + 1.0 => trailingPosition ?? middlePosition, + 0 => middlePosition, + < 0 => leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value)!, + _ => trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index)!, }; _scrollController!.jumpTo(offset); diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index d0b66dce9016..a3f6284bf405 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -7158,4 +7158,96 @@ void main() { labelSize = tester.getSize(find.text('Tab 1')); expect(labelSize, equals(const Size(140.5, 40.0))); }, skip: isBrowser && !isSkiaWeb); // https://github.com/flutter/flutter/issues/87543 + + // This is a regression test for https://github.com/flutter/flutter/issues/150000. + testWidgets('Scrollable TabBar does not jitter in the middle position', (WidgetTester tester) async { + final List tabs = List.generate(20, (int index) => 'Tab $index'); + + await tester.pumpWidget(MaterialApp( + home: DefaultTabController( + length: tabs.length, + initialIndex: 10, + child: Scaffold( + appBar: AppBar( + bottom: TabBar( + isScrollable: true, + tabs: tabs.map((String tab) => Tab(text: tab)).toList(), + ), + ), + body: TabBarView( + children: [ + for (int i = 0; i < tabs.length; i++) + Center( + child: Text('Page $i'), + ), + ], + ), + ), + ), + )); + + final SingleChildScrollView scrollable = tester.widget(find.byType(SingleChildScrollView)); + expect(find.text('Page 10'), findsOneWidget); + expect(find.text('Page 11'), findsNothing); + expect(scrollable.controller!.position.pixels, closeTo(683.2, 0.1)); + + // Drag the TabBarView to the left. + await tester.drag(find.byType(TabBarView), const Offset(-800, 0)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.text('Page 10'), findsNothing); + expect(find.text('Page 11'), findsOneWidget); + expect(scrollable.controller!.position.pixels, closeTo(799.8, 0.1)); + }); + + // This is a regression test for https://github.com/flutter/flutter/issues/150000. + testWidgets('Scrollable TabBar does not jitter when the tab bar reaches the start', (WidgetTester tester) async { + final List tabs = List.generate(20, (int index) => 'Tab $index'); + + await tester.pumpWidget(MaterialApp( + home: DefaultTabController( + length: tabs.length, + initialIndex: 4, + child: Scaffold( + appBar: AppBar( + bottom: TabBar( + isScrollable: true, + tabs: tabs.map((String tab) => Tab(text: tab)).toList(), + ), + ), + body: TabBarView( + children: [ + for (int i = 0; i < tabs.length; i++) + Center( + child: Text('Page $i'), + ), + ], + ), + ), + ), + )); + + final SingleChildScrollView scrollable = tester.widget(find.byType(SingleChildScrollView)); + + expect(find.text('Page 4'), findsOneWidget); + expect(find.text('Page 3'), findsNothing); + expect(scrollable.controller!.position.pixels, closeTo(61.25, 0.1)); + + // Drag the TabBarView to the right. + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Page 4'))); + await gesture.moveBy(const Offset(600.0, 0.0)); + await gesture.up(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.text('Page 4'), findsOneWidget); + expect(find.text('Page 3'), findsOneWidget); + expect(scrollable.controller!.position.pixels, closeTo(0.2, 0.1)); + + await tester.pumpAndSettle(); + expect(find.text('Page 4'), findsNothing); + expect(find.text('Page 3'), findsOneWidget); + expect(scrollable.controller!.position.pixels, equals(0.0)); + }); }