// 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. import 'dart:math' as math; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'color_scheme.dart'; import 'material.dart'; import 'progress_indicator_theme.dart'; import 'theme.dart'; const double _kMinCircularProgressIndicatorSize = 36.0; const int _kIndeterminateLinearDuration = 1800; const int _kIndeterminateCircularDuration = 1333 * 2222; enum _ActivityIndicatorType { material, adaptive } /// A base class for Material Design progress indicators. /// /// This widget cannot be instantiated directly. For a linear progress /// indicator, see [LinearProgressIndicator]. For a circular progress indicator, /// see [CircularProgressIndicator]. /// /// See also: /// /// * <https://material.io/components/progress-indicators> abstract class ProgressIndicator extends StatefulWidget { /// Creates a progress indicator. /// /// {@template flutter.material.ProgressIndicator.ProgressIndicator} /// The [value] argument can either be null for an indeterminate /// progress indicator, or a non-null value between 0.0 and 1.0 for a /// determinate progress indicator. /// /// ## Accessibility /// /// The [semanticsLabel] can be used to identify the purpose of this progress /// bar for screen reading software. The [semanticsValue] property may be used /// for determinate progress indicators to indicate how much progress has been made. /// {@endtemplate} const ProgressIndicator({ super.key, this.value, this.backgroundColor, this.color, this.valueColor, this.semanticsLabel, this.semanticsValue, }); /// If non-null, the value of this progress indicator. /// /// A value of 0.0 means no progress and 1.0 means that progress is complete. /// The value will be clamped to be in the range 0.0-1.0. /// /// If null, this progress indicator is indeterminate, which means the /// indicator displays a predetermined animation that does not indicate how /// much actual progress is being made. final double? value; /// The progress indicator's background color. /// /// It is up to the subclass to implement this in whatever way makes sense /// for the given use case. See the subclass documentation for details. final Color? backgroundColor; /// {@template flutter.progress_indicator.ProgressIndicator.color} /// The progress indicator's color. /// /// This is only used if [ProgressIndicator.valueColor] is null. /// If [ProgressIndicator.color] is also null, then the ambient /// [ProgressIndicatorThemeData.color] will be used. If that /// is null then the current theme's [ColorScheme.primary] will /// be used by default. /// {@endtemplate} final Color? color; /// The progress indicator's color as an animated value. /// /// If null, the progress indicator is rendered with [color]. If that is null, /// then it will use the ambient [ProgressIndicatorThemeData.color]. If that /// is also null then it defaults to the current theme's [ColorScheme.primary]. final Animation<Color?>? valueColor; /// {@template flutter.progress_indicator.ProgressIndicator.semanticsLabel} /// The [SemanticsProperties.label] for this progress indicator. /// /// This value indicates the purpose of the progress bar, and will be /// read out by screen readers to indicate the purpose of this progress /// indicator. /// {@endtemplate} final String? semanticsLabel; /// {@template flutter.progress_indicator.ProgressIndicator.semanticsValue} /// The [SemanticsProperties.value] for this progress indicator. /// /// This will be used in conjunction with the [semanticsLabel] by /// screen reading software to identify the widget, and is primarily /// intended for use with determinate progress indicators to announce /// how far along they are. /// /// For determinate progress indicators, this will be defaulted to /// [ProgressIndicator.value] expressed as a percentage, i.e. `0.1` will /// become '10%'. /// {@endtemplate} final String? semanticsValue; Color _getValueColor(BuildContext context, {Color? defaultColor}) { return valueColor?.value ?? color ?? ProgressIndicatorTheme.of(context).color ?? defaultColor ?? Theme.of(context).colorScheme.primary; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(PercentProperty('value', value, showName: false, ifNull: '<indeterminate>')); } Widget _buildSemanticsWrapper({ required BuildContext context, required Widget child, }) { String? expandedSemanticsValue = semanticsValue; if (value != null) { expandedSemanticsValue ??= '${(value! * 100).round()}%'; } return Semantics( label: semanticsLabel, value: expandedSemanticsValue, child: child, ); } } class _LinearProgressIndicatorPainter extends CustomPainter { const _LinearProgressIndicatorPainter({ required this.backgroundColor, required this.valueColor, this.value, required this.animationValue, required this.textDirection, }) : assert(textDirection != null); final Color backgroundColor; final Color valueColor; final double? value; final double animationValue; final TextDirection textDirection; // The indeterminate progress animation displays two lines whose leading (head) // and trailing (tail) endpoints are defined by the following four curves. static const Curve line1Head = Interval( 0.0, 750.0 / _kIndeterminateLinearDuration, curve: Cubic(0.2, 0.0, 0.8, 1.0), ); static const Curve line1Tail = Interval( 333.0 / _kIndeterminateLinearDuration, (333.0 + 750.0) / _kIndeterminateLinearDuration, curve: Cubic(0.4, 0.0, 1.0, 1.0), ); static const Curve line2Head = Interval( 1000.0 / _kIndeterminateLinearDuration, (1000.0 + 567.0) / _kIndeterminateLinearDuration, curve: Cubic(0.0, 0.0, 0.65, 1.0), ); static const Curve line2Tail = Interval( 1267.0 / _kIndeterminateLinearDuration, (1267.0 + 533.0) / _kIndeterminateLinearDuration, curve: Cubic(0.10, 0.0, 0.45, 1.0), ); @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = backgroundColor ..style = PaintingStyle.fill; canvas.drawRect(Offset.zero & size, paint); paint.color = valueColor; void drawBar(double x, double width) { if (width <= 0.0) { return; } final double left; switch (textDirection) { case TextDirection.rtl: left = size.width - width - x; break; case TextDirection.ltr: left = x; break; } canvas.drawRect(Offset(left, 0.0) & Size(width, size.height), paint); } if (value != null) { drawBar(0.0, clampDouble(value!, 0.0, 1.0) * size.width); } else { final double x1 = size.width * line1Tail.transform(animationValue); final double width1 = size.width * line1Head.transform(animationValue) - x1; final double x2 = size.width * line2Tail.transform(animationValue); final double width2 = size.width * line2Head.transform(animationValue) - x2; drawBar(x1, width1); drawBar(x2, width2); } } @override bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) { return oldPainter.backgroundColor != backgroundColor || oldPainter.valueColor != valueColor || oldPainter.value != value || oldPainter.animationValue != animationValue || oldPainter.textDirection != textDirection; } } /// A Material Design linear progress indicator, also known as a progress bar. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0} /// /// A widget that shows progress along a line. There are two kinds of linear /// progress indicators: /// /// * _Determinate_. Determinate progress indicators have a specific value at /// each point in time, and the value should increase monotonically from 0.0 /// to 1.0, at which time the indicator is complete. To create a determinate /// progress indicator, use a non-null [value] between 0.0 and 1.0. /// * _Indeterminate_. Indeterminate progress indicators do not have a specific /// value at each point in time and instead indicate that progress is being /// made without indicating how much progress remains. To create an /// indeterminate progress indicator, use a null [value]. /// /// The indicator line is displayed with [valueColor], an animated value. To /// specify a constant color value use: `AlwaysStoppedAnimation<Color>(color)`. /// /// The minimum height of the indicator can be specified using [minHeight]. /// The indicator can be made taller by wrapping the widget with a [SizedBox]. /// /// {@tool dartpad} /// This example shows a [LinearProgressIndicator] with a changing value. /// /// ** See code in examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This sample shows the creation of a [LinearProgressIndicator] with a changing value. /// When toggling the switch, [LinearProgressIndicator] uses a determinate value. /// As described in: https://m3.material.io/components/progress-indicators/overview /// /// ** See code in examples/api/lib/material/progress_indicator/linear_progress_indicator.1.dart ** /// {@end-tool} /// /// See also: /// /// * [CircularProgressIndicator], which shows progress along a circular arc. /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] /// when the underlying vertical scrollable is overscrolled. /// * <https://material.io/design/components/progress-indicators.html#linear-progress-indicators> class LinearProgressIndicator extends ProgressIndicator { /// Creates a linear progress indicator. /// /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} const LinearProgressIndicator({ super.key, super.value, super.backgroundColor, super.color, super.valueColor, this.minHeight, super.semanticsLabel, super.semanticsValue, }) : assert(minHeight == null || minHeight > 0); /// {@template flutter.material.LinearProgressIndicator.trackColor} /// Color of the track being filled by the linear indicator. /// /// If [LinearProgressIndicator.backgroundColor] is null then the /// ambient [ProgressIndicatorThemeData.linearTrackColor] will be used. /// If that is null, then the ambient theme's [ColorScheme.background] /// will be used to draw the track. /// {@endtemplate} @override Color? get backgroundColor => super.backgroundColor; /// {@template flutter.material.LinearProgressIndicator.minHeight} /// The minimum height of the line used to draw the linear indicator. /// /// If [LinearProgressIndicator.minHeight] is null then it will use the /// ambient [ProgressIndicatorThemeData.linearMinHeight]. If that is null /// it will use 4dp. /// {@endtemplate} final double? minHeight; @override State<LinearProgressIndicator> createState() => _LinearProgressIndicatorState(); } class _LinearProgressIndicatorState extends State<LinearProgressIndicator> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: _kIndeterminateLinearDuration), vsync: this, ); if (widget.value == null) { _controller.repeat(); } } @override void didUpdateWidget(LinearProgressIndicator oldWidget) { super.didUpdateWidget(oldWidget); if (widget.value == null && !_controller.isAnimating) { _controller.repeat(); } else if (widget.value != null && _controller.isAnimating) { _controller.stop(); } } @override void dispose() { _controller.dispose(); super.dispose(); } Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) { final ProgressIndicatorThemeData defaults = Theme.of(context).useMaterial3 ? _LinearProgressIndicatorDefaultsM3(context) : _LinearProgressIndicatorDefaultsM2(context); final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); final Color trackColor = widget.backgroundColor ?? indicatorTheme.linearTrackColor ?? defaults.linearTrackColor!; final double minHeight = widget.minHeight ?? indicatorTheme.linearMinHeight ?? defaults.linearMinHeight!; return widget._buildSemanticsWrapper( context: context, child: Container( constraints: BoxConstraints( minWidth: double.infinity, minHeight: minHeight, ), child: CustomPaint( painter: _LinearProgressIndicatorPainter( backgroundColor: trackColor, valueColor: widget._getValueColor(context, defaultColor: defaults.color), value: widget.value, // may be null animationValue: animationValue, // ignored if widget.value is not null textDirection: textDirection, ), ), ), ); } @override Widget build(BuildContext context) { final TextDirection textDirection = Directionality.of(context); if (widget.value != null) { return _buildIndicator(context, _controller.value, textDirection); } return AnimatedBuilder( animation: _controller.view, builder: (BuildContext context, Widget? child) { return _buildIndicator(context, _controller.value, textDirection); }, ); } } class _CircularProgressIndicatorPainter extends CustomPainter { _CircularProgressIndicatorPainter({ this.backgroundColor, required this.valueColor, required this.value, required this.headValue, required this.tailValue, required this.offsetValue, required this.rotationValue, required this.strokeWidth, }) : arcStart = value != null ? _startAngle : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 2.0 + offsetValue * 0.5 * math.pi, arcSweep = value != null ? clampDouble(value, 0.0, 1.0) * _sweep : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon); final Color? backgroundColor; final Color valueColor; final double? value; final double headValue; final double tailValue; final double offsetValue; final double rotationValue; final double strokeWidth; final double arcStart; final double arcSweep; static const double _twoPi = math.pi * 2.0; static const double _epsilon = .001; // Canvas.drawArc(r, 0, 2*PI) doesn't draw anything, so just get close. static const double _sweep = _twoPi - _epsilon; static const double _startAngle = -math.pi / 2.0; @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = valueColor ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; if (backgroundColor != null) { final Paint backgroundPaint = Paint() ..color = backgroundColor! ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke; canvas.drawArc(Offset.zero & size, 0, _sweep, false, backgroundPaint); } if (value == null) { // Indeterminate paint.strokeCap = StrokeCap.square; } canvas.drawArc(Offset.zero & size, arcStart, arcSweep, false, paint); } @override bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) { return oldPainter.backgroundColor != backgroundColor || oldPainter.valueColor != valueColor || oldPainter.value != value || oldPainter.headValue != headValue || oldPainter.tailValue != tailValue || oldPainter.offsetValue != offsetValue || oldPainter.rotationValue != rotationValue || oldPainter.strokeWidth != strokeWidth; } } /// A Material Design circular progress indicator, which spins to indicate that /// the application is busy. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0} /// /// A widget that shows progress along a circle. There are two kinds of circular /// progress indicators: /// /// * _Determinate_. Determinate progress indicators have a specific value at /// each point in time, and the value should increase monotonically from 0.0 /// to 1.0, at which time the indicator is complete. To create a determinate /// progress indicator, use a non-null [value] between 0.0 and 1.0. /// * _Indeterminate_. Indeterminate progress indicators do not have a specific /// value at each point in time and instead indicate that progress is being /// made without indicating how much progress remains. To create an /// indeterminate progress indicator, use a null [value]. /// /// The indicator arc is displayed with [valueColor], an animated value. To /// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`. /// /// {@tool dartpad} /// This example shows a [CircularProgressIndicator] with a changing value. /// /// ** See code in examples/api/lib/material/progress_indicator/circular_progress_indicator.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This sample shows the creation of a [CircularProgressIndicator] with a changing value. /// When toggling the switch, [CircularProgressIndicator] uses a determinate value. /// As described in: https://m3.material.io/components/progress-indicators/overview /// /// ** See code in examples/api/lib/material/progress_indicator/circular_progress_indicator.1.dart ** /// {@end-tool} /// /// See also: /// /// * [LinearProgressIndicator], which displays progress along a line. /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] /// when the underlying vertical scrollable is overscrolled. /// * <https://material.io/design/components/progress-indicators.html#circular-progress-indicators> class CircularProgressIndicator extends ProgressIndicator { /// Creates a circular progress indicator. /// /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} const CircularProgressIndicator({ super.key, super.value, super.backgroundColor, super.color, super.valueColor, this.strokeWidth = 4.0, super.semanticsLabel, super.semanticsValue, }) : _indicatorType = _ActivityIndicatorType.material; /// Creates an adaptive progress indicator that is a /// [CupertinoActivityIndicator] in iOS and [CircularProgressIndicator] in /// material theme/non-iOS. /// /// The [value], [backgroundColor], [valueColor], [strokeWidth], /// [semanticsLabel], and [semanticsValue] will be ignored in iOS. /// /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} const CircularProgressIndicator.adaptive({ super.key, super.value, super.backgroundColor, super.valueColor, this.strokeWidth = 4.0, super.semanticsLabel, super.semanticsValue, }) : _indicatorType = _ActivityIndicatorType.adaptive; final _ActivityIndicatorType _indicatorType; /// {@template flutter.material.CircularProgressIndicator.trackColor} /// Color of the circular track being filled by the circular indicator. /// /// If [CircularProgressIndicator.backgroundColor] is null then the /// ambient [ProgressIndicatorThemeData.circularTrackColor] will be used. /// If that is null, then the track will not be painted. /// {@endtemplate} @override Color? get backgroundColor => super.backgroundColor; /// The width of the line used to draw the circle. final double strokeWidth; @override State<CircularProgressIndicator> createState() => _CircularProgressIndicatorState(); } class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin { static const int _pathCount = _kIndeterminateCircularDuration ~/ 1333; static const int _rotationCount = _kIndeterminateCircularDuration ~/ 2222; static final Animatable<double> _strokeHeadTween = CurveTween( curve: const Interval(0.0, 0.5, curve: Curves.fastOutSlowIn), ).chain(CurveTween( curve: const SawTooth(_pathCount), )); static final Animatable<double> _strokeTailTween = CurveTween( curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), ).chain(CurveTween( curve: const SawTooth(_pathCount), )); static final Animatable<double> _offsetTween = CurveTween(curve: const SawTooth(_pathCount)); static final Animatable<double> _rotationTween = CurveTween(curve: const SawTooth(_rotationCount)); late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: _kIndeterminateCircularDuration), vsync: this, ); if (widget.value == null) { _controller.repeat(); } } @override void didUpdateWidget(CircularProgressIndicator oldWidget) { super.didUpdateWidget(oldWidget); if (widget.value == null && !_controller.isAnimating) { _controller.repeat(); } else if (widget.value != null && _controller.isAnimating) { _controller.stop(); } } @override void dispose() { _controller.dispose(); super.dispose(); } Widget _buildCupertinoIndicator(BuildContext context) { final Color? tickColor = widget.backgroundColor; return CupertinoActivityIndicator(key: widget.key, color: tickColor); } Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) { final ProgressIndicatorThemeData defaults = Theme.of(context).useMaterial3 ? _CircularProgressIndicatorDefaultsM3(context) : _CircularProgressIndicatorDefaultsM2(context); final Color? trackColor = widget.backgroundColor ?? ProgressIndicatorTheme.of(context).circularTrackColor; return widget._buildSemanticsWrapper( context: context, child: Container( constraints: const BoxConstraints( minWidth: _kMinCircularProgressIndicatorSize, minHeight: _kMinCircularProgressIndicatorSize, ), child: CustomPaint( painter: _CircularProgressIndicatorPainter( backgroundColor: trackColor, valueColor: widget._getValueColor(context, defaultColor: defaults.color), value: widget.value, // may be null headValue: headValue, // remaining arguments are ignored if widget.value is not null tailValue: tailValue, offsetValue: offsetValue, rotationValue: rotationValue, strokeWidth: widget.strokeWidth, ), ), ), ); } Widget _buildAnimation() { return AnimatedBuilder( animation: _controller, builder: (BuildContext context, Widget? child) { return _buildMaterialIndicator( context, _strokeHeadTween.evaluate(_controller), _strokeTailTween.evaluate(_controller), _offsetTween.evaluate(_controller), _rotationTween.evaluate(_controller), ); }, ); } @override Widget build(BuildContext context) { switch (widget._indicatorType) { case _ActivityIndicatorType.material: if (widget.value != null) { return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0); } return _buildAnimation(); case _ActivityIndicatorType.adaptive: final ThemeData theme = Theme.of(context); assert(theme.platform != null); switch (theme.platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: return _buildCupertinoIndicator(context); case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: if (widget.value != null) { return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0); } return _buildAnimation(); } } } } class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter { _RefreshProgressIndicatorPainter({ required super.valueColor, required super.value, required super.headValue, required super.tailValue, required super.offsetValue, required super.rotationValue, required super.strokeWidth, required this.arrowheadScale, }); final double arrowheadScale; void paintArrowhead(Canvas canvas, Size size) { // ux, uy: a unit vector whose direction parallels the base of the arrowhead. // (So ux, -uy points in the direction the arrowhead points.) final double arcEnd = arcStart + arcSweep; final double ux = math.cos(arcEnd); final double uy = math.sin(arcEnd); assert(size.width == size.height); final double radius = size.width / 2.0; final double arrowheadPointX = radius + ux * radius + -uy * strokeWidth * 2.0 * arrowheadScale; final double arrowheadPointY = radius + uy * radius + ux * strokeWidth * 2.0 * arrowheadScale; final double arrowheadRadius = strokeWidth * 2.0 * arrowheadScale; final double innerRadius = radius - arrowheadRadius; final double outerRadius = radius + arrowheadRadius; final Path path = Path() ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius) ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius) ..lineTo(arrowheadPointX, arrowheadPointY) ..close(); final Paint paint = Paint() ..color = valueColor ..strokeWidth = strokeWidth ..style = PaintingStyle.fill; canvas.drawPath(path, paint); } @override void paint(Canvas canvas, Size size) { super.paint(canvas, size); if (arrowheadScale > 0.0) { paintArrowhead(canvas, size); } } } /// An indicator for the progress of refreshing the contents of a widget. /// /// Typically used for swipe-to-refresh interactions. See [RefreshIndicator] for /// a complete implementation of swipe-to-refresh driven by a [Scrollable] /// widget. /// /// The indicator arc is displayed with [valueColor], an animated value. To /// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`. /// /// See also: /// /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] /// when the underlying vertical scrollable is overscrolled. class RefreshProgressIndicator extends CircularProgressIndicator { /// Creates a refresh progress indicator. /// /// Rather than creating a refresh progress indicator directly, consider using /// a [RefreshIndicator] together with a [Scrollable] widget. /// /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} const RefreshProgressIndicator({ super.key, super.value, super.backgroundColor, super.color, super.valueColor, super.strokeWidth = defaultStrokeWidth, // Different default than CircularProgressIndicator. super.semanticsLabel, super.semanticsValue, }); /// Default stroke width. static const double defaultStrokeWidth = 2.5; /// {@template flutter.material.RefreshProgressIndicator.backgroundColor} /// Background color of that fills the circle under the refresh indicator. /// /// If [RefreshIndicator.backgroundColor] is null then the /// ambient [ProgressIndicatorThemeData.refreshBackgroundColor] will be used. /// If that is null, then the ambient theme's [ThemeData.canvasColor] /// will be used. /// {@endtemplate} @override Color? get backgroundColor => super.backgroundColor; @override State<CircularProgressIndicator> createState() => _RefreshProgressIndicatorState(); } class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { static const double _indicatorSize = 41.0; /// Interval for arrow head to fully grow. static const double _strokeHeadInterval = 0.33; late final Animatable<double> _convertTween = CurveTween( curve: const Interval(0.1, _strokeHeadInterval), ); late final Animatable<double> _additionalRotationTween = TweenSequence<double>( <TweenSequenceItem<double>>[ // Makes arrow to expand a little bit earlier, to match the Android look. TweenSequenceItem<double>( tween: Tween<double>(begin: -0.1, end: -0.2), weight: _strokeHeadInterval, ), // Additional rotation after the arrow expanded TweenSequenceItem<double>( tween: Tween<double>(begin: -0.2, end: 1.35), weight: 1 - _strokeHeadInterval, ), ], ); // Last value received from the widget before null. double? _lastValue; // Always show the indeterminate version of the circular progress indicator. // // When value is non-null the sweep of the progress indicator arrow's arc // varies from 0 to about 300 degrees. // // When value is null the arrow animation starting from wherever we left it. @override Widget build(BuildContext context) { final double? value = widget.value; if (value != null) { _lastValue = value; _controller.value = _convertTween.transform(value) * (1333 / 2 / _kIndeterminateCircularDuration); } return _buildAnimation(); } @override Widget _buildAnimation() { return AnimatedBuilder( animation: _controller, builder: (BuildContext context, Widget? child) { return _buildMaterialIndicator( context, // Lengthen the arc a little 1.05 * _CircularProgressIndicatorState._strokeHeadTween.evaluate(_controller), _CircularProgressIndicatorState._strokeTailTween.evaluate(_controller), _CircularProgressIndicatorState._offsetTween.evaluate(_controller), _CircularProgressIndicatorState._rotationTween.evaluate(_controller), ); }, ); } @override Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) { final double? value = widget.value; final double arrowheadScale = value == null ? 0.0 : const Interval(0.1, _strokeHeadInterval).transform(value); final double rotation; if (value == null && _lastValue == null) { rotation = 0.0; } else { rotation = math.pi * _additionalRotationTween.transform(value ?? _lastValue!); } Color valueColor = widget._getValueColor(context); final double opacity = valueColor.opacity; valueColor = valueColor.withOpacity(1.0); final Color backgroundColor = widget.backgroundColor ?? ProgressIndicatorTheme.of(context).refreshBackgroundColor ?? Theme.of(context).canvasColor; return widget._buildSemanticsWrapper( context: context, child: Container( width: _indicatorSize, height: _indicatorSize, margin: const EdgeInsets.all(4.0), // accommodate the shadow child: Material( type: MaterialType.circle, color: backgroundColor, elevation: 2.0, child: Padding( padding: const EdgeInsets.all(12.0), child: Opacity( opacity: opacity, child: Transform.rotate( angle: rotation, child: CustomPaint( painter: _RefreshProgressIndicatorPainter( valueColor: valueColor, value: null, // Draw the indeterminate progress indicator. headValue: headValue, tailValue: tailValue, offsetValue: offsetValue, rotationValue: rotationValue, strokeWidth: widget.strokeWidth, arrowheadScale: arrowheadScale, ), ), ), ), ), ), ), ); } } // Hand coded defaults based on Material Design 2. class _CircularProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData { _CircularProgressIndicatorDefaultsM2(this.context); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override Color get color => _colors.primary; } class _LinearProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData { _LinearProgressIndicatorDefaultsM2(this.context); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override Color get color => _colors.primary; @override Color get linearTrackColor => _colors.background; @override double get linearMinHeight => 4.0; } // BEGIN GENERATED TOKEN PROPERTIES - ProgressIndicator // Do not edit by hand. The code between the "BEGIN GENERATED" and // "END GENERATED" comments are generated from data in the Material // Design token database by the script: // dev/tools/gen_defaults/bin/gen_defaults.dart. // Token database version: v0_137 class _CircularProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData { _CircularProgressIndicatorDefaultsM3(this.context); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override Color get color => _colors.primary; } class _LinearProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData { _LinearProgressIndicatorDefaultsM3(this.context); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override Color get color => _colors.primary; @override Color get linearTrackColor => _colors.surfaceVariant; @override double get linearMinHeight => 4.0; } // END GENERATED TOKEN PROPERTIES - ProgressIndicator