progress_indicator.dart 23.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8
import 'dart:math' as math;

9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/widgets.dart';
11

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

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

60 61 62
  /// The progress indicator's background color.
  ///
  /// The current theme's [ThemeData.backgroundColor] by default.
63 64
  final Color backgroundColor;

65 66 67
  /// The progress indicator's color as an animated value.
  ///
  /// To specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`.
68
  ///
69
  /// If null, the progress indicator is rendered with the current theme's
70
  /// [ThemeData.accentColor].
71 72
  final Animation<Color> valueColor;

73
  /// {@template flutter.material.progressIndicator.semanticsLabel}
74
  /// The [SemanticsProperties.label] for this progress indicator.
75 76 77 78 79 80 81 82
  ///
  /// 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.material.progressIndicator.semanticsValue}
83
  /// The [SemanticsProperties.value] for this progress indicator.
84 85 86 87 88 89
  ///
  /// 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.
  ///
90 91 92
  /// For determinate progress indicators, this will be defaulted to
  /// [ProgressIndicator.value] expressed as a percentage, i.e. `0.1` will
  /// become '10%'.
93 94 95
  /// {@endtemplate}
  final String semanticsValue;

96
  Color _getBackgroundColor(BuildContext context) => backgroundColor ?? Theme.of(context).backgroundColor;
97
  Color _getValueColor(BuildContext context) => valueColor?.value ?? Theme.of(context).accentColor;
98

99
  @override
100 101
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
102
    properties.add(PercentProperty('value', value, showName: false, ifNull: '<indeterminate>'));
Hixie's avatar
Hixie committed
103
  }
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

  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,
    );
  }
119 120
}

121
class _LinearProgressIndicatorPainter extends CustomPainter {
122 123 124 125 126 127 128 129 130 131 132 133 134 135
  const _LinearProgressIndicatorPainter({
    this.backgroundColor,
    this.valueColor,
    this.value,
    this.animationValue,
    @required this.textDirection,
  }) : assert(textDirection != null);

  final Color backgroundColor;
  final Color valueColor;
  final double value;
  final double animationValue;
  final TextDirection textDirection;

136 137
  // The indeterminate progress animation displays two lines whose leading (head)
  // and trailing (tail) endpoints are defined by the following four curves.
138
  static const Curve line1Head = Interval(
139 140
    0.0,
    750.0 / _kIndeterminateLinearDuration,
141
    curve: Cubic(0.2, 0.0, 0.8, 1.0),
142
  );
143
  static const Curve line1Tail = Interval(
144 145
    333.0 / _kIndeterminateLinearDuration,
    (333.0 + 750.0) / _kIndeterminateLinearDuration,
146
    curve: Cubic(0.4, 0.0, 1.0, 1.0),
147
  );
148
  static const Curve line2Head = Interval(
149 150
    1000.0 / _kIndeterminateLinearDuration,
    (1000.0 + 567.0) / _kIndeterminateLinearDuration,
151
    curve: Cubic(0.0, 0.0, 0.65, 1.0),
152
  );
153
  static const Curve line2Tail = Interval(
154 155
    1267.0 / _kIndeterminateLinearDuration,
    (1267.0 + 533.0) / _kIndeterminateLinearDuration,
156
    curve: Cubic(0.10, 0.0, 0.45, 1.0),
157 158
  );

159
  @override
160
  void paint(Canvas canvas, Size size) {
161
    final Paint paint = Paint()
162
      ..color = backgroundColor
163
      ..style = PaintingStyle.fill;
164
    canvas.drawRect(Offset.zero & size, paint);
165

166
    paint.color = valueColor;
167

168 169 170
    void drawBar(double x, double width) {
      if (width <= 0.0)
        return;
171 172 173 174 175 176 177 178 179 180

      double left;
      switch (textDirection) {
        case TextDirection.rtl:
          left = size.width - width - x;
          break;
        case TextDirection.ltr:
          left = x;
          break;
      }
181
      canvas.drawRect(Offset(left, 0.0) & Size(width, size.height), paint);
182
    }
183 184

    if (value != null) {
185
      drawBar(0.0, value.clamp(0.0, 1.0) * size.width as double);
186 187 188 189 190 191 192 193 194 195
    } 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);
    }
196 197
  }

198
  @override
199 200 201 202
  bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) {
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
        || oldPainter.value != value
203 204
        || oldPainter.animationValue != animationValue
        || oldPainter.textDirection != textDirection;
205 206 207
  }
}

208
/// A material design linear progress indicator, also known as a progress bar.
209
///
210 211
/// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0}
///
212 213 214 215 216 217 218 219 220 221 222 223
/// 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].
///
224 225 226
/// The indicator line is displayed with [valueColor], an animated value. To
/// specify a constant color value use: `AlwaysStoppedAnimation<Color>(color)`.
///
227 228 229
/// The minimum height of the indicator can be specified using [minHeight].
/// The indicator can be made taller by wrapping the widget with a [SizedBox].
///
230 231
/// See also:
///
232 233 234
///  * [CircularProgressIndicator], which shows progress along a circular arc.
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
235
///  * <https://material.io/design/components/progress-indicators.html#linear-progress-indicators>
236
class LinearProgressIndicator extends ProgressIndicator {
237 238
  /// Creates a linear progress indicator.
  ///
239
  /// {@macro flutter.material.progressIndicator.parameters}
240
  const LinearProgressIndicator({
241
    Key key,
242
    double value,
243 244
    Color backgroundColor,
    Animation<Color> valueColor,
245
    this.minHeight,
246 247
    String semanticsLabel,
    String semanticsValue,
248 249 250 251 252 253 254 255 256 257 258 259 260 261
  }) : assert(minHeight == null || minHeight > 0),
       super(
        key: key,
        value: value,
        backgroundColor: backgroundColor,
        valueColor: valueColor,
        semanticsLabel: semanticsLabel,
        semanticsValue: semanticsValue,
      );

  /// The minimum height of the line used to draw the indicator.
  ///
  /// This defaults to 4dp.
  final double minHeight;
262

263
  @override
264
  _LinearProgressIndicatorState createState() => _LinearProgressIndicatorState();
265 266
}

267
class _LinearProgressIndicatorState extends State<LinearProgressIndicator> with SingleTickerProviderStateMixin {
268 269
  AnimationController _controller;

270
  @override
271 272
  void initState() {
    super.initState();
273
    _controller = AnimationController(
274
      duration: const Duration(milliseconds: _kIndeterminateLinearDuration),
275
      vsync: this,
276 277 278 279 280 281 282 283 284 285 286 287
    );
    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();
288 289
  }

290
  @override
291
  void dispose() {
292
    _controller.dispose();
293 294 295
    super.dispose();
  }

296
  Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) {
297 298 299
    return widget._buildSemanticsWrapper(
      context: context,
      child: Container(
300
        constraints: BoxConstraints(
301
          minWidth: double.infinity,
302
          minHeight: widget.minHeight ?? 4.0,
303 304 305 306 307 308 309 310 311
        ),
        child: CustomPaint(
          painter: _LinearProgressIndicatorPainter(
            backgroundColor: widget._getBackgroundColor(context),
            valueColor: widget._getValueColor(context),
            value: widget.value, // may be null
            animationValue: animationValue, // ignored if widget.value is not null
            textDirection: textDirection,
          ),
312 313
        ),
      ),
314 315 316
    );
  }

317
  @override
318
  Widget build(BuildContext context) {
319 320
    final TextDirection textDirection = Directionality.of(context);

321
    if (widget.value != null)
322
      return _buildIndicator(context, _controller.value, textDirection);
323

324
    return AnimatedBuilder(
325
      animation: _controller.view,
326
      builder: (BuildContext context, Widget child) {
327
        return _buildIndicator(context, _controller.value, textDirection);
328
      },
329 330 331 332
    );
  }
}

333
class _CircularProgressIndicatorPainter extends CustomPainter {
334
  _CircularProgressIndicatorPainter({
335
    this.backgroundColor,
336
    this.valueColor,
337 338 339
    this.value,
    this.headValue,
    this.tailValue,
340
    this.offsetValue,
341
    this.rotationValue,
342
    this.strokeWidth,
343
  }) : arcStart = value != null
344
         ? _startAngle
345
         : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 2.0 + offsetValue * 0.5 * math.pi,
346
       arcSweep = value != null
347
         ? (value.clamp(0.0, 1.0) as double) * _sweep
348
         : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon);
349

350
  final Color backgroundColor;
351 352
  final Color valueColor;
  final double value;
353 354
  final double headValue;
  final double tailValue;
355
  final double offsetValue;
356
  final double rotationValue;
357 358 359
  final double strokeWidth;
  final double arcStart;
  final double arcSweep;
360

361 362 363 364 365 366
  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;

367
  @override
368
  void paint(Canvas canvas, Size size) {
369
    final Paint paint = Paint()
370
      ..color = valueColor
371
      ..strokeWidth = strokeWidth
372
      ..style = PaintingStyle.stroke;
373 374 375 376 377 378 379
    if (backgroundColor != null) {
      final Paint backgroundPaint = Paint()
        ..color = backgroundColor
        ..strokeWidth = strokeWidth
        ..style = PaintingStyle.stroke;
      canvas.drawArc(Offset.zero & size, 0, _sweep, false, backgroundPaint);
    }
380

381
    if (value == null) // Indeterminate
382
      paint.strokeCap = StrokeCap.square;
383

384
    canvas.drawArc(Offset.zero & size, arcStart, arcSweep, false, paint);
385 386
  }

387
  @override
388
  bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) {
389 390
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
391
        || oldPainter.value != value
392 393
        || oldPainter.headValue != headValue
        || oldPainter.tailValue != tailValue
394
        || oldPainter.offsetValue != offsetValue
395 396
        || oldPainter.rotationValue != rotationValue
        || oldPainter.strokeWidth != strokeWidth;
397 398 399
  }
}

400 401
/// A material design circular progress indicator, which spins to indicate that
/// the application is busy.
402
///
403 404
/// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0}
///
405 406 407 408 409 410 411 412 413 414 415 416
/// 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].
///
417 418 419
/// The indicator arc is displayed with [valueColor], an animated value. To
/// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`.
///
420 421
/// See also:
///
422 423 424
///  * [LinearProgressIndicator], which displays progress along a line.
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
425
///  * <https://material.io/design/components/progress-indicators.html#circular-progress-indicators>
426
class CircularProgressIndicator extends ProgressIndicator {
427 428
  /// Creates a circular progress indicator.
  ///
429
  /// {@macro flutter.material.progressIndicator.parameters}
430
  const CircularProgressIndicator({
431
    Key key,
432 433
    double value,
    Color backgroundColor,
434
    Animation<Color> valueColor,
435
    this.strokeWidth = 4.0,
436 437 438 439 440 441 442 443 444 445
    String semanticsLabel,
    String semanticsValue,
  }) : super(
         key: key,
         value: value,
         backgroundColor: backgroundColor,
         valueColor: valueColor,
         semanticsLabel: semanticsLabel,
         semanticsValue: semanticsValue,
       );
446

447 448 449
  /// The width of the line used to draw the circle.
  final double strokeWidth;

450
  @override
451
  _CircularProgressIndicatorState createState() => _CircularProgressIndicatorState();
452
}
453

454
class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin {
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
  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));

471
  AnimationController _controller;
472

473
  @override
474 475
  void initState() {
    super.initState();
476
    _controller = AnimationController(
477
      duration: const Duration(milliseconds: _kIndeterminateCircularDuration),
478
      vsync: this,
479 480 481 482 483 484 485 486 487 488 489 490
    );
    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();
491 492
  }

493
  @override
494
  void dispose() {
495
    _controller.dispose();
496 497 498
    super.dispose();
  }

499
  Widget _buildIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
500 501 502 503 504 505 506 507 508
    return widget._buildSemanticsWrapper(
      context: context,
      child: Container(
        constraints: const BoxConstraints(
          minWidth: _kMinCircularProgressIndicatorSize,
          minHeight: _kMinCircularProgressIndicatorSize,
        ),
        child: CustomPaint(
          painter: _CircularProgressIndicatorPainter(
509
            backgroundColor: widget.backgroundColor,
510 511 512 513
            valueColor: widget._getValueColor(context),
            value: widget.value, // may be null
            headValue: headValue, // remaining arguments are ignored if widget.value is not null
            tailValue: tailValue,
514
            offsetValue: offsetValue,
515 516 517
            rotationValue: rotationValue,
            strokeWidth: widget.strokeWidth,
          ),
518 519
        ),
      ),
520 521 522
    );
  }

