progress_indicator.dart 30.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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;

7
import 'package:flutter/cupertino.dart';
8
import 'package:flutter/foundation.dart';
9

10
import 'color_scheme.dart';
11
import 'material.dart';
12
import 'progress_indicator_theme.dart';
13
import 'theme.dart';
14

15
const double _kMinCircularProgressIndicatorSize = 36.0;
16
const int _kIndeterminateLinearDuration = 1800;
17
const int _kIndeterminateCircularDuration = 1333 * 2222;
18

19 20
enum _ActivityIndicatorType { material, adaptive }

21
/// A base class for material design progress indicators.
22 23 24 25 26 27 28
///
/// This widget cannot be instantiated directly. For a linear progress
/// indicator, see [LinearProgressIndicator]. For a circular progress indicator,
/// see [CircularProgressIndicator].
///
/// See also:
///
29
///  * <https://material.io/components/progress-indicators>
30
abstract class ProgressIndicator extends StatefulWidget {
31 32
  /// Creates a progress indicator.
  ///
33
  /// {@template flutter.material.ProgressIndicator.ProgressIndicator}
34 35 36
  /// The [value] argument can either be null for an indeterminate
  /// progress indicator, or non-null for a determinate progress
  /// indicator.
37 38 39 40 41 42 43
  ///
  /// ## 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}
44
  const ProgressIndicator({
45
    Key? key,
46 47
    this.value,
    this.backgroundColor,
48
    this.color,
49
    this.valueColor,
50 51
    this.semanticsLabel,
    this.semanticsValue,
52 53
  }) : super(key: key);

54 55 56
  /// 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.
57 58
  ///
  /// If null, this progress indicator is indeterminate, which means the
59
  /// indicator displays a predetermined animation that does not indicate how
60
  /// much actual progress is being made.
61
  final double? value;
62

63 64
  /// The progress indicator's background color.
  ///
65 66
  /// 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.
67
  final Color? backgroundColor;
68

69
  /// {@template flutter.progress_indicator.ProgressIndicator.color}
70 71
  /// The progress indicator's color.
  ///
72 73 74 75 76 77
  /// 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}
78 79 80
  final Color? color;

  /// The progress indicator's color as an animated value.
81
  ///
82 83 84
  /// 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].
85
  final Animation<Color?>? valueColor;
86

87
  /// {@template flutter.progress_indicator.ProgressIndicator.semanticsLabel}
88
  /// The [SemanticsProperties.label] for this progress indicator.
89 90 91 92 93
  ///
  /// 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}
94
  final String? semanticsLabel;
95

96
  /// {@template flutter.progress_indicator.ProgressIndicator.semanticsValue}
97
  /// The [SemanticsProperties.value] for this progress indicator.
98 99 100 101 102 103
  ///
  /// 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.
  ///
104 105 106
  /// For determinate progress indicators, this will be defaulted to
  /// [ProgressIndicator.value] expressed as a percentage, i.e. `0.1` will
  /// become '10%'.
107
  /// {@endtemplate}
108
  final String? semanticsValue;
109

110 111 112 113 114 115 116
  Color _getValueColor(BuildContext context) {
    return
      valueColor?.value ??
      color ??
      ProgressIndicatorTheme.of(context).color ??
      Theme.of(context).colorScheme.primary;
  }
117

118
  @override
119 120
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
121
    properties.add(PercentProperty('value', value, showName: false, ifNull: '<indeterminate>'));
Hixie's avatar
Hixie committed
122
  }
123 124

  Widget _buildSemanticsWrapper({
125 126
    required BuildContext context,
    required Widget child,
127
  }) {
128
    String? expandedSemanticsValue = semanticsValue;
129
    if (value != null) {
130
      expandedSemanticsValue ??= '${(value! * 100).round()}%';
131 132 133 134 135 136 137
    }
    return Semantics(
      label: semanticsLabel,
      value: expandedSemanticsValue,
      child: child,
    );
  }
138 139
}

140
class _LinearProgressIndicatorPainter extends CustomPainter {
141
  const _LinearProgressIndicatorPainter({
142 143
    required this.backgroundColor,
    required this.valueColor,
144
    this.value,
145 146
    required this.animationValue,
    required this.textDirection,
147 148 149 150
  }) : assert(textDirection != null);

