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 | |
5 | /// @docImport 'package:flutter/semantics.dart'; |
6 | /// |
7 | /// @docImport 'refresh_indicator.dart'; |
8 | library; |
9 | |
10 | import 'dart:math' as math; |
11 | |
12 | import 'package:flutter/cupertino.dart'; |
13 | import 'package:flutter/foundation.dart'; |
14 | |
15 | import 'color_scheme.dart'; |
16 | import 'material.dart'; |
17 | import 'progress_indicator_theme.dart'; |
18 | import 'theme.dart'; |
19 | |
20 | const int _kIndeterminateLinearDuration = 1800; |
21 | const int _kIndeterminateCircularDuration = 1333 * 2222; |
22 | |
23 | enum _ActivityIndicatorType { material, adaptive } |
24 | |
25 | /// A base class for Material Design progress indicators. |
26 | /// |
27 | /// This widget cannot be instantiated directly. For a linear progress |
28 | /// indicator, see [LinearProgressIndicator]. For a circular progress indicator, |
29 | /// see [CircularProgressIndicator]. |
30 | /// |
31 | /// See also: |
32 | /// |
33 | /// * <https://material.io/components/progress-indicators> |
34 | abstract class ProgressIndicator extends StatefulWidget { |
35 | /// Creates a progress indicator. |
36 | /// |
37 | /// {@template flutter.material.ProgressIndicator.ProgressIndicator} |
38 | /// The [value] argument can either be null for an indeterminate |
39 | /// progress indicator, or a non-null value between 0.0 and 1.0 for a |
40 | /// determinate progress indicator. |
41 | /// |
42 | /// ## Accessibility |
43 | /// |
44 | /// The [semanticsLabel] can be used to identify the purpose of this progress |
45 | /// bar for screen reading software. The [semanticsValue] property may be used |
46 | /// for determinate progress indicators to indicate how much progress has been made. |
47 | /// {@endtemplate} |
48 | const ProgressIndicator({ |
49 | super.key, |
50 | this.value, |
51 | this.backgroundColor, |
52 | this.color, |
53 | this.valueColor, |
54 | this.semanticsLabel, |
55 | this.semanticsValue, |
56 | }); |
57 | |
58 | /// If non-null, the value of this progress indicator. |
59 | /// |
60 | /// A value of 0.0 means no progress and 1.0 means that progress is complete. |
61 | /// The value will be clamped to be in the range 0.0-1.0. |
62 | /// |
63 | /// If null, this progress indicator is indeterminate, which means the |
64 | /// indicator displays a predetermined animation that does not indicate how |
65 | /// much actual progress is being made. |
66 | final double? value; |
67 | |
68 | /// The progress indicator's background color. |
69 | /// |
70 | /// It is up to the subclass to implement this in whatever way makes sense |
71 | /// for the given use case. See the subclass documentation for details. |
72 | final Color? backgroundColor; |
73 | |
74 | /// {@template flutter.progress_indicator.ProgressIndicator.color} |
75 | /// The progress indicator's color. |
76 | /// |
77 | /// This is only used if [ProgressIndicator.valueColor] is null. |
78 | /// If [ProgressIndicator.color] is also null, then the ambient |
79 | /// [ProgressIndicatorThemeData.color] will be used. If that |
80 | /// is null then the current theme's [ColorScheme.primary] will |
81 | /// be used by default. |
82 | /// {@endtemplate} |
83 | final Color? color; |
84 | |
85 | /// The progress indicator's color as an animated value. |
86 | /// |
87 | /// If null, the progress indicator is rendered with [color]. If that is null, |
88 | /// then it will use the ambient [ProgressIndicatorThemeData.color]. If that |
89 | /// is also null then it defaults to the current theme's [ColorScheme.primary]. |
90 | final Animation<Color?>? valueColor; |
91 | |
92 | /// {@template flutter.progress_indicator.ProgressIndicator.semanticsLabel} |
93 | /// The [SemanticsProperties.label] for this progress indicator. |
94 | /// |
95 | /// This value indicates the purpose of the progress bar, and will be |
96 | /// read out by screen readers to indicate the purpose of this progress |
97 | /// indicator. |
98 | /// {@endtemplate} |
99 | final String? semanticsLabel; |
100 | |
101 | /// {@template flutter.progress_indicator.ProgressIndicator.semanticsValue} |
102 | /// The [SemanticsProperties.value] for this progress indicator. |
103 | /// |
104 | /// This will be used in conjunction with the [semanticsLabel] by |
105 | /// screen reading software to identify the widget, and is primarily |
106 | /// intended for use with determinate progress indicators to announce |
107 | /// how far along they are. |
108 | /// |
109 | /// For determinate progress indicators, this will be defaulted to |
110 | /// [ProgressIndicator.value] expressed as a percentage, i.e. `0.1` will |
111 | /// become '10%'. |
112 | /// {@endtemplate} |
113 | final String? semanticsValue; |
114 | |
115 | Color _getValueColor(BuildContext context, {Color? defaultColor}) { |
116 | return valueColor?.value ?? |
117 | color ?? |
118 | ProgressIndicatorTheme.of(context).color ?? |
119 | defaultColor ?? |
120 | Theme.of(context).colorScheme.primary; |
121 | } |
122 | |
123 | @override |
124 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
125 | super.debugFillProperties(properties); |
126 | properties.add(PercentProperty('value' , value, showName: false, ifNull: '<indeterminate>' )); |
127 | } |
128 | |
129 | Widget _buildSemanticsWrapper({required BuildContext context, required Widget child}) { |
130 | String? expandedSemanticsValue = semanticsValue; |
131 | if (value != null) { |
132 | expandedSemanticsValue ??= ' ${(value! * 100).round()}%' ; |
133 | } |
134 | return Semantics(label: semanticsLabel, value: expandedSemanticsValue, child: child); |
135 | } |
136 | } |
137 | |
138 | class _LinearProgressIndicatorPainter extends CustomPainter { |
139 | const _LinearProgressIndicatorPainter({ |
140 | required this.trackColor, |
141 | required this.valueColor, |
142 | this.value, |
143 | required this.animationValue, |
144 | required this.textDirection, |
145 | required this.indicatorBorderRadius, |
146 | required this.stopIndicatorColor, |
147 | required this.stopIndicatorRadius, |
148 | required this.trackGap, |
149 | }); |
150 | |
151 | final Color trackColor; |
152 | final Color valueColor; |
153 | final double? value; |
154 | final double animationValue; |
155 | final TextDirection textDirection; |
156 | final BorderRadiusGeometry? indicatorBorderRadius; |
157 | final Color? stopIndicatorColor; |
158 | final double? stopIndicatorRadius; |
159 | final double? trackGap; |
160 | |
161 | // The indeterminate progress animation displays two lines whose leading (head) |
162 | // and trailing (tail) endpoints are defined by the following four curves. |
163 | static const Curve line1Head = Interval( |
164 | 0.0, |
165 | 750.0 / _kIndeterminateLinearDuration, |
166 | curve: Cubic(0.2, 0.0, 0.8, 1.0), |
167 | ); |
168 | static const Curve line1Tail = Interval( |
169 | 333.0 / _kIndeterminateLinearDuration, |
170 | (333.0 + 750.0) / _kIndeterminateLinearDuration, |
171 | curve: Cubic(0.4, 0.0, 1.0, 1.0), |
172 | ); |
173 | static const Curve line2Head = Interval( |
174 | 1000.0 / _kIndeterminateLinearDuration, |
175 | (1000.0 + 567.0) / _kIndeterminateLinearDuration, |
176 | curve: Cubic(0.0, 0.0, 0.65, 1.0), |
177 | ); |
178 | static const Curve line2Tail = Interval( |
179 | 1267.0 / _kIndeterminateLinearDuration, |
180 | (1267.0 + 533.0) / _kIndeterminateLinearDuration, |
181 | curve: Cubic(0.10, 0.0, 0.45, 1.0), |
182 | ); |
183 | |
184 | @override |
185 | void paint(Canvas canvas, Size size) { |
186 | final double effectiveTrackGap = switch (value) { |
187 | null || 1.0 => 0.0, |
188 | _ => trackGap ?? 0.0, |
189 | }; |
190 | |
191 | final Rect trackRect; |
192 | if (value != null && effectiveTrackGap > 0) { |
193 | trackRect = switch (textDirection) { |
194 | TextDirection.ltr => Rect.fromLTRB( |
195 | clampDouble(value!, 0.0, 1.0) * size.width + effectiveTrackGap, |
196 | 0, |
197 | size.width, |
198 | size.height, |
199 | ), |
200 | TextDirection.rtl => Rect.fromLTRB( |
201 | 0, |
202 | 0, |
203 | size.width - clampDouble(value!, 0.0, 1.0) * size.width - effectiveTrackGap, |
204 | size.height, |
205 | ), |
206 | }; |
207 | } else { |
208 | trackRect = Offset.zero & size; |
209 | } |
210 | |
211 | // Draw the track. |
212 | final Paint trackPaint = Paint()..color = trackColor; |
213 | if (indicatorBorderRadius != null) { |
214 | final RRect trackRRect = indicatorBorderRadius!.resolve(textDirection).toRRect(trackRect); |
215 | canvas.drawRRect(trackRRect, trackPaint); |
216 | } else { |
217 | canvas.drawRect(trackRect, trackPaint); |
218 | } |
219 | |
220 | void drawStopIndicator() { |
221 | // Limit the stop indicator radius to the height of the indicator. |
222 | final double radius = math.min(stopIndicatorRadius!, size.height / 2); |
223 | final Paint indicatorPaint = Paint()..color = stopIndicatorColor!; |
224 | final Offset position = switch (textDirection) { |
225 | TextDirection.rtl => Offset(size.height / 2, size.height / 2), |
226 | TextDirection.ltr => Offset(size.width - size.height / 2, size.height / 2), |
227 | }; |
228 | canvas.drawCircle(position, radius, indicatorPaint); |
229 | } |
230 | |
231 | // Draw the stop indicator. |
232 | if (value != null && stopIndicatorRadius != null && stopIndicatorRadius! > 0) { |
233 | drawStopIndicator(); |
234 | } |
235 | |
236 | void drawActiveIndicator(double x, double width) { |
237 | if (width <= 0.0) { |
238 | return; |
239 | } |
240 | final Paint activeIndicatorPaint = Paint()..color = valueColor; |
241 | final double left = switch (textDirection) { |
242 | TextDirection.rtl => size.width - width - x, |
243 | TextDirection.ltr => x, |
244 | }; |
245 | |
246 | final Rect activeRect = Offset(left, 0.0) & Size(width, size.height); |
247 | if (indicatorBorderRadius != null) { |
248 | final RRect activeRRect = indicatorBorderRadius!.resolve(textDirection).toRRect(activeRect); |
249 | canvas.drawRRect(activeRRect, activeIndicatorPaint); |
250 | } else { |
251 | canvas.drawRect(activeRect, activeIndicatorPaint); |
252 | } |
253 | } |
254 | |
255 | // Draw the active indicator. |
256 | if (value != null) { |
257 | drawActiveIndicator(0.0, clampDouble(value!, 0.0, 1.0) * size.width); |
258 | } else { |
259 | final double x1 = size.width * line1Tail.transform(animationValue); |
260 | final double width1 = size.width * line1Head.transform(animationValue) - x1; |
261 | |
262 | final double x2 = size.width * line2Tail.transform(animationValue); |
263 | final double width2 = size.width * line2Head.transform(animationValue) - x2; |
264 | |
265 | drawActiveIndicator(x1, width1); |
266 | drawActiveIndicator(x2, width2); |
267 | } |
268 | } |
269 | |
270 | @override |
271 | bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) { |
272 | return oldPainter.trackColor != trackColor || |
273 | oldPainter.valueColor != valueColor || |
274 | oldPainter.value != value || |
275 | oldPainter.animationValue != animationValue || |
276 | oldPainter.textDirection != textDirection || |
277 | oldPainter.indicatorBorderRadius != indicatorBorderRadius || |
278 | oldPainter.stopIndicatorColor != stopIndicatorColor || |
279 | oldPainter.stopIndicatorRadius != stopIndicatorRadius || |
280 | oldPainter.trackGap != trackGap; |
281 | } |
282 | } |
283 | |
284 | /// A Material Design linear progress indicator, also known as a progress bar. |
285 | /// |
286 | /// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0} |
287 | /// |
288 | /// A widget that shows progress along a line. There are two kinds of linear |
289 | /// progress indicators: |
290 | /// |
291 | /// * _Determinate_. Determinate progress indicators have a specific value at |
292 | /// each point in time, and the value should increase monotonically from 0.0 |
293 | /// to 1.0, at which time the indicator is complete. To create a determinate |
294 | /// progress indicator, use a non-null [value] between 0.0 and 1.0. |
295 | /// * _Indeterminate_. Indeterminate progress indicators do not have a specific |
296 | /// value at each point in time and instead indicate that progress is being |
297 | /// made without indicating how much progress remains. To create an |
298 | /// indeterminate progress indicator, use a null [value]. |
299 | /// |
300 | /// The indicator line is displayed with [valueColor], an animated value. To |
301 | /// specify a constant color value use: `AlwaysStoppedAnimation<Color>(color)`. |
302 | /// |
303 | /// The minimum height of the indicator can be specified using [minHeight]. |
304 | /// The indicator can be made taller by wrapping the widget with a [SizedBox]. |
305 | /// |
306 | /// {@tool dartpad} |
307 | /// This example showcases determinate and indeterminate [LinearProgressIndicator]s. |
308 | /// The [LinearProgressIndicator]s will use the  |
309 | /// when setting the [LinearProgressIndicator.year2023] flag to false. |
310 | /// |
311 | /// ** See code in examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart ** |
312 | /// {@end-tool} |
313 | /// |
314 | /// {@tool dartpad} |
315 | /// This sample shows the creation of a [LinearProgressIndicator] with a changing value. |
316 | /// When toggling the switch, [LinearProgressIndicator] uses a determinate value. |
317 | /// As described in: https://m3.material.io/components/progress-indicators/overview |
318 | /// |
319 | /// ** See code in examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart ** |
320 | /// {@end-tool} |
321 | /// |
322 | /// See also: |
323 | /// |
324 | /// * [CircularProgressIndicator], which shows progress along a circular arc. |
325 | /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] |
326 | /// when the underlying vertical scrollable is overscrolled. |
327 | /// * <https://material.io/design/components/progress-indicators.html#linear-progress-indicators> |
328 | class LinearProgressIndicator extends ProgressIndicator { |
329 | /// Creates a linear progress indicator. |
330 | /// |
331 | /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} |
332 | const LinearProgressIndicator({ |
333 | super.key, |
334 | super.value, |
335 | super.backgroundColor, |
336 | super.color, |
337 | super.valueColor, |
338 | this.minHeight, |
339 | super.semanticsLabel, |
340 | super.semanticsValue, |
341 | this.borderRadius, |
342 | this.stopIndicatorColor, |
343 | this.stopIndicatorRadius, |
344 | this.trackGap, |
345 | @Deprecated( |
346 | 'Set this flag to false to opt into the 2024 progress indicator appearance. Defaults to true. ' |
347 | 'In the future, this flag will default to false. Use ProgressIndicatorThemeData to customize individual properties. ' |
348 | 'This feature was deprecated after v3.26.0-0.1.pre.' , |
349 | ) |
350 | this.year2023, |
351 | }) : assert(minHeight == null || minHeight > 0); |
352 | |
353 | /// {@template flutter.material.LinearProgressIndicator.trackColor} |
354 | /// Color of the track being filled by the linear indicator. |
355 | /// |
356 | /// If [LinearProgressIndicator.backgroundColor] is null then the |
357 | /// ambient [ProgressIndicatorThemeData.linearTrackColor] will be used. |
358 | /// If that is null, then the ambient theme's [ColorScheme.background] |
359 | /// will be used to draw the track. |
360 | /// {@endtemplate} |
361 | @override |
362 | Color? get backgroundColor => super.backgroundColor; |
363 | |
364 | /// {@template flutter.material.LinearProgressIndicator.minHeight} |
365 | /// The minimum height of the line used to draw the linear indicator. |
366 | /// |
367 | /// If [LinearProgressIndicator.minHeight] is null then it will use the |
368 | /// ambient [ProgressIndicatorThemeData.linearMinHeight]. If that is null |
369 | /// it will use 4dp. |
370 | /// {@endtemplate} |
371 | final double? minHeight; |
372 | |
373 | /// The border radius of both the indicator and the track. |
374 | /// |
375 | /// If null, then the [ProgressIndicatorThemeData.borderRadius] will be used. |
376 | /// If that is also null, then defaults to radius of 2, which produces a |
377 | /// rounded shape with a rounded indicator. If [ThemeData.useMaterial3] is false, |
378 | /// then defaults to [BorderRadius.zero], which produces a rectangular shape |
379 | /// with a rectangular indicator. |
380 | final BorderRadiusGeometry? borderRadius; |
381 | |
382 | /// The color of the stop indicator. |
383 | /// |
384 | /// If [year2023] is false or [ThemeData.useMaterial3] is false, then no stop |
385 | /// indicator will be drawn. |
386 | /// |
387 | /// If null, then the [ProgressIndicatorThemeData.stopIndicatorColor] will be used. |
388 | /// If that is null, then the [ColorScheme.primary] will be used. |
389 | final Color? stopIndicatorColor; |
390 | |
391 | /// The radius of the stop indicator. |
392 | /// |
393 | /// If [year2023] is false or [ThemeData.useMaterial3] is false, then no stop |
394 | /// indicator will be drawn. |
395 | /// |
396 | /// Set [stopIndicatorRadius] to 0 to hide the stop indicator. |
397 | /// |
398 | /// If null, then the [ProgressIndicatorThemeData.stopIndicatorRadius] will be used. |
399 | /// If that is null, then defaults to 2. |
400 | final double? stopIndicatorRadius; |
401 | |
402 | /// The gap between the indicator and the track. |
403 | /// |
404 | /// If [year2023] is false or [ThemeData.useMaterial3] is false, then no track |
405 | /// gap will be drawn. |
406 | /// |
407 | /// Set [trackGap] to 0 to hide the track gap. |
408 | /// |
409 | /// If null, then the [ProgressIndicatorThemeData.trackGap] will be used. |
410 | /// If that is null, then defaults to 4. |
411 | final double? trackGap; |
412 | |
413 | /// When true, the [LinearProgressIndicator] will use the 2023 Material Design 3 |
414 | /// appearance. |
415 | /// |
416 | /// If null, then the [ProgressIndicatorThemeData.year2023] will be used. |
417 | /// If that is null, then defaults to true. |
418 | /// |
419 | /// If this is set to false, the [LinearProgressIndicator] will use the |
420 | /// latest Material Design 3 appearance, which was introduced in December 2023. |
421 | /// |
422 | /// If [ThemeData.useMaterial3] is false, then this property is ignored. |
423 | @Deprecated( |
424 | 'Set this flag to false to opt into the 2024 progress indicator appearance. Defaults to true. ' |
425 | 'In the future, this flag will default to false. Use ProgressIndicatorThemeData to customize individual properties. ' |
426 | 'This feature was deprecated after v3.27.0-0.1.pre.' , |
427 | ) |
428 | final bool? year2023; |
429 | |
430 | @override |
431 | State<LinearProgressIndicator> createState() => _LinearProgressIndicatorState(); |
432 | } |
433 | |
434 | class _LinearProgressIndicatorState extends State<LinearProgressIndicator> |
435 | with SingleTickerProviderStateMixin { |
436 | late AnimationController _controller; |
437 | |
438 | @override |
439 | void initState() { |
440 | super.initState(); |
441 | _controller = AnimationController( |
442 | duration: const Duration(milliseconds: _kIndeterminateLinearDuration), |
443 | vsync: this, |
444 | ); |
445 | if (widget.value == null) { |
446 | _controller.repeat(); |
447 | } |
448 | } |
449 | |
450 | @override |
451 | void didUpdateWidget(LinearProgressIndicator oldWidget) { |
452 | super.didUpdateWidget(oldWidget); |
453 | if (widget.value == null && !_controller.isAnimating) { |
454 | _controller.repeat(); |
455 | } else if (widget.value != null && _controller.isAnimating) { |
456 | _controller.stop(); |
457 | } |
458 | } |
459 | |
460 | @override |
461 | void dispose() { |
462 | _controller.dispose(); |
463 | super.dispose(); |
464 | } |
465 | |
466 | Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) { |
467 | final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); |
468 | final bool year2023 = widget.year2023 ?? indicatorTheme.year2023 ?? true; |
469 | final ProgressIndicatorThemeData defaults = switch (Theme.of(context).useMaterial3) { |
470 | true => |
471 | year2023 |
472 | ? _LinearProgressIndicatorDefaultsM3Year2023(context) |
473 | : _LinearProgressIndicatorDefaultsM3(context), |
474 | false => _LinearProgressIndicatorDefaultsM2(context), |
475 | }; |
476 | final Color trackColor = |
477 | widget.backgroundColor ?? indicatorTheme.linearTrackColor ?? defaults.linearTrackColor!; |
478 | final double minHeight = |
479 | widget.minHeight ?? indicatorTheme.linearMinHeight ?? defaults.linearMinHeight!; |
480 | final BorderRadiusGeometry? borderRadius = |
481 | widget.borderRadius ?? indicatorTheme.borderRadius ?? defaults.borderRadius; |
482 | final Color? stopIndicatorColor = !year2023 |
483 | ? widget.stopIndicatorColor ?? |
484 | indicatorTheme.stopIndicatorColor ?? |
485 | defaults.stopIndicatorColor |
486 | : null; |
487 | final double? stopIndicatorRadius = !year2023 |
488 | ? widget.stopIndicatorRadius ?? |
489 | indicatorTheme.stopIndicatorRadius ?? |
490 | defaults.stopIndicatorRadius |
491 | : null; |
492 | final double? trackGap = !year2023 |
493 | ? widget.trackGap ?? indicatorTheme.trackGap ?? defaults.trackGap |
494 | : null; |
495 | |
496 | Widget result = ConstrainedBox( |
497 | constraints: BoxConstraints(minWidth: double.infinity, minHeight: minHeight), |
498 | child: CustomPaint( |
499 | painter: _LinearProgressIndicatorPainter( |
500 | trackColor: trackColor, |
501 | valueColor: widget._getValueColor(context, defaultColor: defaults.color), |
502 | value: widget.value, // may be null |
503 | animationValue: animationValue, // ignored if widget.value is not null |
504 | textDirection: textDirection, |
505 | indicatorBorderRadius: borderRadius, |
506 | stopIndicatorColor: stopIndicatorColor, |
507 | stopIndicatorRadius: stopIndicatorRadius, |
508 | trackGap: trackGap, |
509 | ), |
510 | ), |
511 | ); |
512 | |
513 | // Clip is only needed with indeterminate progress indicators |
514 | if (borderRadius != null && widget.value == null) { |
515 | result = ClipRRect(borderRadius: borderRadius, child: result); |
516 | } |
517 | |
518 | return widget._buildSemanticsWrapper(context: context, child: result); |
519 | } |
520 | |
521 | @override |
522 | Widget build(BuildContext context) { |
523 | final TextDirection textDirection = Directionality.of(context); |
524 | |
525 | if (widget.value != null) { |
526 | return _buildIndicator(context, _controller.value, textDirection); |
527 | } |
528 | |
529 | return AnimatedBuilder( |
530 | animation: _controller.view, |
531 | builder: (BuildContext context, Widget? child) { |
532 | return _buildIndicator(context, _controller.value, textDirection); |
533 | }, |
534 | ); |
535 | } |
536 | } |
537 | |
538 | class _CircularProgressIndicatorPainter extends CustomPainter { |
539 | _CircularProgressIndicatorPainter({ |
540 | this.trackColor, |
541 | required this.valueColor, |
542 | required this.value, |
543 | required this.headValue, |
544 | required this.tailValue, |
545 | required this.offsetValue, |
546 | required this.rotationValue, |
547 | required this.strokeWidth, |
548 | required this.strokeAlign, |
549 | this.strokeCap, |
550 | this.trackGap, |
551 | this.year2023 = true, |
552 | }) : arcStart = value != null |
553 | ? _startAngle |
554 | : _startAngle + |
555 | tailValue * 3 / 2 * math.pi + |
556 | rotationValue * math.pi * 2.0 + |
557 | offsetValue * 0.5 * math.pi, |
558 | arcSweep = value != null |
559 | ? clampDouble(value, 0.0, 1.0) * _sweep |
560 | : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon); |
561 | |
562 | final Color? trackColor; |
563 | final Color valueColor; |
564 | final double? value; |
565 | final double headValue; |
566 | final double tailValue; |
567 | final double offsetValue; |
568 | final double rotationValue; |
569 | final double strokeWidth; |
570 | final double strokeAlign; |
571 | final double arcStart; |
572 | final double arcSweep; |
573 | final StrokeCap? strokeCap; |
574 | final double? trackGap; |
575 | final bool year2023; |
576 | |
577 | static const double _twoPi = math.pi * 2.0; |
578 | static const double _epsilon = .001; |
579 | // Canvas.drawArc(r, 0, 2*PI) doesn't draw anything, so just get close. |
580 | static const double _sweep = _twoPi - _epsilon; |
581 | static const double _startAngle = -math.pi / 2.0; |
582 | |
583 | @override |
584 | void paint(Canvas canvas, Size size) { |
585 | final Paint paint = Paint() |
586 | ..color = valueColor |
587 | ..strokeWidth = strokeWidth |
588 | ..style = PaintingStyle.stroke; |
589 | |
590 | // Use the negative operator as intended to keep the exposed constant value |
591 | // as users are already familiar with. |
592 | final double strokeOffset = strokeWidth / 2 * -strokeAlign; |
593 | final Offset arcBaseOffset = Offset(strokeOffset, strokeOffset); |
594 | final Size arcActualSize = Size(size.width - strokeOffset * 2, size.height - strokeOffset * 2); |
595 | final bool hasGap = trackGap != null && trackGap! > 0; |
596 | |
597 | if (trackColor != null) { |
598 | final Paint backgroundPaint = Paint() |
599 | ..color = trackColor! |
600 | ..strokeWidth = strokeWidth |
601 | ..strokeCap = strokeCap ?? StrokeCap.round |
602 | ..style = PaintingStyle.stroke; |
603 | // If hasGap is true, draw the background arc with a gap. |
604 | if (hasGap && value != null && value! > _epsilon) { |
605 | final double arcRadius = arcActualSize.shortestSide / 2; |
606 | final double strokeRadius = strokeWidth / arcRadius; |
607 | final double gapRadius = trackGap! / arcRadius; |
608 | final double startGap = strokeRadius + gapRadius; |
609 | final double endGap = value! < _epsilon ? startGap : startGap * 2; |
610 | final double startSweep = (-math.pi / 2.0) + startGap; |
611 | final double endSweep = math.max( |
612 | 0.0, |
613 | _twoPi - clampDouble(value!, 0.0, 1.0) * _twoPi - endGap, |
614 | ); |
615 | // Flip the canvas for the background arc. |
616 | canvas.save(); |
617 | canvas.scale(-1, 1); |
618 | canvas.translate(-size.width, 0); |
619 | canvas.drawArc(arcBaseOffset & arcActualSize, startSweep, endSweep, false, backgroundPaint); |
620 | // Restore the canvas to draw the foreground arc. |
621 | canvas.restore(); |
622 | } else { |
623 | canvas.drawArc(arcBaseOffset & arcActualSize, 0, _sweep, false, backgroundPaint); |
624 | } |
625 | } |
626 | |
627 | if (year2023) { |
628 | if (value == null && strokeCap == null) { |
629 | // Indeterminate |
630 | paint.strokeCap = StrokeCap.square; |
631 | } else { |
632 | // Butt when determinate (value != null) && strokeCap == null; |
633 | paint.strokeCap = strokeCap ?? StrokeCap.butt; |
634 | } |
635 | } else { |
636 | paint.strokeCap = strokeCap ?? StrokeCap.round; |
637 | } |
638 | |
639 | canvas.drawArc(arcBaseOffset & arcActualSize, arcStart, arcSweep, false, paint); |
640 | } |
641 | |
642 | @override |
643 | bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) { |
644 | return oldPainter.trackColor != trackColor || |
645 | oldPainter.valueColor != valueColor || |
646 | oldPainter.value != value || |
647 | oldPainter.headValue != headValue || |
648 | oldPainter.tailValue != tailValue || |
649 | oldPainter.offsetValue != offsetValue || |
650 | oldPainter.rotationValue != rotationValue || |
651 | oldPainter.strokeWidth != strokeWidth || |
652 | oldPainter.strokeAlign != strokeAlign || |
653 | oldPainter.strokeCap != strokeCap || |
654 | oldPainter.trackGap != trackGap || |
655 | oldPainter.year2023 != year2023; |
656 | } |
657 | } |
658 | |
659 | /// A Material Design circular progress indicator, which spins to indicate that |
660 | /// the application is busy. |
661 | /// |
662 | /// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0} |
663 | /// |
664 | /// A widget that shows progress along a circle. There are two kinds of circular |
665 | /// progress indicators: |
666 | /// |
667 | /// * _Determinate_. Determinate progress indicators have a specific value at |
668 | /// each point in time, and the value should increase monotonically from 0.0 |
669 | /// to 1.0, at which time the indicator is complete. To create a determinate |
670 | /// progress indicator, use a non-null [value] between 0.0 and 1.0. |
671 | /// * _Indeterminate_. Indeterminate progress indicators do not have a specific |
672 | /// value at each point in time and instead indicate that progress is being |
673 | /// made without indicating how much progress remains. To create an |
674 | /// indeterminate progress indicator, use a null [value]. |
675 | /// |
676 | /// The indicator arc is displayed with [valueColor], an animated value. To |
677 | /// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`. |
678 | /// |
679 | /// {@tool dartpad} |
680 | /// This example showcases determinate and indeterminate [CircularProgressIndicator]s. |
681 | /// The [CircularProgressIndicator]s will use the  |
682 | /// when setting the [CircularProgressIndicator.year2023] flag to false. |
683 | /// |
684 | /// ** See code in examples/api/lib/material/progress_indicator/circular_progress_indicator.0.dart ** |
685 | /// {@end-tool} |
686 | /// |
687 | /// {@tool dartpad} |
688 | /// This sample shows the creation of a [CircularProgressIndicator] with a changing value. |
689 | /// When toggling the switch, [CircularProgressIndicator] uses a determinate value. |
690 | /// As described in: https://m3.material.io/components/progress-indicators/overview |
691 | /// |
692 | /// ** See code in examples/api/lib/material/progress_indicator/circular_progress_indicator.1.dart ** |
693 | /// {@end-tool} |
694 | /// |
695 | /// See also: |
696 | /// |
697 | /// * [LinearProgressIndicator], which displays progress along a line. |
698 | /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] |
699 | /// when the underlying vertical scrollable is overscrolled. |
700 | /// * <https://material.io/design/components/progress-indicators.html#circular-progress-indicators> |
701 | class CircularProgressIndicator extends ProgressIndicator { |
702 | /// Creates a circular progress indicator. |
703 | /// |
704 | /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} |
705 | const CircularProgressIndicator({ |
706 | super.key, |
707 | super.value, |
708 | super.backgroundColor, |
709 | super.color, |
710 | super.valueColor, |
711 | this.strokeWidth, |
712 | this.strokeAlign, |
713 | super.semanticsLabel, |
714 | super.semanticsValue, |
715 | this.strokeCap, |
716 | this.constraints, |
717 | this.trackGap, |
718 | @Deprecated( |
719 | 'Set this flag to false to opt into the 2024 progress indicator appearance. Defaults to true. ' |
720 | 'In the future, this flag will default to false. Use ProgressIndicatorThemeData to customize individual properties. ' |
721 | 'This feature was deprecated after v3.27.0-0.1.pre.' , |
722 | ) |
723 | this.year2023, |
724 | this.padding, |
725 | }) : _indicatorType = _ActivityIndicatorType.material; |
726 | |
727 | /// Creates an adaptive progress indicator that is a |
728 | /// [CupertinoActivityIndicator] on [TargetPlatform.iOS] & |
729 | /// [TargetPlatform.macOS] and a [CircularProgressIndicator] in material |
730 | /// theme/non-Apple platforms. |
731 | /// |
732 | /// The [valueColor], [strokeWidth], [strokeAlign], [strokeCap], |
733 | /// [semanticsLabel], [semanticsValue], [trackGap], [year2023] will be |
734 | /// ignored on iOS & macOS. |
735 | /// |
736 | /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} |
737 | const CircularProgressIndicator.adaptive({ |
738 | super.key, |
739 | super.value, |
740 | super.backgroundColor, |
741 | super.valueColor, |
742 | this.strokeWidth, |
743 | super.semanticsLabel, |
744 | super.semanticsValue, |
745 | this.strokeCap, |
746 | this.strokeAlign, |
747 | this.constraints, |
748 | this.trackGap, |
749 | @Deprecated( |
750 | 'Set this flag to false to opt into the 2024 progress indicator appearance. Defaults to true. ' |
751 | 'In the future, this flag will default to false. Use ProgressIndicatorThemeData to customize individual properties. ' |
752 | 'This feature was deprecated after v3.27.0-0.2.pre.' , |
753 | ) |
754 | this.year2023, |
755 | this.padding, |
756 | }) : _indicatorType = _ActivityIndicatorType.adaptive; |
757 | |
758 | final _ActivityIndicatorType _indicatorType; |
759 | |
760 | /// {@template flutter.material.CircularProgressIndicator.trackColor} |
761 | /// Color of the circular track being filled by the circular indicator. |
762 | /// |
763 | /// If [CircularProgressIndicator.backgroundColor] is null then the |
764 | /// ambient [ProgressIndicatorThemeData.circularTrackColor] will be used. |
765 | /// If that is null, then the track will not be painted. |
766 | /// {@endtemplate} |
767 | @override |
768 | Color? get backgroundColor => super.backgroundColor; |
769 | |
770 | /// The width of the line used to draw the circle. |
771 | final double? strokeWidth; |
772 | |
773 | /// The relative position of the stroke on a [CircularProgressIndicator]. |
774 | /// |
775 | /// Values typically range from -1.0 ([strokeAlignInside], inside stroke) |
776 | /// to 1.0 ([strokeAlignOutside], outside stroke), |
777 | /// without any bound constraints (e.g., a value of -2.0 is not typical, but allowed). |
778 | /// A value of 0 ([strokeAlignCenter]) will center the border |
779 | /// on the edge of the widget. |
780 | /// |
781 | /// If [year2023] is true, then the default value is [strokeAlignCenter]. |
782 | /// Otherwise, the default value is [strokeAlignInside]. |
783 | final double? strokeAlign; |
784 | |
785 | /// The progress indicator's line ending. |
786 | /// |
787 | /// This determines the shape of the stroke ends of the progress indicator. |
788 | /// By default, [strokeCap] is null. |
789 | /// When [value] is null (indeterminate), the stroke ends are set to |
790 | /// [StrokeCap.square]. When [value] is not null, the stroke |
791 | /// ends are set to [StrokeCap.butt]. |
792 | /// |
793 | /// Setting [strokeCap] to [StrokeCap.round] will result in a rounded end. |
794 | /// Setting [strokeCap] to [StrokeCap.butt] with [value] == null will result |
795 | /// in a slightly different indeterminate animation; the indicator completely |
796 | /// disappears and reappears on its minimum value. |
797 | /// Setting [strokeCap] to [StrokeCap.square] with [value] != null will |
798 | /// result in a different display of [value]. The indicator will start |
799 | /// drawing from slightly less than the start, and end slightly after |
800 | /// the end. This will produce an alternative result, as the |
801 | /// default behavior, for example, that a [value] of 0.5 starts at 90 degrees |
802 | /// and ends at 270 degrees. With [StrokeCap.square], it could start 85 |
803 | /// degrees and end at 275 degrees. |
804 | final StrokeCap? strokeCap; |
805 | |
806 | /// Defines minimum and maximum sizes for a [CircularProgressIndicator]. |
807 | /// |
808 | /// If null, then the [ProgressIndicatorThemeData.constraints] will be used. |
809 | /// Otherwise, defaults to a minimum width and height of 36 pixels. |
810 | final BoxConstraints? constraints; |
811 | |
812 | /// The gap between the active indicator and the background track. |
813 | /// |
814 | /// If [year2023] is false or [ThemeData.useMaterial3] is false, then no track |
815 | /// gap will be drawn. |
816 | /// |
817 | /// Set [trackGap] to 0 to hide the track gap. |
818 | /// |
819 | /// If null, then the [ProgressIndicatorThemeData.trackGap] will be used. |
820 | /// If that is null, then defaults to 4. |
821 | final double? trackGap; |
822 | |
823 | /// When true, the [CircularProgressIndicator] will use the 2023 Material Design 3 |
824 | /// appearance. |
825 | /// |
826 | /// If null, then the [ProgressIndicatorThemeData.year2023] will be used. |
827 | /// If that is null, then defaults to true. |
828 | /// |
829 | /// If this is set to false, the [CircularProgressIndicator] will use the |
830 | /// latest Material Design 3 appearance, which was introduced in December 2023. |
831 | /// |
832 | /// If [ThemeData.useMaterial3] is false, then this property is ignored. |
833 | @Deprecated( |
834 | 'Set this flag to false to opt into the 2024 progress indicator appearance. Defaults to true. ' |
835 | 'In the future, this flag will default to false. Use ProgressIndicatorThemeData to customize individual properties. ' |
836 | 'This feature was deprecated after v3.27.0-0.2.pre.' , |
837 | ) |
838 | final bool? year2023; |
839 | |
840 | /// The padding around the indicator track. |
841 | /// |
842 | /// If null, then the [ProgressIndicatorThemeData.circularTrackPadding] will be |
843 | /// used. If that is null and [year2023] is false, then defaults to `EdgeInsets.all(4.0)` |
844 | /// padding. Otherwise, defaults to zero padding. |
845 | final EdgeInsetsGeometry? padding; |
846 | |
847 | /// The indicator stroke is drawn fully inside of the indicator path. |
848 | /// |
849 | /// This is a constant for use with [strokeAlign]. |
850 | static const double strokeAlignInside = -1.0; |
851 | |
852 | /// The indicator stroke is drawn on the center of the indicator path, |
853 | /// with half of the [strokeWidth] on the inside, and the other half |
854 | /// on the outside of the path. |
855 | /// |
856 | /// This is a constant for use with [strokeAlign]. |
857 | /// |
858 | /// This is the default value for [strokeAlign]. |
859 | static const double strokeAlignCenter = 0.0; |
860 | |
861 | /// The indicator stroke is drawn on the outside of the indicator path. |
862 | /// |
863 | /// This is a constant for use with [strokeAlign]. |
864 | static const double strokeAlignOutside = 1.0; |
865 | |
866 | @override |
867 | State<CircularProgressIndicator> createState() => _CircularProgressIndicatorState(); |
868 | } |
869 | |
870 | class _CircularProgressIndicatorState extends State<CircularProgressIndicator> |
871 | with SingleTickerProviderStateMixin { |
872 | static const int _pathCount = _kIndeterminateCircularDuration ~/ 1333; |
873 | static const int _rotationCount = _kIndeterminateCircularDuration ~/ 2222; |
874 | |
875 | static final Animatable<double> _strokeHeadTween = CurveTween( |
876 | curve: const Interval(0.0, 0.5, curve: Curves.fastOutSlowIn), |
877 | ).chain(CurveTween(curve: const SawTooth(_pathCount))); |
878 | static final Animatable<double> _strokeTailTween = CurveTween( |
879 | curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), |
880 | ).chain(CurveTween(curve: const SawTooth(_pathCount))); |
881 | static final Animatable<double> _offsetTween = CurveTween(curve: const SawTooth(_pathCount)); |
882 | static final Animatable<double> _rotationTween = CurveTween( |
883 | curve: const SawTooth(_rotationCount), |
884 | ); |
885 | |
886 | late AnimationController _controller; |
887 | |
888 | @override |
889 | void initState() { |
890 | super.initState(); |
891 | _controller = AnimationController( |
892 | duration: const Duration(milliseconds: _kIndeterminateCircularDuration), |
893 | vsync: this, |
894 | ); |
895 | if (widget.value == null) { |
896 | _controller.repeat(); |
897 | } |
898 | } |
899 | |
900 | @override |
901 | void didUpdateWidget(CircularProgressIndicator oldWidget) { |
902 | super.didUpdateWidget(oldWidget); |
903 | if (widget.value == null && !_controller.isAnimating) { |
904 | _controller.repeat(); |
905 | } else if (widget.value != null && _controller.isAnimating) { |
906 | _controller.stop(); |
907 | } |
908 | } |
909 | |
910 | @override |
911 | void dispose() { |
912 | _controller.dispose(); |
913 | super.dispose(); |
914 | } |
915 | |
916 | Widget _buildCupertinoIndicator(BuildContext context) { |
917 | final Color? tickColor = widget.backgroundColor; |
918 | final double? value = widget.value; |
919 | if (value == null) { |
920 | return CupertinoActivityIndicator(key: widget.key, color: tickColor); |
921 | } |
922 | return CupertinoActivityIndicator.partiallyRevealed( |
923 | key: widget.key, |
924 | color: tickColor, |
925 | progress: value, |
926 | ); |
927 | } |
928 | |
929 | Widget _buildMaterialIndicator( |
930 | BuildContext context, |
931 | double headValue, |
932 | double tailValue, |
933 | double offsetValue, |
934 | double rotationValue, |
935 | ) { |
936 | final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); |
937 | final bool year2023 = widget.year2023 ?? indicatorTheme.year2023 ?? true; |
938 | final ProgressIndicatorThemeData defaults = switch (Theme.of(context).useMaterial3) { |
939 | true => |
940 | year2023 |
941 | ? _CircularProgressIndicatorDefaultsM3Year2023( |
942 | context, |
943 | indeterminate: widget.value == null, |
944 | ) |
945 | : _CircularProgressIndicatorDefaultsM3(context, indeterminate: widget.value == null), |
946 | false => _CircularProgressIndicatorDefaultsM2(context, indeterminate: widget.value == null), |
947 | }; |
948 | final Color? trackColor = |
949 | widget.backgroundColor ?? indicatorTheme.circularTrackColor ?? defaults.circularTrackColor; |
950 | final double strokeWidth = |
951 | widget.strokeWidth ?? indicatorTheme.strokeWidth ?? defaults.strokeWidth!; |
952 | final double strokeAlign = |
953 | widget.strokeAlign ?? indicatorTheme.strokeAlign ?? defaults.strokeAlign!; |
954 | final StrokeCap? strokeCap = widget.strokeCap ?? indicatorTheme.strokeCap; |
955 | final BoxConstraints constraints = |
956 | widget.constraints ?? indicatorTheme.constraints ?? defaults.constraints!; |
957 | final double? trackGap = year2023 |
958 | ? null |
959 | : widget.trackGap ?? indicatorTheme.trackGap ?? defaults.trackGap; |
960 | final EdgeInsetsGeometry? effectivePadding = |
961 | widget.padding ?? indicatorTheme.circularTrackPadding ?? defaults.circularTrackPadding; |
962 | |
963 | Widget result = ConstrainedBox( |
964 | constraints: constraints, |
965 | child: CustomPaint( |
966 | painter: _CircularProgressIndicatorPainter( |
967 | trackColor: trackColor, |
968 | valueColor: widget._getValueColor(context, defaultColor: defaults.color), |
969 | value: widget.value, // may be null |
970 | headValue: headValue, // remaining arguments are ignored if widget.value is not null |
971 | tailValue: tailValue, |
972 | offsetValue: offsetValue, |
973 | rotationValue: rotationValue, |
974 | strokeWidth: strokeWidth, |
975 | strokeAlign: strokeAlign, |
976 | strokeCap: strokeCap, |
977 | trackGap: trackGap, |
978 | year2023: year2023, |
979 | ), |
980 | ), |
981 | ); |
982 | |
983 | if (effectivePadding != null) { |
984 | result = Padding(padding: effectivePadding, child: result); |
985 | } |
986 | |
987 | return widget._buildSemanticsWrapper(context: context, child: result); |
988 | } |
989 | |
990 | Widget _buildAnimation() { |
991 | return AnimatedBuilder( |
992 | animation: _controller, |
993 | builder: (BuildContext context, Widget? child) { |
994 | return _buildMaterialIndicator( |
995 | context, |
996 | _strokeHeadTween.evaluate(_controller), |
997 | _strokeTailTween.evaluate(_controller), |
998 | _offsetTween.evaluate(_controller), |
999 | _rotationTween.evaluate(_controller), |
1000 | ); |
1001 | }, |
1002 | ); |
1003 | } |
1004 | |
1005 | @override |
1006 | Widget build(BuildContext context) { |
1007 | switch (widget._indicatorType) { |
1008 | case _ActivityIndicatorType.material: |
1009 | if (widget.value != null) { |
1010 | return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0); |
1011 | } |
1012 | return _buildAnimation(); |
1013 | case _ActivityIndicatorType.adaptive: |
1014 | final ThemeData theme = Theme.of(context); |
1015 | switch (theme.platform) { |
1016 | case TargetPlatform.iOS: |
1017 | case TargetPlatform.macOS: |
1018 | return _buildCupertinoIndicator(context); |
1019 | case TargetPlatform.android: |
1020 | case TargetPlatform.fuchsia: |
1021 | case TargetPlatform.linux: |
1022 | case TargetPlatform.windows: |
1023 | if (widget.value != null) { |
1024 | return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0); |
1025 | } |
1026 | return _buildAnimation(); |
1027 | } |
1028 | } |
1029 | } |
1030 | } |
1031 | |
1032 | class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter { |
1033 | _RefreshProgressIndicatorPainter({ |
1034 | required super.valueColor, |
1035 | required super.value, |
1036 | required super.headValue, |
1037 | required super.tailValue, |
1038 | required super.offsetValue, |
1039 | required super.rotationValue, |
1040 | required super.strokeWidth, |
1041 | required super.strokeAlign, |
1042 | required this.arrowheadScale, |
1043 | required super.strokeCap, |
1044 | }); |
1045 | |
1046 | final double arrowheadScale; |
1047 | |
1048 | void paintArrowhead(Canvas canvas, Size size) { |
1049 | // ux, uy: a unit vector whose direction parallels the base of the arrowhead. |
1050 | // (So ux, -uy points in the direction the arrowhead points.) |
1051 | final double arcEnd = arcStart + arcSweep; |
1052 | final double ux = math.cos(arcEnd); |
1053 | final double uy = math.sin(arcEnd); |
1054 | |
1055 | assert(size.width == size.height); |
1056 | final double radius = size.width / 2.0; |
1057 | final double arrowheadPointX = radius + ux * radius + -uy * strokeWidth * 2.0 * arrowheadScale; |
1058 | final double arrowheadPointY = radius + uy * radius + ux * strokeWidth * 2.0 * arrowheadScale; |
1059 | final double arrowheadRadius = strokeWidth * 2.0 * arrowheadScale; |
1060 | final double innerRadius = radius - arrowheadRadius; |
1061 | final double outerRadius = radius + arrowheadRadius; |
1062 | |
1063 | final Path path = Path() |
1064 | ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius) |
1065 | ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius) |
1066 | ..lineTo(arrowheadPointX, arrowheadPointY) |
1067 | ..close(); |
1068 | |
1069 | final Paint paint = Paint() |
1070 | ..color = valueColor |
1071 | ..strokeWidth = strokeWidth |
1072 | ..style = PaintingStyle.fill; |
1073 | canvas.drawPath(path, paint); |
1074 | } |
1075 | |
1076 | @override |
1077 | void paint(Canvas canvas, Size size) { |
1078 | super.paint(canvas, size); |
1079 | if (arrowheadScale > 0.0) { |
1080 | paintArrowhead(canvas, size); |
1081 | } |
1082 | } |
1083 | } |
1084 | |
1085 | /// An indicator for the progress of refreshing the contents of a widget. |
1086 | /// |
1087 | /// Typically used for swipe-to-refresh interactions. See [RefreshIndicator] for |
1088 | /// a complete implementation of swipe-to-refresh driven by a [Scrollable] |
1089 | /// widget. |
1090 | /// |
1091 | /// The indicator arc is displayed with [valueColor], an animated value. To |
1092 | /// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`. |
1093 | /// |
1094 | /// See also: |
1095 | /// |
1096 | /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] |
1097 | /// when the underlying vertical scrollable is overscrolled. |
1098 | class RefreshProgressIndicator extends CircularProgressIndicator { |
1099 | /// Creates a refresh progress indicator. |
1100 | /// |
1101 | /// Rather than creating a refresh progress indicator directly, consider using |
1102 | /// a [RefreshIndicator] together with a [Scrollable] widget. |
1103 | /// |
1104 | /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} |
1105 | const RefreshProgressIndicator({ |
1106 | super.key, |
1107 | super.value, |
1108 | super.backgroundColor, |
1109 | super.color, |
1110 | super.valueColor, |
1111 | super.strokeWidth = defaultStrokeWidth, // Different default than CircularProgressIndicator. |
1112 | super.strokeAlign, |
1113 | super.semanticsLabel, |
1114 | super.semanticsValue, |
1115 | super.strokeCap, |
1116 | this.elevation = 2.0, |
1117 | this.indicatorMargin = const EdgeInsets.all(4.0), |
1118 | this.indicatorPadding = const EdgeInsets.all(12.0), |
1119 | }); |
1120 | |
1121 | /// {@macro flutter.material.material.elevation} |
1122 | final double elevation; |
1123 | |
1124 | /// The amount of space by which to inset the whole indicator. |
1125 | /// It accommodates the [elevation] of the indicator. |
1126 | final EdgeInsetsGeometry indicatorMargin; |
1127 | |
1128 | /// The amount of space by which to inset the inner refresh indicator. |
1129 | final EdgeInsetsGeometry indicatorPadding; |
1130 | |
1131 | /// Default stroke width. |
1132 | static const double defaultStrokeWidth = 2.5; |
1133 | |
1134 | /// {@template flutter.material.RefreshProgressIndicator.backgroundColor} |
1135 | /// Background color of that fills the circle under the refresh indicator. |
1136 | /// |
1137 | /// If [RefreshIndicator.backgroundColor] is null then the |
1138 | /// ambient [ProgressIndicatorThemeData.refreshBackgroundColor] will be used. |
1139 | /// If that is null, then the ambient theme's [ThemeData.canvasColor] |
1140 | /// will be used. |
1141 | /// {@endtemplate} |
1142 | @override |
1143 | Color? get backgroundColor => super.backgroundColor; |
1144 | |
1145 | @override |
1146 | State<CircularProgressIndicator> createState() => _RefreshProgressIndicatorState(); |
1147 | } |
1148 | |
1149 | class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { |
1150 | static const double _indicatorSize = 41.0; |
1151 | |
1152 | /// Interval for arrow head to fully grow. |
1153 | static const double _strokeHeadInterval = 0.33; |
1154 | |
1155 | late final Animatable<double> _convertTween = CurveTween( |
1156 | curve: const Interval(0.1, _strokeHeadInterval), |
1157 | ); |
1158 | |
1159 | late final Animatable<double> _additionalRotationTween = TweenSequence<double>( |
1160 | <TweenSequenceItem<double>>[ |
1161 | // Makes arrow to expand a little bit earlier, to match the Android look. |
1162 | TweenSequenceItem<double>( |
1163 | tween: Tween<double>(begin: -0.1, end: -0.2), |
1164 | weight: _strokeHeadInterval, |
1165 | ), |
1166 | // Additional rotation after the arrow expanded |
1167 | TweenSequenceItem<double>( |
1168 | tween: Tween<double>(begin: -0.2, end: 1.35), |
1169 | weight: 1 - _strokeHeadInterval, |
1170 | ), |
1171 | ], |
1172 | ); |
1173 | |
1174 | // Last value received from the widget before null. |
1175 | double? _lastValue; |
1176 | |
1177 | /// Force casting the widget as [RefreshProgressIndicator]. |
1178 | @override |
1179 | RefreshProgressIndicator get widget => super.widget as RefreshProgressIndicator; |
1180 | |
1181 | // Always show the indeterminate version of the circular progress indicator. |
1182 | // |
1183 | // When value is non-null the sweep of the progress indicator arrow's arc |
1184 | // varies from 0 to about 300 degrees. |
1185 | // |
1186 | // When value is null the arrow animation starting from wherever we left it. |
1187 | @override |
1188 | Widget build(BuildContext context) { |
1189 | final double? value = widget.value; |
1190 | if (value != null) { |
1191 | _lastValue = value; |
1192 | _controller.value = |
1193 | _convertTween.transform(value) * (1333 / 2 / _kIndeterminateCircularDuration); |
1194 | } |
1195 | return _buildAnimation(); |
1196 | } |
1197 | |
1198 | @override |
1199 | Widget _buildAnimation() { |
1200 | return AnimatedBuilder( |
1201 | animation: _controller, |
1202 | builder: (BuildContext context, Widget? child) { |
1203 | return _buildMaterialIndicator( |
1204 | context, |
1205 | // Lengthen the arc a little |
1206 | 1.05 * _CircularProgressIndicatorState._strokeHeadTween.evaluate(_controller), |
1207 | _CircularProgressIndicatorState._strokeTailTween.evaluate(_controller), |
1208 | _CircularProgressIndicatorState._offsetTween.evaluate(_controller), |
1209 | _CircularProgressIndicatorState._rotationTween.evaluate(_controller), |
1210 | ); |
1211 | }, |
1212 | ); |
1213 | } |
1214 | |
1215 | @override |
1216 | Widget _buildMaterialIndicator( |
1217 | BuildContext context, |
1218 | double headValue, |
1219 | double tailValue, |
1220 | double offsetValue, |
1221 | double rotationValue, |
1222 | ) { |
1223 | final double? value = widget.value; |
1224 | final double arrowheadScale = value == null |
1225 | ? 0.0 |
1226 | : const Interval(0.1, _strokeHeadInterval).transform(value); |
1227 | final double rotation; |
1228 | |
1229 | if (value == null && _lastValue == null) { |
1230 | rotation = 0.0; |
1231 | } else { |
1232 | rotation = math.pi * _additionalRotationTween.transform(value ?? _lastValue!); |
1233 | } |
1234 | |
1235 | Color valueColor = widget._getValueColor(context); |
1236 | final double opacity = valueColor.opacity; |
1237 | valueColor = valueColor.withOpacity(1.0); |
1238 | |
1239 | final ProgressIndicatorThemeData defaults = switch (Theme.of(context).useMaterial3) { |
1240 | true => _CircularProgressIndicatorDefaultsM3Year2023(context, indeterminate: value == null), |
1241 | false => _CircularProgressIndicatorDefaultsM2(context, indeterminate: value == null), |
1242 | }; |
1243 | final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); |
1244 | final Color backgroundColor = |
1245 | widget.backgroundColor ?? |
1246 | indicatorTheme.refreshBackgroundColor ?? |
1247 | Theme.of(context).canvasColor; |
1248 | final double strokeWidth = |
1249 | widget.strokeWidth ?? indicatorTheme.strokeWidth ?? defaults.strokeWidth!; |
1250 | final double strokeAlign = |
1251 | widget.strokeAlign ?? indicatorTheme.strokeAlign ?? defaults.strokeAlign!; |
1252 | final StrokeCap? strokeCap = widget.strokeCap ?? indicatorTheme.strokeCap; |
1253 | |
1254 | return widget._buildSemanticsWrapper( |
1255 | context: context, |
1256 | child: Padding( |
1257 | padding: widget.indicatorMargin, |
1258 | child: SizedBox.fromSize( |
1259 | size: const Size.square(_indicatorSize), |
1260 | child: Material( |
1261 | type: MaterialType.circle, |
1262 | color: backgroundColor, |
1263 | elevation: widget.elevation, |
1264 | child: Padding( |
1265 | padding: widget.indicatorPadding, |
1266 | child: Opacity( |
1267 | opacity: opacity, |
1268 | child: Transform.rotate( |
1269 | angle: rotation, |
1270 | child: CustomPaint( |
1271 | painter: _RefreshProgressIndicatorPainter( |
1272 | valueColor: valueColor, |
1273 | value: null, // Draw the indeterminate progress indicator. |
1274 | headValue: headValue, |
1275 | tailValue: tailValue, |
1276 | offsetValue: offsetValue, |
1277 | rotationValue: rotationValue, |
1278 | strokeWidth: strokeWidth, |
1279 | strokeAlign: strokeAlign, |
1280 | arrowheadScale: arrowheadScale, |
1281 | strokeCap: strokeCap, |
1282 | ), |
1283 | ), |
1284 | ), |
1285 | ), |
1286 | ), |
1287 | ), |
1288 | ), |
1289 | ), |
1290 | ); |
1291 | } |
1292 | } |
1293 | |
1294 | // Hand coded defaults based on Material Design 2. |
1295 | class _CircularProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData { |
1296 | _CircularProgressIndicatorDefaultsM2(this.context, {required this.indeterminate}); |
1297 | |
1298 | final BuildContext context; |
1299 | late final ColorScheme _colors = Theme.of(context).colorScheme; |
1300 | final bool indeterminate; |
1301 | |
1302 | @override |
1303 | Color get color => _colors.primary; |
1304 | |
1305 | @override |
1306 | double? get strokeWidth => 4.0; |
1307 | |
1308 | @override |
1309 | double? get strokeAlign => CircularProgressIndicator.strokeAlignCenter; |
1310 | |
1311 | @override |
1312 | BoxConstraints get constraints => const BoxConstraints(minWidth: 36.0, minHeight: 36.0); |
1313 | } |
1314 | |
1315 | class _LinearProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData { |
1316 | _LinearProgressIndicatorDefaultsM2(this.context); |
1317 | |
1318 | final BuildContext context; |
1319 | late final ColorScheme _colors = Theme.of(context).colorScheme; |
1320 | |
1321 | @override |
1322 | Color get color => _colors.primary; |
1323 | |
1324 | @override |
1325 | Color get linearTrackColor => _colors.background; |
1326 | |
1327 | @override |
1328 | double get linearMinHeight => 4.0; |
1329 | } |
1330 | |
1331 | class _CircularProgressIndicatorDefaultsM3Year2023 extends ProgressIndicatorThemeData { |
1332 | _CircularProgressIndicatorDefaultsM3Year2023(this.context, {required this.indeterminate}); |
1333 | |
1334 | final BuildContext context; |
1335 | late final ColorScheme _colors = Theme.of(context).colorScheme; |
1336 | final bool indeterminate; |
1337 | |
1338 | @override |
1339 | Color get color => _colors.primary; |
1340 | |
1341 | @override |
1342 | double get strokeWidth => 4.0; |
1343 | |
1344 | @override |
1345 | double? get strokeAlign => CircularProgressIndicator.strokeAlignCenter; |
1346 | |
1347 | @override |
1348 | BoxConstraints get constraints => const BoxConstraints(minWidth: 36.0, minHeight: 36.0); |
1349 | } |
1350 | |
1351 | class _LinearProgressIndicatorDefaultsM3Year2023 extends ProgressIndicatorThemeData { |
1352 | _LinearProgressIndicatorDefaultsM3Year2023(this.context); |
1353 | |
1354 | final BuildContext context; |
1355 | late final ColorScheme _colors = Theme.of(context).colorScheme; |
1356 | |
1357 | @override |
1358 | Color get color => _colors.primary; |
1359 | |
1360 | @override |
1361 | Color get linearTrackColor => _colors.secondaryContainer; |
1362 | |
1363 | @override |
1364 | double get linearMinHeight => 4.0; |
1365 | } |
1366 | |
1367 | // BEGIN GENERATED TOKEN PROPERTIES - ProgressIndicator |
1368 | |
1369 | // Do not edit by hand. The code between the "BEGIN GENERATED" and |
1370 | // "END GENERATED" comments are generated from data in the Material |
1371 | // Design token database by the script: |
1372 | // dev/tools/gen_defaults/bin/gen_defaults.dart. |
1373 | |
1374 | // dart format off |
1375 | class _CircularProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData { |
1376 | _CircularProgressIndicatorDefaultsM3(this.context, { required this.indeterminate }); |
1377 | |
1378 | final BuildContext context; |
1379 | late final ColorScheme _colors = Theme.of(context).colorScheme; |
1380 | final bool indeterminate; |
1381 | |
1382 | @override |
1383 | Color get color => _colors.primary; |
1384 | |
1385 | @override |
1386 | Color? get circularTrackColor => indeterminate ? null : _colors.secondaryContainer; |
1387 | |
1388 | @override |
1389 | double get strokeWidth => 4.0; |
1390 | |
1391 | @override |
1392 | double? get strokeAlign => CircularProgressIndicator.strokeAlignInside; |
1393 | |
1394 | @override |
1395 | BoxConstraints get constraints => const BoxConstraints( |
1396 | minWidth: 40.0, |
1397 | minHeight: 40.0, |
1398 | ); |
1399 | |
1400 | @override |
1401 | double? get trackGap => 4.0; |
1402 | |
1403 | @override |
1404 | EdgeInsetsGeometry? get circularTrackPadding => const EdgeInsets.all(4.0); |
1405 | } |
1406 | |
1407 | class _LinearProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData { |
1408 | _LinearProgressIndicatorDefaultsM3(this.context); |
1409 | |
1410 | final BuildContext context; |
1411 | late final ColorScheme _colors = Theme.of(context).colorScheme; |
1412 | |
1413 | @override |
1414 | Color get color => _colors.primary; |
1415 | |
1416 | @override |
1417 | Color get linearTrackColor => _colors.secondaryContainer; |
1418 | |
1419 | @override |
1420 | double get linearMinHeight => 4.0; |
1421 | |
1422 | @override |
1423 | BorderRadius get borderRadius => BorderRadius.circular(4.0 / 2); |
1424 | |
1425 | @override |
1426 | Color get stopIndicatorColor => _colors.primary; |
1427 | |
1428 | @override |
1429 | double? get stopIndicatorRadius => 4.0 / 2; |
1430 | |
1431 | @override |
1432 | double? get trackGap => 4.0; |
1433 | } |
1434 | // dart format on |
1435 | |
1436 | // END GENERATED TOKEN PROPERTIES - ProgressIndicator |
1437 | |