blob: f747ab7d24c4fba57cbd158669121459f46b39d4 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'colors.dart';
const double _kDefaultIndicatorRadius = 10.0;
// Extracted from iOS 13.2 Beta.
const Color _kActiveTickColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFF3C3C44),
darkColor: Color(0xFFEBEBF5),
);
/// Define the iOS version style of [CupertinoActivityIndicator].
enum CupertinoActivityIndicatorIOSVersionStyle {
/// The style that is used in iOS13 and earlier (12 points).
@Deprecated(
'Use iOS14 instead. '
'This feature was deprecated after v1.21.0-1.0.pre.'
)
iOS13,
/// The style that was introduced in iOS14 (8 points).
iOS14,
}
/// An iOS-style activity indicator that spins clockwise.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=AENVH-ZqKDQ}
///
/// See also:
///
/// * <https://developer.apple.com/ios/human-interface-guidelines/controls/progress-indicators/#activity-indicators>
class CupertinoActivityIndicator extends StatefulWidget {
/// Creates an iOS-style activity indicator that spins clockwise.
const CupertinoActivityIndicator({
Key key,
this.animating = true,
this.radius = _kDefaultIndicatorRadius,
@Deprecated(
'Leave this field default to use latest style. '
'This feature was deprecated after v1.21.0-1.0.pre.'
)
this.iOSVersionStyle = CupertinoActivityIndicatorIOSVersionStyle.iOS14,
}) : assert(animating != null),
assert(radius != null),
assert(radius > 0.0),
progress = 1.0,
super(key: key);
/// Creates a non-animated iOS-style activity indicator that displays
/// a partial count of ticks based on the value of [progress].
///
/// When provided, the value of [progress] must be between 0.0 (zero ticks
/// will be shown) and 1.0 (all ticks will be shown) inclusive. Defaults
/// to 1.0.
const CupertinoActivityIndicator.partiallyRevealed({
Key key,
this.radius = _kDefaultIndicatorRadius,
this.progress = 1.0,
@Deprecated(
'Leave this field default to use latest style. '
'This feature was deprecated after v1.21.0-1.0.pre.'
)
this.iOSVersionStyle = CupertinoActivityIndicatorIOSVersionStyle.iOS14,
}) : assert(radius != null),
assert(radius > 0.0),
assert(progress != null),
assert(progress >= 0.0),
assert(progress <= 1.0),
animating = false,
super(key: key);
/// Whether the activity indicator is running its animation.
///
/// Defaults to true.
final bool animating;
/// Radius of the spinner widget.
///
/// Defaults to 10px. Must be positive and cannot be null.
final double radius;
/// Determines the percentage of spinner ticks that will be shown. Typical usage would
/// display all ticks, however, this allows for more fine-grained control such as
/// during pull-to-refresh when the drag-down action shows one tick at a time as
/// the user continues to drag down.
///
/// Defaults to 1.0. Must be between 0.0 and 1.0 inclusive, and cannot be null.
final double progress;
/// The iOS version style of activity indicator.
///
/// Defaults to [CupertinoActivityIndicatorIOSVersionStyle.iOS14].
// TODO(ctrysbita): Deprecate after official release, https://github.com/flutter/flutter/issues/62521
final CupertinoActivityIndicatorIOSVersionStyle iOSVersionStyle;
@override
_CupertinoActivityIndicatorState createState() =>
_CupertinoActivityIndicatorState();
}
class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
if (widget.animating) {
_controller.repeat();
}
}
@override
void didUpdateWidget(CupertinoActivityIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.animating != oldWidget.animating) {
if (widget.animating)
_controller.repeat();
else
_controller.stop();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: widget.radius * 2,
width: widget.radius * 2,
child: CustomPaint(
painter: _CupertinoActivityIndicatorPainter(
position: _controller,
activeColor:
CupertinoDynamicColor.resolve(_kActiveTickColor, context),
radius: widget.radius,
progress: widget.progress,
iOSVersionStyle: widget.iOSVersionStyle,
),
),
);
}
}
const double _kTwoPI = math.pi * 2.0;
/// Alpha values extracted from the native component (for both dark and light mode) to
/// draw the spinning ticks.
const Map<CupertinoActivityIndicatorIOSVersionStyle, List<int>>
_kAlphaValuesMap = <CupertinoActivityIndicatorIOSVersionStyle, List<int>>{
/// 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
],
/// 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;
class _CupertinoActivityIndicatorPainter extends CustomPainter {
_CupertinoActivityIndicatorPainter({
@required this.position,
@required this.activeColor,
@required this.radius,
@required this.progress,
CupertinoActivityIndicatorIOSVersionStyle iOSVersionStyle =
CupertinoActivityIndicatorIOSVersionStyle.iOS13,
}) : alphaValues = _kAlphaValuesMap[iOSVersionStyle],
tickFundamentalRRect = RRect.fromLTRBXY(
-radius / _kDefaultIndicatorRadius,
-radius /
(iOSVersionStyle ==
CupertinoActivityIndicatorIOSVersionStyle.iOS14
? 3.0
: 2.0),
radius / _kDefaultIndicatorRadius,
-radius,
radius / _kDefaultIndicatorRadius,
radius / _kDefaultIndicatorRadius,
),
super(repaint: position);
final Animation<double> position;
final Color activeColor;
final double radius;
final double progress;
final List<int> alphaValues;
final RRect tickFundamentalRRect;
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint();
final int tickCount = alphaValues.length;
canvas.save();
canvas.translate(size.width / 2.0, size.height / 2.0);
final int activeTick = (tickCount * position.value).floor();
for (int i = 0; i < tickCount * progress; ++i) {
final int t = (i - activeTick) % tickCount;
paint.color = activeColor
.withAlpha(progress < 1 ? _partiallyRevealedAlpha : alphaValues[t]);
canvas.drawRRect(tickFundamentalRRect, paint);
canvas.rotate(_kTwoPI / tickCount);
}
canvas.restore();
}
@override
bool shouldRepaint(_CupertinoActivityIndicatorPainter oldPainter) {
return oldPainter.position != position ||
oldPainter.activeColor != activeColor ||
oldPainter.progress != progress;
}
}