  final Color backgroundColor;
  final Color valueColor;
151
  final double? value;
152 153 154
  final double animationValue;
  final TextDirection textDirection;

155 156
  // The indeterminate progress animation displays two lines whose leading (head)
  // and trailing (tail) endpoints are defined by the following four curves.
157
  static const Curve line1Head = Interval(
158 159
    0.0,
    750.0 / _kIndeterminateLinearDuration,
160
    curve: Cubic(0.2, 0.0, 0.8, 1.0),
161
  );
162
  static const Curve line1Tail = Interval(
163 164
    333.0 / _kIndeterminateLinearDuration,
    (333.0 + 750.0) / _kIndeterminateLinearDuration,
165
    curve: Cubic(0.4, 0.0, 1.0, 1.0),
166
  );
167
  static const Curve line2Head = Interval(
168 169
    1000.0 / _kIndeterminateLinearDuration,
    (1000.0 + 567.0) / _kIndeterminateLinearDuration,
170
    curve: Cubic(0.0, 0.0, 0.65, 1.0),
171
  );
172
  static const Curve line2Tail = Interval(
173 174
    1267.0 / _kIndeterminateLinearDuration,
    (1267.0 + 533.0) / _kIndeterminateLinearDuration,
175
    curve: Cubic(0.10, 0.0, 0.45, 1.0),
176 177
  );

178
  @override
179
  void paint(Canvas canvas, Size size) {
180
    final Paint paint = Paint()
181
      ..color = backgroundColor
182
      ..style = PaintingStyle.fill;
183
    canvas.drawRect(Offset.zero & size, paint);
184

185
    paint.color = valueColor;
186

187 188 189
    void drawBar(double x, double width) {
      if (width <= 0.0)
        return;
190

191
      final double left;
192 193 194 195 196 197 198 199
      switch (textDirection) {
        case TextDirection.rtl:
          left = size.width - width - x;
          break;
        case TextDirection.ltr:
          left = x;
          break;
      }
200
      canvas.drawRect(Offset(left, 0.0) & Size(width, size.height), paint);
201
    }
202 203

    if (value != null) {
204
      drawBar(0.0, value!.clamp(0.0, 1.0) * size.width);
205 206 207 208 209 210 211 212 213 214
    } 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);
    }
215 216
  }

217
  @override
218 219 220 221
  bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) {
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
        || oldPainter.value != value
222 223
        || oldPainter.animationValue != animationValue
        || oldPainter.textDirection != textDirection;
224 225 226
  }
}

227
/// A material design linear progress indicator, also known as a progress bar.
228
///
229 230
/// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0}
///
231 232 233 234 235 236 237 238 239 240 241 242
/// 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].
///
243 244 245
/// The indicator line is displayed with [valueColor], an animated value. To
/// specify a constant color value use: `AlwaysStoppedAnimation<Color>(color)`.
///
246 247 248
/// The minimum height of the indicator can be specified using [minHeight].
/// The indicator can be made taller by wrapping the widget with a [SizedBox].
///
249
/// {@tool dartpad --template=stateful_widget_material_ticker}
250 251 252 253
///
/// This example shows a [LinearProgressIndicator] with a changing value.
///
/// ```dart
254
///  late AnimationController controller;
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
///
///  @override
///  void initState() {
///    controller = AnimationController(
///      vsync: this,
///      duration: const Duration(seconds: 5),
///    )..addListener(() {
///        setState(() {});
///      });
///    controller.repeat(reverse: true);
///    super.initState();
///  }
///
/// @override
/// void dispose() {
///   controller.dispose();
///   super.dispose();
/// }
///
274
/// @override
275 276 277 278 279 280
/// Widget build(BuildContext context) {
///   return Scaffold(
///     body: Padding(
///       padding: const EdgeInsets.all(20.0),
///       child: Column(
///         mainAxisAlignment: MainAxisAlignment.spaceEvenly,
281 282
///         children: <Widget>[
///           const Text(
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
///             'Linear progress indicator with a fixed color',
///             style: const TextStyle(fontSize: 20),
///           ),
///           LinearProgressIndicator(
///             value: controller.value,
///             semanticsLabel: 'Linear progress indicator',
///           ),
///         ],
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
298 299
/// See also:
///
300 301 302
///  * [CircularProgressIndicator], which shows progress along a circular arc.
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
303
///  * <https://material.io/design/components/progress-indicators.html#linear-progress-indicators>
304
class LinearProgressIndicator extends ProgressIndicator {
305 306
  /// Creates a linear progress indicator.
  ///
307
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
308
  const LinearProgressIndicator({
309 310 311
    Key? key,
    double? value,
    Color? backgroundColor,
312
    Color? color,
313
    Animation<Color?>? valueColor,
314
    this.minHeight,
315 316
    String? semanticsLabel,
    String? semanticsValue,
317 318
  }) : assert(minHeight == null || minHeight > 0),
       super(
319 320 321 322 323 324 325 326 327 328 329
         key: key,
         value: value,
         backgroundColor: backgroundColor,
         color: color,
         valueColor: valueColor,
         semanticsLabel: semanticsLabel,
         semanticsValue: semanticsValue,
       );

