<!-- start_original_pr_link --> Reverts: flutter/flutter#162315 <!-- end_original_pr_link --> <!-- start_initiating_author --> Initiated by: TahaTesser <!-- end_initiating_author --> <!-- start_revert_reason --> Reason for reverting: Red tree due to unapproved golden images. <!-- end_revert_reason --> <!-- start_original_pr_author --> Original PR Author: TahaTesser <!-- end_original_pr_author --> <!-- start_reviewers --> Reviewed By: {justinmc} <!-- end_reviewers --> <!-- start_revert_body --> This change reverts the following previous change: Fixes [https://github.com/flutter/flutter/issues/162098](https://github.com/flutter/flutter/issues/162098) ### Description This PR fixes `Tab` linear and elastic animation blinks/flickers when skipping multiple tabs. Previous attempt to fix elastic animation didn't cover linear animation tests and didn't have enough number of tab items which this PR fixes. - Fixed Linear and elastic animation blink issue. - Added tests for linear and elastic animation with various tab sizes (LTR and RTL) - Added tests for linear and elastic animation when skipping tabs (LTR and RTL) ### Code Sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; // import 'package:flutter/scheduler.dart'; void main() { // timeDilation = 10; runApp(const TabBarDemo()); } class TabBarDemo extends StatelessWidget { const TabBarDemo({super.key}); @override Widget build(BuildContext context) { final List<Widget> tabs = <Widget>[ 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'), ]; return MaterialApp( home: DefaultTabController( length: tabs.length, child: Scaffold( appBar: AppBar( bottom: TabBar( tabAlignment: TabAlignment.start, isScrollable: true, indicatorAnimation: TabIndicatorAnimation.elastic, tabs: tabs, ), title: const Text('Tabs Demo'), ), body: TabBarView( children: <Widget>[ for (int i = 0; i < tabs.length; i++) const Icon(Icons.directions_car), ], ), ), ), ); } } ``` </details> ### Before https://github.com/user-attachments/assets/5c271948-5a01-4520-90a3-921c20c79470 ### After https://github.com/user-attachments/assets/6af32d43-3588-488f-ba50-be59323ed692 ### Linear animation before (left) and After (right) comparison. <img width="1048" alt="Screenshot 2025-01-28 at 17 27 50" src="https://github.com/user-attachments/assets/4ba587a5-24d0-40ce-817c-366d004abc05" /> ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md <!-- end_revert_body --> Co-authored-by: auto-submit[bot] <flutter-engprod-team@google.com>
This commit is contained in:
parent
b007899d3a
commit
2c1e4b1615
@ -586,10 +586,27 @@ 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 => _applyLinearEffect(size: size, value: value),
|
||||
TabIndicatorAnimation.elastic => _applyElasticEffect(size: size, value: value),
|
||||
TabIndicatorAnimation.linear => _currentRect,
|
||||
TabIndicatorAnimation.elastic => _applyElasticEffect(fromRect, toRect, _currentRect!),
|
||||
};
|
||||
|
||||
assert(_currentRect != null);
|
||||
@ -611,17 +628,6 @@ 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);
|
||||
@ -633,38 +639,20 @@ class _IndicatorPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
/// Applies the elastic effect to the indicator.
|
||||
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())!;
|
||||
|
||||
Rect _applyElasticEffect(Rect fromRect, Rect toRect, Rect currentRect) {
|
||||
// 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 rect;
|
||||
return currentRect;
|
||||
}
|
||||
|
||||
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;
|
||||
@ -676,7 +664,7 @@ class _IndicatorPainter extends CustomPainter {
|
||||
|
||||
// If the animation has finished, there is no need to apply the stretch effect.
|
||||
if (tabChangeProgress == 1.0) {
|
||||
return rect;
|
||||
return currentRect;
|
||||
}
|
||||
|
||||
final double leftFraction;
|
||||
@ -713,7 +701,7 @@ class _IndicatorPainter extends CustomPainter {
|
||||
};
|
||||
}
|
||||
|
||||
return Rect.fromLTRB(lerpRectLeft, rect.top, lerpRectRight, rect.bottom);
|
||||
return Rect.fromLTRB(lerpRectLeft, currentRect.top, lerpRectRight, currentRect.bottom);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -3753,8 +3753,10 @@ void main() {
|
||||
tabBarBox,
|
||||
paints..line(
|
||||
strokeWidth: indicatorWeight,
|
||||
p1: const Offset(4951.0, indicatorY),
|
||||
p2: const Offset(5049.0, indicatorY),
|
||||
// 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),
|
||||
),
|
||||
);
|
||||
|
||||
@ -7822,14 +7824,9 @@ void main() {
|
||||
addTearDown(animationSheet.dispose);
|
||||
|
||||
final List<Widget> tabs = <Widget>[
|
||||
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'),
|
||||
const Tab(text: 'Medium'),
|
||||
const Tab(text: 'Extremely Very Long Label'),
|
||||
const Tab(text: 'C'),
|
||||
];
|
||||
|
||||
final TabController controller = createTabController(
|
||||
@ -7837,7 +7834,7 @@ void main() {
|
||||
length: tabs.length,
|
||||
);
|
||||
|
||||
Widget buildTabBar() {
|
||||
Widget target() {
|
||||
return animationSheet.record(
|
||||
boilerplate(
|
||||
child: Container(
|
||||
@ -7852,19 +7849,13 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50));
|
||||
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
|
||||
|
||||
await tester.tap(find.byType(Tab).at(2));
|
||||
await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500));
|
||||
await tester.tap(find.text('Extremely Very Long Label'));
|
||||
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 tester.tap(find.text('C'));
|
||||
await tester.pumpFrames(target(), const Duration(milliseconds: 500));
|
||||
|
||||
await expectLater(
|
||||
animationSheet.collate(1),
|
||||
@ -7872,179 +7863,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<Widget> tabs = <Widget>[
|
||||
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<Widget> tabs = <Widget>[
|
||||
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<Widget> tabs = <Widget>[
|
||||
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 {
|
||||
@ -8054,14 +7872,18 @@ void main() {
|
||||
addTearDown(animationSheet.dispose);
|
||||
|
||||
final List<Widget> tabs = <Widget>[
|
||||
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'),
|
||||
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'),
|
||||
];
|
||||
|
||||
final TabController controller = createTabController(
|
||||
@ -8069,13 +7891,60 @@ void main() {
|
||||
length: tabs.length,
|
||||
);
|
||||
|
||||
Widget buildTabBar() {
|
||||
Widget target() {
|
||||
return animationSheet.record(
|
||||
boilerplate(
|
||||
child: Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child: TabBar(
|
||||
indicatorAnimation: TabIndicatorAnimation.elastic,
|
||||
isScrollable: true,
|
||||
controller: controller,
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
|
||||
|
||||
controller.animateTo(tabs.length - 1);
|
||||
await tester.pumpFrames(target(), const Duration(milliseconds: 500));
|
||||
|
||||
controller.animateTo(0);
|
||||
await tester.pumpFrames(target(), const Duration(milliseconds: 500));
|
||||
|
||||
await expectLater(
|
||||
animationSheet.collate(1),
|
||||
matchesGoldenFile('tab_indicator.elastic_animation.various_size_tabs.scrollable.ltr.png'),
|
||||
);
|
||||
}, 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<Widget> tabs = <Widget>[
|
||||
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,
|
||||
@ -8085,26 +7954,17 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50));
|
||||
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
|
||||
|
||||
await tester.tap(find.byType(Tab).at(2));
|
||||
await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500));
|
||||
await tester.tap(find.text('Extremely Very Long Label'));
|
||||
await tester.pumpFrames(target(), 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 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.scrollable.ltr.png'),
|
||||
matchesGoldenFile('tab_indicator.elastic_animation.various_size_tabs.rtl.png'),
|
||||
);
|
||||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
|
||||
|
||||
@ -8117,14 +7977,18 @@ void main() {
|
||||
addTearDown(animationSheet.dispose);
|
||||
|
||||
final List<Widget> tabs = <Widget>[
|
||||
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'),
|
||||
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'),
|
||||
];
|
||||
|
||||
final TabController controller = createTabController(
|
||||
@ -8132,15 +7996,15 @@ void main() {
|
||||
length: tabs.length,
|
||||
);
|
||||
|
||||
Widget buildTabBar() {
|
||||
Widget target() {
|
||||
return animationSheet.record(
|
||||
boilerplate(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Container(
|
||||
alignment: Alignment.topLeft,
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
indicatorAnimation: TabIndicatorAnimation.elastic,
|
||||
isScrollable: true,
|
||||
controller: controller,
|
||||
tabs: tabs,
|
||||
),
|
||||
@ -8149,22 +8013,13 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
|
||||
|
||||
controller.animateTo(tabs.length - 1);
|
||||
await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 500));
|
||||
await tester.pumpFrames(target(), 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 tester.pumpFrames(target(), const Duration(milliseconds: 500));
|
||||
|
||||
await expectLater(
|
||||
animationSheet.collate(1),
|
||||
@ -8172,133 +8027,6 @@ 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<Widget> tabs = <Widget>[
|
||||
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<Widget> tabs = <Widget>[
|
||||
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(
|
||||
@ -8306,14 +8034,19 @@ void main() {
|
||||
);
|
||||
addTearDown(animationSheet.dispose);
|
||||
|
||||
final List<Widget> tabs = List<Widget>.generate(10, (int index) => Tab(text: 'Tab $index'));
|
||||
final List<Widget> tabs = <Widget>[
|
||||
const Tab(text: 'Medium'),
|
||||
const Tab(text: 'Extremely Very Long Label'),
|
||||
const Tab(text: 'C'),
|
||||
const Tab(text: 'Short'),
|
||||
];
|
||||
|
||||
final TabController controller = createTabController(
|
||||
vsync: const TestVSync(),
|
||||
length: tabs.length,
|
||||
);
|
||||
|
||||
Widget buildTabBar() {
|
||||
Widget target() {
|
||||
return animationSheet.record(
|
||||
boilerplate(
|
||||
child: Container(
|
||||
@ -8328,72 +8061,17 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpFrames(buildTabBar(), const Duration(milliseconds: 50));
|
||||
await tester.pumpFrames(target(), const Duration(milliseconds: 50));
|
||||
|
||||
await tester.tap(find.text('Tab 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.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 tester.tap(find.text('Medium'));
|
||||
await tester.pumpFrames(target(), 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<Widget> tabs = List<Widget>.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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user