Relands https://github.com/flutter/flutter/pull/162315 Removed animated sheet golden tests as they're not consistent for long animation tests. Rewritten the tests to be more precise using mock canvas checks. --- 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
This commit is contained in:
parent
157f71d4ad
commit
9263410375
@ -583,27 +583,10 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
_painter ??= indicator.createBoxPainter(markNeedsPaint);
|
_painter ??= indicator.createBoxPainter(markNeedsPaint);
|
||||||
|
|
||||||
final double value = controller.animation!.value;
|
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) {
|
_currentRect = switch (indicatorAnimation) {
|
||||||
TabIndicatorAnimation.linear => _currentRect,
|
TabIndicatorAnimation.linear => _applyLinearEffect(size: size, value: value),
|
||||||
TabIndicatorAnimation.elastic => _applyElasticEffect(fromRect, toRect, _currentRect!),
|
TabIndicatorAnimation.elastic => _applyElasticEffect(size: size, value: value),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert(_currentRect != null);
|
assert(_currentRect != null);
|
||||||
@ -625,6 +608,17 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
_painter!.paint(canvas, _currentRect!.topLeft, configuration);
|
_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).
|
// Ease out sine (decelerating).
|
||||||
double decelerateInterpolation(double fraction) {
|
double decelerateInterpolation(double fraction) {
|
||||||
return math.sin((fraction * math.pi) / 2.0);
|
return math.sin((fraction * math.pi) / 2.0);
|
||||||
@ -636,20 +630,38 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Applies the elastic effect to the indicator.
|
/// 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
|
// 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
|
// 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.
|
// dragging a [TabBarView], but it's still ok, to avoid unnecessary calculations.
|
||||||
if (controller.animation!.isCompleted) {
|
if (controller.animation!.isCompleted) {
|
||||||
return currentRect;
|
return rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
final double index = controller.index.toDouble();
|
|
||||||
final double value = controller.animation!.value;
|
|
||||||
final double tabChangeProgress;
|
final double tabChangeProgress;
|
||||||
|
|
||||||
if (controller.indexIsChanging) {
|
if (controller.indexIsChanging) {
|
||||||
double progressLeft = (index - value).abs();
|
|
||||||
final int tabsDelta = (controller.index - controller.previousIndex).abs();
|
final int tabsDelta = (controller.index - controller.previousIndex).abs();
|
||||||
if (tabsDelta != 0) {
|
if (tabsDelta != 0) {
|
||||||
progressLeft /= tabsDelta;
|
progressLeft /= tabsDelta;
|
||||||
@ -661,7 +673,7 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
|
|
||||||
// If the animation has finished, there is no need to apply the stretch effect.
|
// If the animation has finished, there is no need to apply the stretch effect.
|
||||||
if (tabChangeProgress == 1.0) {
|
if (tabChangeProgress == 1.0) {
|
||||||
return currentRect;
|
return rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
final double leftFraction;
|
final double leftFraction;
|
||||||
@ -698,7 +710,7 @@ class _IndicatorPainter extends CustomPainter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return Rect.fromLTRB(lerpRectLeft, currentRect.top, lerpRectRight, currentRect.bottom);
|
return Rect.fromLTRB(lerpRectLeft, rect.top, lerpRectRight, rect.bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user