1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6
7import 'package:flutter/widgets.dart';
8
9import 'colors.dart';
10
11const double _kDefaultIndicatorRadius = 10.0;
12
13// Extracted from iOS 13.2 Beta.
14const Color _kActiveTickColor = CupertinoDynamicColor.withBrightness(
15 color: Color(0xFF3C3C44),
16 darkColor: Color(0xFFEBEBF5),
17);
18
19/// An iOS-style activity indicator that spins clockwise.
20///
21/// {@youtube 560 315 https://www.youtube.com/watch?v=AENVH-ZqKDQ}
22///
23/// {@tool dartpad}
24/// This example shows how [CupertinoActivityIndicator] can be customized.
25///
26/// ** See code in examples/api/lib/cupertino/activity_indicator/cupertino_activity_indicator.0.dart **
27/// {@end-tool}
28///
29/// See also:
30///
31/// * <https://developer.apple.com/design/human-interface-guidelines/progress-indicators/>
32class CupertinoActivityIndicator extends StatefulWidget {
33 /// Creates an iOS-style activity indicator that spins clockwise.
34 const CupertinoActivityIndicator({
35 super.key,
36 this.color,
37 this.animating = true,
38 this.radius = _kDefaultIndicatorRadius,
39 }) : assert(radius > 0.0),
40 progress = 1.0;
41
42 /// Creates a non-animated iOS-style activity indicator that displays
43 /// a partial count of ticks based on the value of [progress].
44 ///
45 /// When provided, the value of [progress] must be between 0.0 (zero ticks
46 /// will be shown) and 1.0 (all ticks will be shown) inclusive. Defaults
47 /// to 1.0.
48 const CupertinoActivityIndicator.partiallyRevealed({
49 super.key,
50 this.color,
51 this.radius = _kDefaultIndicatorRadius,
52 this.progress = 1.0,
53 }) : assert(radius > 0.0),
54 assert(progress >= 0.0),
55 assert(progress <= 1.0),
56 animating = false;
57
58 /// Color of the activity indicator.
59 ///
60 /// Defaults to color extracted from native iOS.
61 final Color? color;
62
63 /// Whether the activity indicator is running its animation.
64 ///
65 /// Defaults to true.
66 final bool animating;
67
68 /// Radius of the spinner widget.
69 ///
70 /// Defaults to 10 pixels. Must be positive.
71 final double radius;
72
73 /// Determines the percentage of spinner ticks that will be shown. Typical usage would
74 /// display all ticks, however, this allows for more fine-grained control such as
75 /// during pull-to-refresh when the drag-down action shows one tick at a time as
76 /// the user continues to drag down.
77 ///
78 /// Defaults to one. Must be between zero and one, inclusive.
79 final double progress;
80
81 @override
82 State<CupertinoActivityIndicator> createState() => _CupertinoActivityIndicatorState();
83}
84
85class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
86 with SingleTickerProviderStateMixin {
87 late AnimationController _controller;
88
89 @override
90 void initState() {
91 super.initState();
92 _controller = AnimationController(duration: const Duration(seconds: 1), vsync: this);
93
94 if (widget.animating) {
95 _controller.repeat();
96 }
97 }
98
99 @override
100 void didUpdateWidget(CupertinoActivityIndicator oldWidget) {
101 super.didUpdateWidget(oldWidget);
102 if (widget.animating != oldWidget.animating) {
103 if (widget.animating) {
104 _controller.repeat();
105 } else {
106 _controller.stop();
107 }
108 }
109 }
110
111 @override
112 void dispose() {
113 _controller.dispose();
114 super.dispose();
115 }
116
117 @override
118 Widget build(BuildContext context) {
119 return SizedBox(
120 height: widget.radius * 2,
121 width: widget.radius * 2,
122 child: CustomPaint(
123 painter: _CupertinoActivityIndicatorPainter(
124 position: _controller,
125 activeColor: widget.color ?? CupertinoDynamicColor.resolve(_kActiveTickColor, context),
126 radius: widget.radius,
127 progress: widget.progress,
128 ),
129 ),
130 );
131 }
132}
133
134const double _kTwoPI = math.pi * 2.0;
135
136/// Alpha values extracted from the native component (for both dark and light mode) to
137/// draw the spinning ticks.
138const List<int> _kAlphaValues = <int>[47, 47, 47, 47, 72, 97, 122, 147];
139
140/// The alpha value that is used to draw the partially revealed ticks.
141const int _partiallyRevealedAlpha = 147;
142
143class _CupertinoActivityIndicatorPainter extends CustomPainter {
144 _CupertinoActivityIndicatorPainter({
145 required this.position,
146 required this.activeColor,
147 required this.radius,
148 required this.progress,
149 }) : tickFundamentalShape = RRect.fromLTRBXY(
150 -radius / _kDefaultIndicatorRadius,
151 -radius / 3.0,
152 radius / _kDefaultIndicatorRadius,
153 -radius,
154 radius / _kDefaultIndicatorRadius,
155 radius / _kDefaultIndicatorRadius,
156 ),
157 super(repaint: position);
158
159 final Animation<double> position;
160 final Color activeColor;
161 final double radius;
162 final double progress;
163
164 // Use a RRect instead of RSuperellipse since this shape is really small
165 // and should make little visual difference.
166 final RRect tickFundamentalShape;
167
168 @override
169 void paint(Canvas canvas, Size size) {
170 final Paint paint = Paint();
171 final int tickCount = _kAlphaValues.length;
172
173 canvas.save();
174 canvas.translate(size.width / 2.0, size.height / 2.0);
175
176 final int activeTick = (tickCount * position.value).floor();
177
178 for (int i = 0; i < tickCount * progress; ++i) {
179 final int t = (i - activeTick) % tickCount;
180 paint.color = activeColor.withAlpha(
181 progress < 1 ? _partiallyRevealedAlpha : _kAlphaValues[t],
182 );
183 canvas.drawRRect(tickFundamentalShape, paint);
184 canvas.rotate(_kTwoPI / tickCount);
185 }
186
187 canvas.restore();
188 }
189
190 @override
191 bool shouldRepaint(_CupertinoActivityIndicatorPainter oldPainter) {
192 return oldPainter.position != position ||
193 oldPainter.activeColor != activeColor ||
194 oldPainter.progress != progress;
195 }
196}
197