523
  Widget _buildAnimation() {
524
    return AnimatedBuilder(
525
      animation: _controller,
526
      builder: (BuildContext context, Widget child) {
527 528
        return _buildIndicator(
          context,
529 530 531 532
          _strokeHeadTween.evaluate(_controller),
          _strokeTailTween.evaluate(_controller),
          _offsetTween.evaluate(_controller),
          _rotationTween.evaluate(_controller),
533
        );
534
      },
535 536
    );
  }
537 538 539

  @override
  Widget build(BuildContext context) {
540
    if (widget.value != null)
541 542 543
      return _buildIndicator(context, 0.0, 0.0, 0, 0.0);
    return _buildAnimation();
  }
544
}
545 546 547 548 549 550 551

class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
  _RefreshProgressIndicatorPainter({
    Color valueColor,
    double value,
    double headValue,
    double tailValue,
552
    double offsetValue,
553
    double rotationValue,
554
    double strokeWidth,
555
    this.arrowheadScale,
556 557 558 559 560
  }) : super(
    valueColor: valueColor,
    value: value,
    headValue: headValue,
    tailValue: tailValue,
561
    offsetValue: offsetValue,
562
    rotationValue: rotationValue,
563
    strokeWidth: strokeWidth,
564 565
  );

566 567
  final double arrowheadScale;

568 569
  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
570
    // (So ux, -uy points in the direction the arrowhead points.)
571 572 573 574 575 576
    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;
577 578 579 580 581
    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;
582

583
    final Path path = Path()
584 585
      ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
      ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
586
      ..lineTo(arrowheadPointX, arrowheadPointY)
587
      ..close();
588
    final Paint paint = Paint()
589 590 591 592 593 594 595 596 597
      ..color = valueColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.fill;
    canvas.drawPath(path, paint);
  }

  @override
  void paint(Canvas canvas, Size size) {
    super.paint(canvas, size);
598 599
    if (arrowheadScale > 0.0)
      paintArrowhead(canvas, size);
600 601 602
  }
}

603 604 605
/// 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
606
/// a complete implementation of swipe-to-refresh driven by a [Scrollable]
607 608
/// widget.
///
609 610 611
/// The indicator arc is displayed with [valueColor], an animated value. To
/// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`.
///
612 613
/// See also:
///
614 615
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
616
class RefreshProgressIndicator extends CircularProgressIndicator {
617 618 619
  /// Creates a refresh progress indicator.
  ///
  /// Rather than creating a refresh progress indicator directly, consider using
Adam Barth's avatar
Adam Barth committed
620
  /// a [RefreshIndicator] together with a [Scrollable] widget.
621
  ///
622
  /// {@macro flutter.material.progressIndicator.parameters}
623
  const RefreshProgressIndicator({
624 625 626
    Key key,
    double value,
    Color backgroundColor,
627
    Animation<Color> valueColor,
628
    double strokeWidth = 2.0, // Different default than CircularProgressIndicator.
629 630
    String semanticsLabel,
    String semanticsValue,
631 632 633 634
  }) : super(
    key: key,
    value: value,
    backgroundColor: backgroundColor,
635 636
    valueColor: valueColor,
    strokeWidth: strokeWidth,
637 638
    semanticsLabel: semanticsLabel,
    semanticsValue: semanticsValue,
639
  );
640 641

  @override
642
  _RefreshProgressIndicatorState createState() => _RefreshProgressIndicatorState();
643 644 645
}

class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
646
  static const double _indicatorSize = 40.0;
647

648 649 650 651 652 653
  // 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) {
654
    if (widget.value != null)
655
      _controller.value = widget.value * (1333 / 2 / _kIndeterminateCircularDuration);
656 657
    else if (!_controller.isAnimating)
      _controller.repeat();
658 659 660
    return _buildAnimation();
  }

661
  @override
662
  Widget _buildIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
663
    final double arrowheadScale = widget.value == null ? 0.0 : ((widget.value * 2.0).clamp(0.0, 1.0) as double);
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681
    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: widget.backgroundColor ?? Theme.of(context).canvasColor,
          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,
682
                offsetValue: offsetValue,
683 684 685 686
                rotationValue: rotationValue,
                strokeWidth: widget.strokeWidth,
                arrowheadScale: arrowheadScale,
              ),
687 688 689 690
            ),
          ),
        ),
      ),
691 692 693
    );
  }
}