  /// {@template flutter.material.LinearProgressIndicator.trackColor}
  /// Color of the track being filled by the linear indicator.
330
  ///
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
  /// 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}
346
  final double? minHeight;
347

348
  @override
349
  State<LinearProgressIndicator> createState() => _LinearProgressIndicatorState();
350 351
}

352
class _LinearProgressIndicatorState extends State<LinearProgressIndicator> with SingleTickerProviderStateMixin {
353
  late AnimationController _controller;
354

355
  @override
356 357
  void initState() {
    super.initState();
358
    _controller = AnimationController(
359
      duration: const Duration(milliseconds: _kIndeterminateLinearDuration),
360
      vsync: this,
361 362 363 364 365 366 367 368 369 370 371 372
    );
    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();
373 374
  }

375
  @override
376
  void dispose() {
377
    _controller.dispose();
378 379 380
    super.dispose();
  }

381
  Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) {
382 383 384 385 386 387 388
    final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context);
    final Color trackColor =
      widget.backgroundColor ??
      indicatorTheme.linearTrackColor ??
      Theme.of(context).colorScheme.background;
    final double minHeight = widget.minHeight ?? indicatorTheme.linearMinHeight ?? 4.0;

389 390 391
    return widget._buildSemanticsWrapper(
      context: context,
      child: Container(
392
        constraints: BoxConstraints(
393
          minWidth: double.infinity,
394
          minHeight: minHeight,
395 396 397
        ),
        child: CustomPaint(
          painter: _LinearProgressIndicatorPainter(
398
            backgroundColor: trackColor,
399 400 401 402 403
            valueColor: widget._getValueColor(context),
            value: widget.value, // may be null
            animationValue: animationValue, // ignored if widget.value is not null
            textDirection: textDirection,
          ),
404 405
        ),
      ),
406 407 408
    );
  }

409
  @override
410
  Widget build(BuildContext context) {
411
    final TextDirection textDirection = Directionality.of(context);
412

413
    if (widget.value != null)
414
      return _buildIndicator(context, _controller.value, textDirection);
415

416
    return AnimatedBuilder(
417
      animation: _controller.view,
418
      builder: (BuildContext context, Widget? child) {
419
        return _buildIndicator(context, _controller.value, textDirection);
420
      },
421 422 423 424
    );
  }
}

425
class _CircularProgressIndicatorPainter extends CustomPainter {
426
  _CircularProgressIndicatorPainter({
427
    this.backgroundColor,
428 429 430 431 432 433 434
    required this.valueColor,
    required this.value,
    required this.headValue,
    required this.tailValue,
    required this.offsetValue,
    required this.rotationValue,
    required this.strokeWidth,
435
  }) : arcStart = value != null
436
         ? _startAngle
437
         : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 2.0 + offsetValue * 0.5 * math.pi,
438
       arcSweep = value != null
439
         ? value.clamp(0.0, 1.0) * _sweep
440
         : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon);
