diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 3ab9d6c721..b76cb37708 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -411,29 +411,14 @@ class _IndicatorPainter extends CustomPainter { _needsPaint = false; _painter ??= indicator.createBoxPainter(markNeedsPaint); - if (controller.indexIsChanging) { - // The user tapped on a tab, the tab controller's animation is running. - final Rect targetRect = indicatorRect(size, controller.index); - _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller)); - } else { - // The user is dragging the TabBarView's PageView left or right. - final int currentIndex = controller.index; - final Rect? previous = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null; - final Rect middle = indicatorRect(size, currentIndex); - final Rect? next = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null; - final double index = controller.index.toDouble(); - final double value = controller.animation!.value; - if (value == index - 1.0) - _currentRect = previous ?? middle; - else if (value == index + 1.0) - _currentRect = next ?? middle; - else if (value == index) - _currentRect = middle; - else if (value < index) - _currentRect = previous == null ? middle : Rect.lerp(middle, previous, index - value); - else - _currentRect = next == null ? middle : Rect.lerp(middle, next, value - index); - } + final double index = controller.index.toDouble(); + final double value = controller.animation!.value; + final bool ltr = index > value; + final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex).toInt(); + final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex).toInt(); + final Rect fromRect = indicatorRect(size, from); + final Rect toRect = indicatorRect(size, to); + _currentRect = Rect.lerp(fromRect, toRect, (value - from).abs()); assert(_currentRect != null); final ImageConfiguration configuration = ImageConfiguration( diff --git a/packages/flutter/test/material/tabs_test.dart b/packages/flutter/test/material/tabs_test.dart index a0ed498667..99d8df60cc 100644 --- a/packages/flutter/test/material/tabs_test.dart +++ b/packages/flutter/test/material/tabs_test.dart @@ -1955,11 +1955,10 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 500)); - // The x coordinates of p1 and p2 were derived empirically, not analytically. expect(tabBarBox, paints..line( strokeWidth: indicatorWeight, - p1: const Offset(2476.0, indicatorY), - p2: const Offset(2574.0, indicatorY), + p1: const Offset(4951.0, indicatorY), + p2: const Offset(5049.0, indicatorY), )); await tester.pump(const Duration(milliseconds: 501)); @@ -1974,6 +1973,82 @@ void main() { )); }); + testWidgets('Tab indicator animation test', (WidgetTester tester) async { + const double indicatorWeight = 8.0; + + final List tabs = List.generate(4, (int index) { + return Tab(text: 'Tab $index'); + }); + + final TabController controller = TabController( + vsync: const TestVSync(), + length: tabs.length, + ); + + await tester.pumpWidget( + boilerplate( + child: Container( + alignment: Alignment.topLeft, + child: TabBar( + indicatorWeight: indicatorWeight, + controller: controller, + tabs: tabs, + ), + ), + ), + ); + + final RenderBox tabBarBox = tester.firstRenderObject(find.byType(TabBar)); + + // Initial indicator position. + const double indicatorY = 54.0 - indicatorWeight / 2.0; + double indicatorLeft = indicatorWeight / 2.0; + double indicatorRight = 200.0 - (indicatorWeight / 2.0); + + expect(tabBarBox, paints..line( + strokeWidth: indicatorWeight, + p1: Offset(indicatorLeft, indicatorY), + p2: Offset(indicatorRight, indicatorY), + )); + + // Select tab 1. + controller.animateTo(1, duration: const Duration(milliseconds: 1000), curve: Curves.linear); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + indicatorLeft = 100.0 + indicatorWeight / 2.0; + indicatorRight = 300.0 - (indicatorWeight / 2.0); + + expect(tabBarBox, paints..line( + strokeWidth: indicatorWeight, + p1: Offset(indicatorLeft, indicatorY), + p2: Offset(indicatorRight, indicatorY), + )); + + // Select tab 2 when animation is running. + controller.animateTo(2, duration: const Duration(milliseconds: 1000), curve: Curves.linear); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + indicatorLeft = 250.0 + indicatorWeight / 2.0; + indicatorRight = 450.0 - (indicatorWeight / 2.0); + + expect(tabBarBox, paints..line( + strokeWidth: indicatorWeight, + p1: Offset(indicatorLeft, indicatorY), + p2: Offset(indicatorRight, indicatorY), + )); + + // Final indicator position. + await tester.pumpAndSettle(); + indicatorLeft = 400.0 + indicatorWeight / 2.0; + indicatorRight = 600.0 - (indicatorWeight / 2.0); + + expect(tabBarBox, paints..line( + strokeWidth: indicatorWeight, + p1: Offset(indicatorLeft, indicatorY), + p2: Offset(indicatorRight, indicatorY), + )); + }); + testWidgets('correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester);