diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index e537af3eab..5b26b64445 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -586,27 +586,10 @@ class _IndicatorPainter extends CustomPainter { _painter ??= indicator.createBoxPainter(markNeedsPaint); final double value = controller.animation!.value; - final int to = - controller.indexIsChanging - ? controller.index - : switch (textDirection) { - TextDirection.ltr => value.ceil(), - TextDirection.rtl => value.floor(), - }.clamp(0, maxTabIndex); - final int from = - controller.indexIsChanging - ? controller.previousIndex - : switch (textDirection) { - TextDirection.ltr => (to - 1), - TextDirection.rtl => (to + 1), - }.clamp(0, maxTabIndex); - final Rect toRect = indicatorRect(size, to); - final Rect fromRect = indicatorRect(size, from); - _currentRect = Rect.lerp(fromRect, toRect, (value - from).abs()); _currentRect = switch (indicatorAnimation) { - TabIndicatorAnimation.linear => _currentRect, - TabIndicatorAnimation.elastic => _applyElasticEffect(fromRect, toRect, _currentRect!), + TabIndicatorAnimation.linear => _applyLinearEffect(size: size, value: value), + TabIndicatorAnimation.elastic => _applyElasticEffect(size: size, value: value), }; assert(_currentRect != null); @@ -628,6 +611,17 @@ class _IndicatorPainter extends CustomPainter { _painter!.paint(canvas, _currentRect!.topLeft, configuration); } + /// Applies the linear effect to the indicator. + Rect? _applyLinearEffect({required Size size, required double value}) { + final double index = controller.index.toDouble(); + final bool ltr = index > value; + final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex); + final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex); + final Rect fromRect = indicatorRect(size, from); + final Rect toRect = indicatorRect(size, to); + return Rect.lerp(fromRect, toRect, (value - from).abs()); + } + // Ease out sine (decelerating). double decelerateInterpolation(double fraction) { return math.sin((fraction * math.pi) / 2.0); @@ -639,20 +633,38 @@ class _IndicatorPainter extends CustomPainter { } /// Applies the elastic effect to the indicator. - Rect _applyElasticEffect(Rect fromRect, Rect toRect, Rect currentRect) { + Rect? _applyElasticEffect({required Size size, required double value}) { + final double index = controller.index.toDouble(); + double progressLeft = (index - value).abs(); + + final int to = + progressLeft == 0.0 || !controller.indexIsChanging + ? switch (textDirection) { + TextDirection.ltr => value.ceil(), + TextDirection.rtl => value.floor(), + }.clamp(0, maxTabIndex) + : controller.index; + final int from = + progressLeft == 0.0 || !controller.indexIsChanging + ? switch (textDirection) { + TextDirection.ltr => (to - 1), + TextDirection.rtl => (to + 1), + }.clamp(0, maxTabIndex) + : controller.previousIndex; + final Rect toRect = indicatorRect(size, to); + final Rect fromRect = indicatorRect(size, from); + final Rect rect = Rect.lerp(fromRect, toRect, (value - from).abs())!; + // If the tab animation is completed, there is no need to stretch the indicator // This only works for the tab change animation via tab index, not when // dragging a [TabBarView], but it's still ok, to avoid unnecessary calculations. if (controller.animation!.isCompleted) { - return currentRect; + return rect; } - final double index = controller.index.toDouble(); - final double value = controller.animation!.value; final double tabChangeProgress; if (controller.indexIsChanging) { - double progressLeft = (index - value).abs(); final int tabsDelta = (controller.index - controller.previousIndex).abs(); if (tabsDelta != 0) { progressLeft /= tabsDelta; @@ -664,7 +676,7 @@ class _IndicatorPainter extends CustomPainter { // If the animation has finished, there is no need to apply the stretch effect. if (tabChangeProgress == 1.0) { - return currentRect; + return rect; } final double leftFraction; @@ -701,7 +713,7 @@ class _IndicatorPainter extends CustomPainter { }; } - return Rect.fromLTRB(lerpRectLeft, currentRect.top, lerpRectRight, currentRect.bottom); + return Rect.fromLTRB(lerpRectLeft, rect.top, lerpRectRight, rect.bottom); } @override diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index e49bb6471c..23feb4303c 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -3753,10 +3753,8 @@ void main() { tabBarBox, paints..line( strokeWidth: indicatorWeight, - // In RTL, the elastic tab animation expands the width of the tab with a negative offset - // when jumping from the first tab to the last tab in a scrollable tab bar. - p1: const Offset(-480149, indicatorY), - p2: const Offset(-480051, indicatorY), + p1: const Offset(4951.0, indicatorY), + p2: const Offset(5049.0, indicatorY), ), ); @@ -7824,9 +7822,14 @@ void main() { addTearDown(animationSheet.dispose); final List tabs = [ - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), + const Tab(text: 'Short'), + const Tab(text: 'A Bit Longer Text'), + const Tab(text: 'An Extremely Long Tab Label That Overflows'), + const Tab(text: 'Tiny'), + const Tab(text: 'Moderate Length'), + const Tab(text: 'Just Right'), + const Tab(text: 'Supercalifragilisticexpialidocious'), + const Tab(text: 'Longer Than Usual'), ]; final TabController controller = createTabController( @@ -7834,7 +7837,7 @@ void main() { length: tabs.length, ); - Widget target() { + Widget buildTabBar() { return animationSheet.record( boilerplate( child: Container( @@ -7849,13 +7852,19 @@ void main() { ); } - await tester.pumpFrames(target(), const Duration(milliseconds: 50)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); - await tester.tap(find.text('Extremely Very Long Label')); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); + await tester.tap(find.byType(Tab).at(2)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); - await tester.tap(find.text('C')); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); + await tester.tap(find.byType(Tab).at(3)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(5)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(4)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); await expectLater( animationSheet.collate(1), @@ -7863,6 +7872,179 @@ void main() { ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + testWidgets('Elastic Tab animation with various size tabs - RTL', (WidgetTester tester) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder( + frameSize: const Size(800, 100), + ); + addTearDown(animationSheet.dispose); + + final List tabs = [ + const Tab(text: 'Short'), + const Tab(text: 'A Bit Longer Text'), + const Tab(text: 'An Extremely Long Tab Label That Overflows'), + const Tab(text: 'Tiny'), + const Tab(text: 'Moderate Length'), + const Tab(text: 'Just Right'), + const Tab(text: 'Supercalifragilisticexpialidocious'), + const Tab(text: 'Longer Than Usual'), + ]; + + final TabController controller = createTabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + Widget buildTabBar() { + return animationSheet.record( + boilerplate( + textDirection: TextDirection.rtl, + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicatorAnimation: TabIndicatorAnimation.elastic, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + } + + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); + + await tester.tap(find.byType(Tab).at(2)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(3)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(5)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(4)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await expectLater( + animationSheet.collate(1), + matchesGoldenFile('tab_indicator.elastic_animation.various_size_tabs.rtl.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + + testWidgets('Linear Tab animation with various size tabs - LTR', (WidgetTester tester) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder( + frameSize: const Size(800, 100), + ); + addTearDown(animationSheet.dispose); + + final List tabs = [ + const Tab(text: 'Short'), + const Tab(text: 'A Bit Longer Text'), + const Tab(text: 'An Extremely Long Tab Label That Overflows'), + const Tab(text: 'Tiny'), + const Tab(text: 'Moderate Length'), + const Tab(text: 'Just Right'), + const Tab(text: 'Supercalifragilisticexpialidocious'), + const Tab(text: 'Longer Than Usual'), + ]; + + final TabController controller = createTabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + Widget buildTabBar() { + return animationSheet.record( + boilerplate( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicatorAnimation: TabIndicatorAnimation.linear, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + } + + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); + + await tester.tap(find.byType(Tab).at(2)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(3)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(5)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(4)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await expectLater( + animationSheet.collate(1), + matchesGoldenFile('tab_indicator.linear_animation.various_size_tabs.ltr.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + + testWidgets('Linear Tab animation with various size tabs - RTL', (WidgetTester tester) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder( + frameSize: const Size(800, 100), + ); + addTearDown(animationSheet.dispose); + + final List tabs = [ + const Tab(text: 'Short'), + const Tab(text: 'A Bit Longer Text'), + const Tab(text: 'An Extremely Long Tab Label That Overflows'), + const Tab(text: 'Tiny'), + const Tab(text: 'Moderate Length'), + const Tab(text: 'Just Right'), + const Tab(text: 'Supercalifragilisticexpialidocious'), + const Tab(text: 'Longer Than Usual'), + ]; + + final TabController controller = createTabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + Widget buildTabBar() { + return animationSheet.record( + boilerplate( + textDirection: TextDirection.rtl, + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicatorAnimation: TabIndicatorAnimation.linear, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + } + + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); + + await tester.tap(find.byType(Tab).at(2)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(3)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(5)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(4)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await expectLater( + animationSheet.collate(1), + matchesGoldenFile('tab_indicator.linear_animation.various_size_tabs.rtl.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + testWidgets('Elastic Tab animation with various size tabs in a scrollable tab bar - LTR', ( WidgetTester tester, ) async { @@ -7872,18 +8054,14 @@ void main() { addTearDown(animationSheet.dispose); final List tabs = [ - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), + const Tab(text: 'Short'), + const Tab(text: 'A Bit Longer Text'), + const Tab(text: 'An Extremely Long Tab Label That Overflows'), + const Tab(text: 'Tiny'), + const Tab(text: 'Moderate Length'), + const Tab(text: 'Just Right'), + const Tab(text: 'Supercalifragilisticexpialidocious'), + const Tab(text: 'Longer Than Usual'), ]; final TabController controller = createTabController( @@ -7891,14 +8069,14 @@ void main() { length: tabs.length, ); - Widget target() { + Widget buildTabBar() { return animationSheet.record( boilerplate( child: Container( alignment: Alignment.topLeft, child: TabBar( - indicatorAnimation: TabIndicatorAnimation.elastic, isScrollable: true, + indicatorAnimation: TabIndicatorAnimation.elastic, controller: controller, tabs: tabs, ), @@ -7907,13 +8085,22 @@ void main() { ); } - await tester.pumpFrames(target(), const Duration(milliseconds: 50)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); + + await tester.tap(find.byType(Tab).at(2)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(1)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); controller.animateTo(tabs.length - 1); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); controller.animateTo(0); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(1)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); await expectLater( animationSheet.collate(1), @@ -7921,53 +8108,6 @@ void main() { ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 - testWidgets('Elastic Tab animation with various size tabs - RTL', (WidgetTester tester) async { - final AnimationSheetBuilder animationSheet = AnimationSheetBuilder( - frameSize: const Size(800, 100), - ); - addTearDown(animationSheet.dispose); - - final List tabs = [ - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), - ]; - - final TabController controller = createTabController( - vsync: const TestVSync(), - length: tabs.length, - ); - - Widget target() { - return animationSheet.record( - boilerplate( - textDirection: TextDirection.rtl, - child: Container( - alignment: Alignment.topLeft, - child: TabBar( - indicatorAnimation: TabIndicatorAnimation.elastic, - controller: controller, - tabs: tabs, - ), - ), - ), - ); - } - - await tester.pumpFrames(target(), const Duration(milliseconds: 50)); - - await tester.tap(find.text('Extremely Very Long Label')); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); - - await tester.tap(find.text('C')); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); - - await expectLater( - animationSheet.collate(1), - matchesGoldenFile('tab_indicator.elastic_animation.various_size_tabs.rtl.png'), - ); - }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 - testWidgets('Elastic Tab animation with various size tabs in a scrollable tab bar - RTL', ( WidgetTester tester, ) async { @@ -7977,18 +8117,14 @@ void main() { addTearDown(animationSheet.dispose); final List tabs = [ - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), + const Tab(text: 'Short'), + const Tab(text: 'A Bit Longer Text'), + const Tab(text: 'An Extremely Long Tab Label That Overflows'), + const Tab(text: 'Tiny'), + const Tab(text: 'Moderate Length'), + const Tab(text: 'Just Right'), + const Tab(text: 'Supercalifragilisticexpialidocious'), + const Tab(text: 'Longer Than Usual'), ]; final TabController controller = createTabController( @@ -7996,15 +8132,15 @@ void main() { length: tabs.length, ); - Widget target() { + Widget buildTabBar() { return animationSheet.record( boilerplate( textDirection: TextDirection.rtl, child: Container( alignment: Alignment.topLeft, child: TabBar( - indicatorAnimation: TabIndicatorAnimation.elastic, isScrollable: true, + indicatorAnimation: TabIndicatorAnimation.elastic, controller: controller, tabs: tabs, ), @@ -8013,13 +8149,22 @@ void main() { ); } - await tester.pumpFrames(target(), const Duration(milliseconds: 50)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); + + await tester.tap(find.byType(Tab).at(2)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(1)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); controller.animateTo(tabs.length - 1); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); controller.animateTo(0); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(1)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); await expectLater( animationSheet.collate(1), @@ -8027,6 +8172,133 @@ void main() { ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + testWidgets('Linear Tab animation with various size tabs in a scrollable tab bar - LTR', ( + WidgetTester tester, + ) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder( + frameSize: const Size(800, 100), + ); + addTearDown(animationSheet.dispose); + + final List tabs = [ + const Tab(text: 'Short'), + const Tab(text: 'A Bit Longer Text'), + const Tab(text: 'An Extremely Long Tab Label That Overflows'), + const Tab(text: 'Tiny'), + const Tab(text: 'Moderate Length'), + const Tab(text: 'Just Right'), + const Tab(text: 'Supercalifragilisticexpialidocious'), + const Tab(text: 'Longer Than Usual'), + ]; + + final TabController controller = createTabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + Widget buildTabBar() { + return animationSheet.record( + boilerplate( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + isScrollable: true, + indicatorAnimation: TabIndicatorAnimation.linear, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + } + + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); + + await tester.tap(find.byType(Tab).at(2)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(1)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + controller.animateTo(tabs.length - 1); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + controller.animateTo(0); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(1)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await expectLater( + animationSheet.collate(1), + matchesGoldenFile('tab_indicator.linear_animation.various_size_tabs.scrollable.ltr.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + + testWidgets('Linear Tab animation with various size tabs in a scrollable tab bar - RTL', ( + WidgetTester tester, + ) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder( + frameSize: const Size(800, 100), + ); + addTearDown(animationSheet.dispose); + + final List tabs = [ + const Tab(text: 'Short'), + const Tab(text: 'A Bit Longer Text'), + const Tab(text: 'An Extremely Long Tab Label That Overflows'), + const Tab(text: 'Tiny'), + const Tab(text: 'Moderate Length'), + const Tab(text: 'Just Right'), + const Tab(text: 'Supercalifragilisticexpialidocious'), + const Tab(text: 'Longer Than Usual'), + ]; + + final TabController controller = createTabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + Widget buildTabBar() { + return animationSheet.record( + boilerplate( + textDirection: TextDirection.rtl, + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + isScrollable: true, + indicatorAnimation: TabIndicatorAnimation.linear, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + } + + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); + + await tester.tap(find.byType(Tab).at(2)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(1)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + controller.animateTo(tabs.length - 1); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + controller.animateTo(0); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.byType(Tab).at(1)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await expectLater( + animationSheet.collate(1), + matchesGoldenFile('tab_indicator.linear_animation.various_size_tabs.scrollable.rtl.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + // Regression test for https://github.com/flutter/flutter/issues/160631 testWidgets('Elastic Tab animation when skipping tabs', (WidgetTester tester) async { final AnimationSheetBuilder animationSheet = AnimationSheetBuilder( @@ -8034,19 +8306,14 @@ void main() { ); addTearDown(animationSheet.dispose); - final List tabs = [ - const Tab(text: 'Medium'), - const Tab(text: 'Extremely Very Long Label'), - const Tab(text: 'C'), - const Tab(text: 'Short'), - ]; + final List tabs = List.generate(10, (int index) => Tab(text: 'Tab $index')); final TabController controller = createTabController( vsync: const TestVSync(), length: tabs.length, ); - Widget target() { + Widget buildTabBar() { return animationSheet.record( boilerplate( child: Container( @@ -8061,17 +8328,72 @@ void main() { ); } - await tester.pumpFrames(target(), const Duration(milliseconds: 50)); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); - await tester.tap(find.text('C')); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); + await tester.tap(find.text('Tab 2')); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); - await tester.tap(find.text('Medium')); - await tester.pumpFrames(target(), const Duration(milliseconds: 500)); + await tester.tap(find.text('Tab 1')); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.text('Tab 4')); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.text('Tab 5')); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); await expectLater( animationSheet.collate(1), matchesGoldenFile('tab_indicator.elastic_animation.skipping_tabs.png'), ); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 + + // Regression test for https://github.com/flutter/flutter/issues/162098 + testWidgets('Linear Tab animation when skipping tab', (WidgetTester tester) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder( + frameSize: const Size(800, 100), + ); + addTearDown(animationSheet.dispose); + + final List tabs = List.generate(10, (int index) => Tab(text: 'Tab $index')); + + final TabController controller = createTabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + Widget buildTabBar() { + return animationSheet.record( + boilerplate( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicatorAnimation: TabIndicatorAnimation.linear, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + } + + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50)); + + await tester.tap(find.text('Tab 2')); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.text('Tab 1')); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.text('Tab 4')); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await tester.tap(find.text('Tab 5')); + await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500)); + + await expectLater( + animationSheet.collate(1), + matchesGoldenFile('tab_indicator.linear_animation.skipping_tabs.png'), + ); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001 }