441

442
  final Color? backgroundColor;
443
  final Color valueColor;
444
  final double? value;
445 446
  final double headValue;
  final double tailValue;
447
  final double offsetValue;
448
  final double rotationValue;
449 450 451
  final double strokeWidth;
  final double arcStart;
  final double arcSweep;
452

453 454 455 456 457 458
  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;

459
  @override
460
  void paint(Canvas canvas, Size size) {
461
    final Paint paint = Paint()
462
      ..color = valueColor
463
      ..strokeWidth = strokeWidth
464
      ..style = PaintingStyle.stroke;
465 466
    if (backgroundColor != null) {
      final Paint backgroundPaint = Paint()
467
        ..color = backgroundColor!
468 469 470 471
        ..strokeWidth = strokeWidth
        ..style = PaintingStyle.stroke;
      canvas.drawArc(Offset.zero & size, 0, _sweep, false, backgroundPaint);
    }
472

473
    if (value == null) // Indeterminate
474
      paint.strokeCap = StrokeCap.square;
475

476
    canvas.drawArc(Offset.zero & size, arcStart, arcSweep, false, paint);
477 478
  }

479
  @override
480
  bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) {
481 482
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
483
        || oldPainter.value != value
484 485
        || oldPainter.headValue != headValue
        || oldPainter.tailValue != tailValue
486
        || oldPainter.offsetValue != offsetValue
487 488
        || oldPainter.rotationValue != rotationValue
        || oldPainter.strokeWidth != strokeWidth;
489 490 491
  }
}

492 493
/// A material design circular progress indicator, which spins to indicate that
/// the application is busy.
494
///
495 496
/// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0}
///
497 498 499 500 501 502 503 504 505 506 507 508
/// 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].
///
509 510 511
/// The indicator arc is displayed with [valueColor], an animated value. To
/// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`.
///
512
/// {@tool dartpad --template=stateful_widget_material_ticker}
513 514 515 516
///
/// This example shows a [CircularProgressIndicator] with a changing value.
///
/// ```dart
517
///  late AnimationController controller;
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536
///
///  @override
///  void initState() {
///    controller = AnimationController(
///      vsync: this,
///      duration: const Duration(seconds: 5),
///    )..addListener(() {
///        setState(() {});
///      });
///    controller.repeat(reverse: true);
///    super.initState();
///  }
///
/// @override
/// void dispose() {
///   controller.dispose();
///   super.dispose();
/// }
///
537
/// @override
538 539 540 541 542 543
/// Widget build(BuildContext context) {
///   return Scaffold(
///     body: Padding(
///       padding: const EdgeInsets.all(20.0),
///       child: Column(
///         mainAxisAlignment: MainAxisAlignment.spaceEvenly,
544
///         children: <Widget>[
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
///           Text(
///             'Linear progress indicator with a fixed color',
///             style: Theme.of(context).textTheme.headline6,
///           ),
///           CircularProgressIndicator(
///             value: controller.value,
///             semanticsLabel: 'Linear progress indicator',
///           ),
///         ],
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
///
561 562
/// See also:
///
563 564 565
///  * [LinearProgressIndicator], which displays progress along a line.
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
566
///  * <https://material.io/design/components/progress-indicators.html#circular-progress-indicators>
567
class CircularProgressIndicator extends ProgressIndicator {
568 569
  /// Creates a circular progress indicator.
  ///
570
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
571
  const CircularProgressIndicator({
572 573 574
    Key? key,
    double? value,
    Color? backgroundColor,
575
    Color? color,
576
    Animation<Color?>? valueColor,
577
    this.strokeWidth = 4.0,
578 579
    String? semanticsLabel,
    String? semanticsValue,
580 581 582 583 584
  }) : _indicatorType = _ActivityIndicatorType.material,
       super(
         key: key,
         value: value,
         backgroundColor: backgroundColor,
585
         color: color,
586 587 588 589 590 591 592 593 594 595 596 597
         valueColor: valueColor,
         semanticsLabel: semanticsLabel,
         semanticsValue: semanticsValue,
       );

