Implement new activity indicator for iOS14 (#60179)

Updates the activity indicator style to iOS14, but places it behind a flag, to be deprecated when iOS 14 is released.
This commit is contained in:
Jason C.H 2020-07-15 00:21:27 +08:00 committed by GitHub
parent ce55e42f2b
commit c047769487
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 177 additions and 50 deletions

View file

@ -18,6 +18,15 @@ const Color _kActiveTickColor = CupertinoDynamicColor.withBrightness(
darkColor: Color(0xFFEBEBF5), darkColor: Color(0xFFEBEBF5),
); );
/// Define the iOS version style of [CupertinoActivityIndicator].
enum CupertinoActivityIndicatorIOSVersionStyle {
/// The style that is used in iOS13 and earlier (12 points).
iOS13,
/// The style that was introduced in iOS14 (8 points).
iOS14,
}
/// An iOS-style activity indicator that spins clockwise. /// An iOS-style activity indicator that spins clockwise.
/// ///
/// {@youtube 560 315 https://www.youtube.com/watch?v=AENVH-ZqKDQ} /// {@youtube 560 315 https://www.youtube.com/watch?v=AENVH-ZqKDQ}
@ -31,6 +40,7 @@ class CupertinoActivityIndicator extends StatefulWidget {
Key key, Key key,
this.animating = true, this.animating = true,
this.radius = _kDefaultIndicatorRadius, this.radius = _kDefaultIndicatorRadius,
this.iOSVersionStyle = CupertinoActivityIndicatorIOSVersionStyle.iOS13,
}) : assert(animating != null), }) : assert(animating != null),
assert(radius != null), assert(radius != null),
assert(radius > 0.0), assert(radius > 0.0),
@ -47,6 +57,7 @@ class CupertinoActivityIndicator extends StatefulWidget {
Key key, Key key,
this.radius = _kDefaultIndicatorRadius, this.radius = _kDefaultIndicatorRadius,
this.progress = 1.0, this.progress = 1.0,
this.iOSVersionStyle = CupertinoActivityIndicatorIOSVersionStyle.iOS13,
}) : assert(radius != null), }) : assert(radius != null),
assert(radius > 0.0), assert(radius > 0.0),
assert(progress != null), assert(progress != null),
@ -73,12 +84,19 @@ class CupertinoActivityIndicator extends StatefulWidget {
/// Defaults to 1.0. Must be between 0.0 and 1.0 inclusive, and cannot be null. /// Defaults to 1.0. Must be between 0.0 and 1.0 inclusive, and cannot be null.
final double progress; final double progress;
/// The iOS version style of activity indicator.
///
/// Defaults to [CupertinoActivityIndicatorIOSVersionStyle.iOS13].
// TODO(ctrysbita): Change default style to iOS14 after official release, https://github.com/flutter/flutter/issues/60047
final CupertinoActivityIndicatorIOSVersionStyle iOSVersionStyle;
@override @override
_CupertinoActivityIndicatorState createState() => _CupertinoActivityIndicatorState(); _CupertinoActivityIndicatorState createState() =>
_CupertinoActivityIndicatorState();
} }
class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator> with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
AnimationController _controller; AnimationController _controller;
@override @override
@ -89,9 +107,10 @@ class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
vsync: this, vsync: this,
); );
if (widget.animating) if (widget.animating) {
_controller.repeat(); _controller.repeat();
} }
}
@override @override
void didUpdateWidget(CupertinoActivityIndicator oldWidget) { void didUpdateWidget(CupertinoActivityIndicator oldWidget) {
@ -118,9 +137,11 @@ class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
child: CustomPaint( child: CustomPaint(
painter: _CupertinoActivityIndicatorPainter( painter: _CupertinoActivityIndicatorPainter(
position: _controller, position: _controller,
activeColor: CupertinoDynamicColor.resolve(_kActiveTickColor, context), activeColor:
CupertinoDynamicColor.resolve(_kActiveTickColor, context),
radius: widget.radius, radius: widget.radius,
progress: widget.progress, progress: widget.progress,
iOSVersionStyle: widget.iOSVersionStyle,
), ),
), ),
); );
@ -128,15 +149,42 @@ class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
} }
const double _kTwoPI = math.pi * 2.0; const double _kTwoPI = math.pi * 2.0;
const int _kTickCount = 12;
// Alpha values extracted from the native component (for both dark and light mode) to /// Alpha values extracted from the native component (for both dark and light mode) to
// draw the spinning ticks. The list must have a length of _kTickCount. The order of /// draw the spinning ticks.
// these values is designed to match the first frame of the iOS activity indicator which const Map<CupertinoActivityIndicatorIOSVersionStyle, List<int>>
// has the most prominent tick at 9 o'clock. _kAlphaValuesMap = <CupertinoActivityIndicatorIOSVersionStyle, List<int>>{
const List<int> _alphaValues = <int>[47, 47, 47, 47, 64, 81, 97, 114, 131, 147, 47, 47]; /// The order of these values is designed to match the first frame of the iOS activity indicator which
/// has the most prominent tick at 9 o'clock.
CupertinoActivityIndicatorIOSVersionStyle.iOS13: <int>[
47,
47,
47,
47,
64,
81,
97,
114,
131,
147,
47,
47
],
// The alpha value that is used to draw the partially revealed ticks. /// Alpha values for new style that introduced in iOS14.
CupertinoActivityIndicatorIOSVersionStyle.iOS14: <int>[
47,
47,
47,
47,
72,
97,
122,
147,
],
};
/// The alpha value that is used to draw the partially revealed ticks.
const int _partiallyRevealedAlpha = 147; const int _partiallyRevealedAlpha = 147;
class _CupertinoActivityIndicatorPainter extends CustomPainter { class _CupertinoActivityIndicatorPainter extends CustomPainter {
@ -145,9 +193,16 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter {
@required this.activeColor, @required this.activeColor,
@required this.radius, @required this.radius,
@required this.progress, @required this.progress,
}) : tickFundamentalRRect = RRect.fromLTRBXY( CupertinoActivityIndicatorIOSVersionStyle iOSVersionStyle =
CupertinoActivityIndicatorIOSVersionStyle.iOS13,
}) : alphaValues = _kAlphaValuesMap[iOSVersionStyle],
tickFundamentalRRect = RRect.fromLTRBXY(
-radius / _kDefaultIndicatorRadius, -radius / _kDefaultIndicatorRadius,
-radius / 2.0, -radius /
(iOSVersionStyle ==
CupertinoActivityIndicatorIOSVersionStyle.iOS14
? 3.0
: 2.0),
radius / _kDefaultIndicatorRadius, radius / _kDefaultIndicatorRadius,
-radius, -radius,
radius / _kDefaultIndicatorRadius, radius / _kDefaultIndicatorRadius,
@ -156,25 +211,29 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter {
super(repaint: position); super(repaint: position);
final Animation<double> position; final Animation<double> position;
final RRect tickFundamentalRRect;
final Color activeColor; final Color activeColor;
final double radius; final double radius;
final double progress; final double progress;
final List<int> alphaValues;
final RRect tickFundamentalRRect;
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final Paint paint = Paint(); final Paint paint = Paint();
final int tickCount = alphaValues.length;
canvas.save(); canvas.save();
canvas.translate(size.width / 2.0, size.height / 2.0); canvas.translate(size.width / 2.0, size.height / 2.0);
final int activeTick = (_kTickCount * position.value).floor(); final int activeTick = (tickCount * position.value).floor();
for (int i = 0; i < _kTickCount * progress; ++i) { for (int i = 0; i < tickCount * progress; ++i) {
final int t = (i - activeTick) % _kTickCount; final int t = (i - activeTick) % tickCount;
paint.color = activeColor.withAlpha(progress < 1 ? _partiallyRevealedAlpha : _alphaValues[t]); paint.color = activeColor
.withAlpha(progress < 1 ? _partiallyRevealedAlpha : alphaValues[t]);
canvas.drawRRect(tickFundamentalRRect, paint); canvas.drawRRect(tickFundamentalRRect, paint);
canvas.rotate(_kTwoPI / _kTickCount); canvas.rotate(_kTwoPI / tickCount);
} }
canvas.restore(); canvas.restore();
@ -182,6 +241,8 @@ class _CupertinoActivityIndicatorPainter extends CustomPainter {
@override @override
bool shouldRepaint(_CupertinoActivityIndicatorPainter oldPainter) { bool shouldRepaint(_CupertinoActivityIndicatorPainter oldPainter) {
return oldPainter.position != position || oldPainter.activeColor != activeColor || oldPainter.progress != progress; return oldPainter.position != position ||
oldPainter.activeColor != activeColor ||
oldPainter.progress != progress;
} }
} }

View file

@ -11,8 +11,8 @@ import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart'; import '../rendering/mock_canvas.dart';
void main() { void main() {
testWidgets('Activity indicator animate property works', (WidgetTester tester) async { testWidgets('Activity indicator animate property works',
(WidgetTester tester) async {
await tester.pumpWidget(buildCupertinoActivityIndicator()); await tester.pumpWidget(buildCupertinoActivityIndicator());
expect(SchedulerBinding.instance.transientCallbackCount, equals(1)); expect(SchedulerBinding.instance.transientCallbackCount, equals(1));
@ -38,7 +38,10 @@ void main() {
key: key, key: key,
child: Container( child: Container(
color: CupertinoColors.white, color: CupertinoColors.white,
child: const CupertinoActivityIndicator(animating: false, radius: 35), child: const CupertinoActivityIndicator(
animating: false,
radius: 35,
),
), ),
), ),
), ),
@ -58,7 +61,10 @@ void main() {
key: key, key: key,
child: Container( child: Container(
color: CupertinoColors.black, color: CupertinoColors.black,
child: const CupertinoActivityIndicator(animating: false, radius: 35), child: const CupertinoActivityIndicator(
animating: false,
radius: 35,
),
), ),
), ),
), ),
@ -71,6 +77,60 @@ void main() {
); );
}); });
testWidgets('Activity indicator with iOS14 style',
(WidgetTester tester) async {
final Key key = UniqueKey();
await tester.pumpWidget(
Center(
child: MediaQuery(
data: const MediaQueryData(platformBrightness: Brightness.light),
child: RepaintBoundary(
key: key,
child: Container(
color: CupertinoColors.white,
child: const CupertinoActivityIndicator(
animating: false,
radius: 35,
iOSVersionStyle:
CupertinoActivityIndicatorIOSVersionStyle.iOS14,
),
),
),
),
),
);
await expectLater(
find.byKey(key),
matchesGoldenFile('activityIndicator.iOS14.paused.light.png'),
);
await tester.pumpWidget(
Center(
child: MediaQuery(
data: const MediaQueryData(platformBrightness: Brightness.dark),
child: RepaintBoundary(
key: key,
child: Container(
color: CupertinoColors.black,
child: const CupertinoActivityIndicator(
animating: false,
radius: 35,
iOSVersionStyle:
CupertinoActivityIndicatorIOSVersionStyle.iOS14,
),
),
),
),
),
);
await expectLater(
find.byKey(key),
matchesGoldenFile('activityIndicator.iOS14.paused.dark.png'),
);
});
testWidgets('Activity indicator 0% in progress', (WidgetTester tester) async { testWidgets('Activity indicator 0% in progress', (WidgetTester tester) async {
final Key key = UniqueKey(); final Key key = UniqueKey();
await tester.pumpWidget( await tester.pumpWidget(
@ -79,7 +139,8 @@ void main() {
key: key, key: key,
child: Container( child: Container(
color: CupertinoColors.white, color: CupertinoColors.white,
child: const CupertinoActivityIndicator.partiallyRevealed(progress: 0), child:
const CupertinoActivityIndicator.partiallyRevealed(progress: 0),
), ),
), ),
), ),
@ -91,7 +152,8 @@ void main() {
); );
}); });
testWidgets('Activity indicator 30% in progress', (WidgetTester tester) async { testWidgets('Activity indicator 30% in progress',
(WidgetTester tester) async {
final Key key = UniqueKey(); final Key key = UniqueKey();
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
@ -99,7 +161,9 @@ void main() {
key: key, key: key,
child: Container( child: Container(
color: CupertinoColors.white, color: CupertinoColors.white,
child: const CupertinoActivityIndicator.partiallyRevealed(progress: 0.5), child: const CupertinoActivityIndicator.partiallyRevealed(
progress: 0.5,
),
), ),
), ),
), ),
@ -111,7 +175,8 @@ void main() {
); );
}); });
testWidgets('Activity indicator 100% in progress', (WidgetTester tester) async { testWidgets('Activity indicator 100% in progress',
(WidgetTester tester) async {
final Key key = UniqueKey(); final Key key = UniqueKey();
await tester.pumpWidget( await tester.pumpWidget(
Center( Center(
@ -119,7 +184,8 @@ void main() {
key: key, key: key,
child: Container( child: Container(
color: CupertinoColors.white, color: CupertinoColors.white,
child: const CupertinoActivityIndicator.partiallyRevealed(progress: 1), child:
const CupertinoActivityIndicator.partiallyRevealed(progress: 1),
), ),
), ),
), ),
@ -148,7 +214,7 @@ void main() {
}); });
} }
Widget buildCupertinoActivityIndicator([ bool animating ]) { Widget buildCupertinoActivityIndicator([bool animating]) {
return MediaQuery( return MediaQuery(
data: const MediaQueryData(platformBrightness: Brightness.light), data: const MediaQueryData(platformBrightness: Brightness.light),
child: CupertinoActivityIndicator( child: CupertinoActivityIndicator(