From e5bd2b3d556ab393e93167070edefe11122c5d39 Mon Sep 17 00:00:00 2001 From: Coin Date: Sun, 17 May 2020 05:47:03 +0800 Subject: [PATCH] Make CircularProgressIndicator's animation match native (#50412) --- .../lib/src/material/progress_indicator.dart | 66 +++++++++---------- .../material/progress_indicator_test.dart | 24 +++++++ 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/packages/flutter/lib/src/material/progress_indicator.dart b/packages/flutter/lib/src/material/progress_indicator.dart index 3f6d51b941f..c776d117336 100644 --- a/packages/flutter/lib/src/material/progress_indicator.dart +++ b/packages/flutter/lib/src/material/progress_indicator.dart @@ -12,6 +12,7 @@ import 'theme.dart'; const double _kMinCircularProgressIndicatorSize = 36.0; const int _kIndeterminateLinearDuration = 1800; +const int _kIndeterminateCircularDuration = 1333 * 2222; /// A base class for material design progress indicators. /// @@ -331,12 +332,12 @@ class _CircularProgressIndicatorPainter extends CustomPainter { this.value, this.headValue, this.tailValue, - this.stepValue, + this.offsetValue, this.rotationValue, this.strokeWidth, }) : arcStart = value != null ? _startAngle - : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 1.7 - stepValue * 0.8 * math.pi, + : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 2.0 + offsetValue * 0.5 * math.pi, arcSweep = value != null ? (value.clamp(0.0, 1.0) as double) * _sweep : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon); @@ -346,7 +347,7 @@ class _CircularProgressIndicatorPainter extends CustomPainter { final double value; final double headValue; final double tailValue; - final int stepValue; + final double offsetValue; final double rotationValue; final double strokeWidth; final double arcStart; @@ -385,7 +386,7 @@ class _CircularProgressIndicatorPainter extends CustomPainter { || oldPainter.value != value || oldPainter.headValue != headValue || oldPainter.tailValue != tailValue - || oldPainter.stepValue != stepValue + || oldPainter.offsetValue != offsetValue || oldPainter.rotationValue != rotationValue || oldPainter.strokeWidth != strokeWidth; } @@ -443,31 +444,30 @@ class CircularProgressIndicator extends ProgressIndicator { _CircularProgressIndicatorState createState() => _CircularProgressIndicatorState(); } -// Tweens used by circular progress indicator -final Animatable _kStrokeHeadTween = CurveTween( - curve: const Interval(0.0, 0.5, curve: Curves.fastOutSlowIn), -).chain(CurveTween( - curve: const SawTooth(5), -)); - -final Animatable _kStrokeTailTween = CurveTween( - curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), -).chain(CurveTween( - curve: const SawTooth(5), -)); - -final Animatable _kStepTween = StepTween(begin: 0, end: 5); - -final Animatable _kRotationTween = CurveTween(curve: const SawTooth(5)); - class _CircularProgressIndicatorState extends State with SingleTickerProviderStateMixin { + static const int _pathCount = _kIndeterminateCircularDuration ~/ 1333; + static const int _rotationCount = _kIndeterminateCircularDuration ~/ 2222; + + static final Animatable _strokeHeadTween = CurveTween( + curve: const Interval(0.0, 0.5, curve: Curves.fastOutSlowIn), + ).chain(CurveTween( + curve: const SawTooth(_pathCount), + )); + static final Animatable _strokeTailTween = CurveTween( + curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), + ).chain(CurveTween( + curve: const SawTooth(_pathCount), + )); + static final Animatable _offsetTween = CurveTween(curve: const SawTooth(_pathCount)); + static final Animatable _rotationTween = CurveTween(curve: const SawTooth(_rotationCount)); + AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( - duration: const Duration(seconds: 5), + duration: const Duration(milliseconds: _kIndeterminateCircularDuration), vsync: this, ); if (widget.value == null) @@ -489,7 +489,7 @@ class _CircularProgressIndicatorState extends State w super.dispose(); } - Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) { + Widget _buildIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) { return widget._buildSemanticsWrapper( context: context, child: Container( @@ -504,7 +504,7 @@ class _CircularProgressIndicatorState extends State w value: widget.value, // may be null headValue: headValue, // remaining arguments are ignored if widget.value is not null tailValue: tailValue, - stepValue: stepValue, + offsetValue: offsetValue, rotationValue: rotationValue, strokeWidth: widget.strokeWidth, ), @@ -519,10 +519,10 @@ class _CircularProgressIndicatorState extends State w builder: (BuildContext context, Widget child) { return _buildIndicator( context, - _kStrokeHeadTween.evaluate(_controller), - _kStrokeTailTween.evaluate(_controller), - _kStepTween.evaluate(_controller), - _kRotationTween.evaluate(_controller), + _strokeHeadTween.evaluate(_controller), + _strokeTailTween.evaluate(_controller), + _offsetTween.evaluate(_controller), + _rotationTween.evaluate(_controller), ); }, ); @@ -542,7 +542,7 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter double value, double headValue, double tailValue, - int stepValue, + double offsetValue, double rotationValue, double strokeWidth, this.arrowheadScale, @@ -551,7 +551,7 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter value: value, headValue: headValue, tailValue: tailValue, - stepValue: stepValue, + offsetValue: offsetValue, rotationValue: rotationValue, strokeWidth: strokeWidth, ); @@ -645,14 +645,14 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { @override Widget build(BuildContext context) { if (widget.value != null) - _controller.value = widget.value / 10.0; + _controller.value = widget.value * (1333 / 2 / _kIndeterminateCircularDuration); else if (!_controller.isAnimating) _controller.repeat(); return _buildAnimation(); } @override - Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) { + Widget _buildIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) { final double arrowheadScale = widget.value == null ? 0.0 : ((widget.value * 2.0).clamp(0.0, 1.0) as double); return widget._buildSemanticsWrapper( context: context, @@ -672,7 +672,7 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { value: null, // Draw the indeterminate progress indicator. headValue: headValue, tailValue: tailValue, - stepValue: stepValue, + offsetValue: offsetValue, rotationValue: rotationValue, strokeWidth: widget.strokeWidth, arrowheadScale: arrowheadScale, diff --git a/packages/flutter/test/material/progress_indicator_test.dart b/packages/flutter/test/material/progress_indicator_test.dart index 9742e8af934..663a8b0877f 100644 --- a/packages/flutter/test/material/progress_indicator_test.dart +++ b/packages/flutter/test/material/progress_indicator_test.dart @@ -530,4 +530,28 @@ void main() { handle.dispose(); }); + + testWidgets('Indeterminate CircularProgressIndicator uses expected animation', (WidgetTester tester) async { + final AnimationSheetBuilder animationSheet = AnimationSheetBuilder(frameSize: const Size(40, 40)); + + await tester.pumpFrames(animationSheet.record( + const Directionality( + textDirection: TextDirection.ltr, + child: Padding( + padding: EdgeInsets.all(4), + child: CircularProgressIndicator(), + ), + ), + ), const Duration(seconds: 2)); + + tester.binding.setSurfaceSize(animationSheet.sheetSize()); + + final Widget display = await animationSheet.display(); + await tester.pumpWidget(display); + + await expectLater( + find.byWidget(display), + matchesGoldenFile('material.circular_progress_indicator.indeterminate.png'), + ); + }, skip: isBrowser); }