  /// 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.
  ///
598
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
599 600 601 602 603 604 605 606 607 608
  const CircularProgressIndicator.adaptive({
    Key? key,
    double? value,
    Color? backgroundColor,
    Animation<Color?>? valueColor,
    this.strokeWidth = 4.0,
    String? semanticsLabel,
    String? semanticsValue,
  }) : _indicatorType = _ActivityIndicatorType.adaptive,
       super(
609 610 611 612 613 614 615
         key: key,
         value: value,
         backgroundColor: backgroundColor,
         valueColor: valueColor,
         semanticsLabel: semanticsLabel,
         semanticsValue: semanticsValue,
       );
616

617 618
  final _ActivityIndicatorType _indicatorType;

619 620 621 622 623 624 625 626 627 628
  /// {@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;

629 630 631
  /// The width of the line used to draw the circle.
  final double strokeWidth;

632
  @override
633
  State<CircularProgressIndicator> createState() => _CircularProgressIndicatorState();
634
}
635

636
class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin {
637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
  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));

653
  late AnimationController _controller;
654

655
  @override
656 657
  void initState() {
    super.initState();
658
    _controller = AnimationController(
659
      duration: const Duration(milliseconds: _kIndeterminateCircularDuration),
660
      vsync: this,
661 662 663 664 665 666 667 668 669 670 671 672
    );
    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();
673 674
  }

675
  @override
676
  void dispose() {
677
    _controller.dispose();
678 679 680
    super.dispose();
  }

681 682 683 684 685
  Widget _buildCupertinoIndicator(BuildContext context) {
    return CupertinoActivityIndicator(key: widget.key);
  }

  Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
686 687
    final Color? trackColor = widget.backgroundColor ?? ProgressIndicatorTheme.of(context).circularTrackColor;

688 689 690 691 692 693 694 695 696
    return widget._buildSemanticsWrapper(
      context: context,
      child: Container(
        constraints: const BoxConstraints(
          minWidth: _kMinCircularProgressIndicatorSize,
          minHeight: _kMinCircularProgressIndicatorSize,
        ),
        child: CustomPaint(
          painter: _CircularProgressIndicatorPainter(
697
            backgroundColor: trackColor,
698 699 700 701
            valueColor: widget._getValueColor(context),
            value: widget.value, // may be null
            headValue: headValue, // remaining arguments are ignored if widget.value is not null
            tailValue: tailValue,
702
            offsetValue: offsetValue,
703 704 705
            rotationValue: rotationValue,
            strokeWidth: widget.strokeWidth,
          ),
706 707
        ),
      ),
708 709 710
    );
  }

711
  Widget _buildAnimation() {
712
    return AnimatedBuilder(
713
      animation: _controller,
714
      builder: (BuildContext context, Widget? child) {
715
        return _buildMaterialIndicator(
716
          context,
717 718 719 720
          _strokeHeadTween.evaluate(_controller),
          _strokeTailTween.evaluate(_controller),
          _offsetTween.evaluate(_controller),
          _rotationTween.evaluate(_controller),
721
        );
722
      },
723 724
    );
  }
725 726 727

  @override
  Widget build(BuildContext context) {
728 729 730 731 732 733
    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:
734
        final ThemeData theme = Theme.of(context);
735 736 737 738 739 740 741 742 743 744 745 746 747 748
        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();
        }
    }
749
  }
750
}
751 752 753

class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
  _RefreshProgressIndicatorPainter({
754 755 756 757 758 759 760 761
    required Color valueColor,
    required double? value,
    required double headValue,
    required double tailValue,
    required double offsetValue,
    required double rotationValue,
    required double strokeWidth,
    required this.arrowheadScale,
762 763 764 765 766
  }) : super(
    valueColor: valueColor,
    value: value,
    headValue: headValue,
    tailValue: tailValue,
767
    offsetValue: offsetValue,
768
    rotationValue: rotationValue,
769
    strokeWidth: strokeWidth,
770 771
  );

772 773
  final double arrowheadScale;

774 775
  void paintArrowhead(Canvas canvas, Size size) {
    // ux, uy: a unit vector whose direction parallels the base of the arrowhead.
Ian Hickson's avatar
Ian Hickson committed
776
    // (So ux, -uy points in the direction the arrowhead points.)
777 778 779 780 781 782
    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;
783 784 785 786 787
    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 * 1.5 * arrowheadScale;
    final double innerRadius = radius - arrowheadRadius;
    final double outerRadius = radius + arrowheadRadius;
788

789
    final Path path = Path()
790 791
      ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
      ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
792
      ..lineTo(arrowheadPointX, arrowheadPointY)
793
      ..close();
794
    final Paint paint = Paint()
795 796 797 798 799 800 801 802 803
      ..color = valueColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.fill;
    canvas.drawPath(path, paint);
  }

  @override
  void paint(Canvas canvas, Size size) {
    super.paint(canvas, size);
804 805
    if (arrowheadScale > 0.0)
      paintArrowhead(canvas, size);
806 807 808
  }
}

809 810 811
/// An indicator for the progress of refreshing the contents of a widget.
///
/// Typically used for swipe-to-refresh interactions. See [RefreshIndicator] for
Adam Barth's avatar
Adam Barth committed
812
/// a complete implementation of swipe-to-refresh driven by a [Scrollable]
813 814
/// widget.
///
815 816 817
/// The indicator arc is displayed with [valueColor], an animated value. To
/// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`.
///
818 819
/// See also:
///
820 821
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
822
class RefreshProgressIndicator extends CircularProgressIndicator {
823 824 825
  /// Creates a refresh progress indicator.
  ///
  /// Rather than creating a refresh progress indicator directly, consider using
Adam Barth's avatar
Adam Barth committed
826
  /// a [RefreshIndicator] together with a [Scrollable] widget.
827
  ///
828
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
829
  const RefreshProgressIndicator({
830 831 832
    Key? key,
    double? value,
    Color? backgroundColor,
833
    Color? color,
834
    Animation<Color?>? valueColor,
835
    double strokeWidth = 2.0, // Different default than CircularProgressIndicator.
836 837
    String? semanticsLabel,
    String? semanticsValue,
838 839 840 841
  }) : super(
    key: key,
    value: value,
    backgroundColor: backgroundColor,
842
    color: color,
843 844
    valueColor: valueColor,
    strokeWidth: strokeWidth,
845 846
    semanticsLabel: semanticsLabel,
    semanticsValue: semanticsValue,
847
  );
848

849 850 851 852 853 854 855 856 857 858
  /// {@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;
859
  @override
860
  State<CircularProgressIndicator> createState() => _RefreshProgressIndicatorState();
861 862 863
}

class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
864
  static const double _indicatorSize = 40.0;
865

866 867 868 869 870 871
  // 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 270 degrees. When value is null the arrow animates
  // starting from wherever we left it.
  @override
  Widget build(BuildContext context) {
872
    if (widget.value != null)
873
      _controller.value = widget.value! * (1333 / 2 / _kIndeterminateCircularDuration);
874 875
    else if (!_controller.isAnimating)
      _controller.repeat();
876 877 878
    return _buildAnimation();
  }

879
  @override
880
  Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
881
    final double arrowheadScale = widget.value == null ? 0.0 : (widget.value! * 2.0).clamp(0.0, 1.0);
882 883 884 885
    final Color backgroundColor =
      widget.backgroundColor ??
      ProgressIndicatorTheme.of(context).refreshBackgroundColor ??
      Theme.of(context).canvasColor;
886 887 888 889 890 891 892 893
    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,
894
          color: backgroundColor,
895 896 897 898 899 900 901 902 903
          elevation: 2.0,
          child: Padding(
            padding: const EdgeInsets.all(12.0),
            child: CustomPaint(
              painter: _RefreshProgressIndicatorPainter(
                valueColor: widget._getValueColor(context),
                value: null, // Draw the indeterminate progress indicator.
                headValue: headValue,
                tailValue: tailValue,
904
                offsetValue: offsetValue,
905 906 907 908
                rotationValue: rotationValue,
                strokeWidth: widget.strokeWidth,
                arrowheadScale: arrowheadScale,
              ),
909 910 911 912
            ),
          ),
        ),
      ),
913 914 915
    );
  }
}