progress_indicator.dart 30 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
  /// The [value] argument can either be null for an indeterminate
35 36
  /// progress indicator, or a non-null value between 0.0 and 1.0 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
    super.key,
46 47
    this.value,
    this.backgroundColor,
48
    this.color,
49
    this.valueColor,
50 51
    this.semanticsLabel,
    this.semanticsValue,
52
  });
53

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
  /// The value will be clamped to be in the range 0.0-1.0.
58 59
  ///
  /// If null, this progress indicator is indeterminate, which means the
60
  /// indicator displays a predetermined animation that does not indicate how
61
  /// much actual progress is being made.
62
  final double? value;
63

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

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

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

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

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

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

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

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

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

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

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

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

186
    paint.color = valueColor;
187

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

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

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

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

229
/// A Material Design linear progress indicator, also known as a progress bar.
230
///
231 232
/// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0}
///
233 234 235 236 237 238 239 240 241 242 243 244
/// 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].
///
245 246 247
/// The indicator line is displayed with [valueColor], an animated value. To
/// specify a constant color value use: `AlwaysStoppedAnimation<Color>(color)`.
///
248 249 250
/// The minimum height of the indicator can be specified using [minHeight].
/// The indicator can be made taller by wrapping the widget with a [SizedBox].
///
251
/// {@tool dartpad}
252 253
/// This example shows a [LinearProgressIndicator] with a changing value.
///
254
/// ** See code in examples/api/lib/material/progress_indicator/linear_progress_indicator.0.dart **
255 256
/// {@end-tool}
///
257 258
/// See also:
///
259 260 261
///  * [CircularProgressIndicator], which shows progress along a circular arc.
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
262
///  * <https://material.io/design/components/progress-indicators.html#linear-progress-indicators>
263
class LinearProgressIndicator extends ProgressIndicator {
264 265
  /// Creates a linear progress indicator.
  ///
266
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
267
  const LinearProgressIndicator({
268 269 270 271 272
    super.key,
    super.value,
    super.backgroundColor,
    super.color,
    super.valueColor,
273
    this.minHeight,
274 275 276
    super.semanticsLabel,
    super.semanticsValue,
  }) : assert(minHeight == null || minHeight > 0);
277 278 279

  /// {@template flutter.material.LinearProgressIndicator.trackColor}
  /// Color of the track being filled by the linear indicator.
280
  ///
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
  /// 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}
296
  final double? minHeight;
297

298
  @override
299
  State<LinearProgressIndicator> createState() => _LinearProgressIndicatorState();
300 301
}

302
class _LinearProgressIndicatorState extends State<LinearProgressIndicator> with SingleTickerProviderStateMixin {
303
  late AnimationController _controller;
304

305
  @override
306 307
  void initState() {
    super.initState();
308
    _controller = AnimationController(
309
      duration: const Duration(milliseconds: _kIndeterminateLinearDuration),
310
      vsync: this,
311
    );
312
    if (widget.value == null) {
313
      _controller.repeat();
314
    }
315 316 317 318 319
  }

  @override
  void didUpdateWidget(LinearProgressIndicator oldWidget) {
    super.didUpdateWidget(oldWidget);
320
    if (widget.value == null && !_controller.isAnimating) {
321
      _controller.repeat();
322
    } else if (widget.value != null && _controller.isAnimating) {
323
      _controller.stop();
324
    }
325 326
  }

327
  @override
328
  void dispose() {
329
    _controller.dispose();
330 331 332
    super.dispose();
  }

333
  Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) {
334 335 336 337 338 339 340
    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;

341 342 343
    return widget._buildSemanticsWrapper(
      context: context,
      child: Container(
344
        constraints: BoxConstraints(
345
          minWidth: double.infinity,
346
          minHeight: minHeight,
347 348 349
        ),
        child: CustomPaint(
          painter: _LinearProgressIndicatorPainter(
350
            backgroundColor: trackColor,
351 352 353 354 355
            valueColor: widget._getValueColor(context),
            value: widget.value, // may be null
            animationValue: animationValue, // ignored if widget.value is not null
            textDirection: textDirection,
          ),
356 357
        ),
      ),
358 359 360
    );
  }

361
  @override
362
  Widget build(BuildContext context) {
363
    final TextDirection textDirection = Directionality.of(context);
364

365
    if (widget.value != null) {
366
      return _buildIndicator(context, _controller.value, textDirection);
367
    }
368

369
    return AnimatedBuilder(
370
      animation: _controller.view,
371
      builder: (BuildContext context, Widget? child) {
372
        return _buildIndicator(context, _controller.value, textDirection);
373
      },
374 375 376 377
    );
  }
}

378
class _CircularProgressIndicatorPainter extends CustomPainter {
379
  _CircularProgressIndicatorPainter({
380
    this.backgroundColor,
381 382 383 384 385 386 387
    required this.valueColor,
    required this.value,
    required this.headValue,
    required this.tailValue,
    required this.offsetValue,
    required this.rotationValue,
    required this.strokeWidth,
388
  }) : arcStart = value != null
389
         ? _startAngle
390
         : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 2.0 + offsetValue * 0.5 * math.pi,
391
       arcSweep = value != null
392
         ? clampDouble(value, 0.0, 1.0) * _sweep
393
         : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon);
394

395
  final Color? backgroundColor;
396
  final Color valueColor;
397
  final double? value;
398 399
  final double headValue;
  final double tailValue;
400
  final double offsetValue;
401
  final double rotationValue;
402 403 404
  final double strokeWidth;
  final double arcStart;
  final double arcSweep;
405

406 407 408 409 410 411
  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;

412
  @override
413
  void paint(Canvas canvas, Size size) {
414
    final Paint paint = Paint()
415
      ..color = valueColor
416
      ..strokeWidth = strokeWidth
417
      ..style = PaintingStyle.stroke;
418 419
    if (backgroundColor != null) {
      final Paint backgroundPaint = Paint()
420
        ..color = backgroundColor!
421 422 423 424
        ..strokeWidth = strokeWidth
        ..style = PaintingStyle.stroke;
      canvas.drawArc(Offset.zero & size, 0, _sweep, false, backgroundPaint);
    }
425

426
    if (value == null) { // Indeterminate
427
      paint.strokeCap = StrokeCap.square;
428
    }
429

430
    canvas.drawArc(Offset.zero & size, arcStart, arcSweep, false, paint);
431 432
  }

433
  @override
434
  bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) {
435 436
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
437
        || oldPainter.value != value
438 439
        || oldPainter.headValue != headValue
        || oldPainter.tailValue != tailValue
440
        || oldPainter.offsetValue != offsetValue
441 442
        || oldPainter.rotationValue != rotationValue
        || oldPainter.strokeWidth != strokeWidth;
443 444 445
  }
}

446
/// A Material Design circular progress indicator, which spins to indicate that
447
/// the application is busy.
448
///
449 450
/// {@youtube 560 315 https://www.youtube.com/watch?v=O-rhXZLtpv0}
///
451 452 453 454 455 456 457 458 459 460 461 462
/// 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].
///
463 464 465
/// The indicator arc is displayed with [valueColor], an animated value. To
/// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`.
///
466
/// {@tool dartpad}
467 468
/// This example shows a [CircularProgressIndicator] with a changing value.
///
469
/// ** See code in examples/api/lib/material/progress_indicator/circular_progress_indicator.0.dart **
470 471
/// {@end-tool}
///
472 473
/// See also:
///
474 475 476
///  * [LinearProgressIndicator], which displays progress along a line.
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
477
///  * <https://material.io/design/components/progress-indicators.html#circular-progress-indicators>
478
class CircularProgressIndicator extends ProgressIndicator {
479 480
  /// Creates a circular progress indicator.
  ///
481
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
482
  const CircularProgressIndicator({
483 484 485 486 487
    super.key,
    super.value,
    super.backgroundColor,
    super.color,
    super.valueColor,
488
    this.strokeWidth = 4.0,
489 490 491
    super.semanticsLabel,
    super.semanticsValue,
  }) : _indicatorType = _ActivityIndicatorType.material;
492 493 494 495 496 497 498 499

  /// 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.
  ///
500
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
501
  const CircularProgressIndicator.adaptive({
502 503 504 505
    super.key,
    super.value,
    super.backgroundColor,
    super.valueColor,
506
    this.strokeWidth = 4.0,
507 508 509
    super.semanticsLabel,
    super.semanticsValue,
  }) : _indicatorType = _ActivityIndicatorType.adaptive;
510

511 512
  final _ActivityIndicatorType _indicatorType;

513 514 515 516 517 518 519 520 521 522
  /// {@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;

523 524 525
  /// The width of the line used to draw the circle.
  final double strokeWidth;

526
  @override
527
  State<CircularProgressIndicator> createState() => _CircularProgressIndicatorState();
528
}
529

530
class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin {
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
  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));

547
  late AnimationController _controller;
548

549
  @override
550 551
  void initState() {
    super.initState();
552
    _controller = AnimationController(
553
      duration: const Duration(milliseconds: _kIndeterminateCircularDuration),
554
      vsync: this,
555
    );
556
    if (widget.value == null) {
557
      _controller.repeat();
558
    }
559 560 561 562 563
  }

  @override
  void didUpdateWidget(CircularProgressIndicator oldWidget) {
    super.didUpdateWidget(oldWidget);
564
    if (widget.value == null && !_controller.isAnimating) {
565
      _controller.repeat();
566
    } else if (widget.value != null && _controller.isAnimating) {
567
      _controller.stop();
568
    }
569 570
  }

571
  @override
572
  void dispose() {
573
    _controller.dispose();
574 575 576
    super.dispose();
  }

577
  Widget _buildCupertinoIndicator(BuildContext context) {
578 579
    final Color? tickColor = widget.backgroundColor;
    return CupertinoActivityIndicator(key: widget.key, color: tickColor);
580 581 582
  }

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

585 586 587 588 589 590 591 592 593
    return widget._buildSemanticsWrapper(
      context: context,
      child: Container(
        constraints: const BoxConstraints(
          minWidth: _kMinCircularProgressIndicatorSize,
          minHeight: _kMinCircularProgressIndicatorSize,
        ),
        child: CustomPaint(
          painter: _CircularProgressIndicatorPainter(
594
            backgroundColor: trackColor,
595 596 597 598
            valueColor: widget._getValueColor(context),
            value: widget.value, // may be null
            headValue: headValue, // remaining arguments are ignored if widget.value is not null
            tailValue: tailValue,
599
            offsetValue: offsetValue,
600 601 602
            rotationValue: rotationValue,
            strokeWidth: widget.strokeWidth,
          ),
603 604
        ),
      ),
605 606 607
    );
  }

608
  Widget _buildAnimation() {
609
    return AnimatedBuilder(
610
      animation: _controller,
611
      builder: (BuildContext context, Widget? child) {
612
        return _buildMaterialIndicator(
613
          context,
614 615 616 617
          _strokeHeadTween.evaluate(_controller),
          _strokeTailTween.evaluate(_controller),
          _offsetTween.evaluate(_controller),
          _rotationTween.evaluate(_controller),
618
        );
619
      },
620 621
    );
  }
622 623 624

  @override
  Widget build(BuildContext context) {
625 626
    switch (widget._indicatorType) {
      case _ActivityIndicatorType.material:
627
        if (widget.value != null) {
628
          return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0);
629
        }
630 631
        return _buildAnimation();
      case _ActivityIndicatorType.adaptive:
632
        final ThemeData theme = Theme.of(context);
633 634 635 636 637 638 639 640 641
        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:
642
            if (widget.value != null) {
643
              return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0);
644
            }
645 646 647
            return _buildAnimation();
        }
    }
648
  }
649
}
650 651 652

class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
  _RefreshProgressIndicatorPainter({
653 654 655 656 657 658 659
    required super.valueColor,
    required super.value,
    required super.headValue,
    required super.tailValue,
    required super.offsetValue,
    required super.rotationValue,
    required super.strokeWidth,
660
    required this.arrowheadScale,
661
  });
662

663 664
  final double arrowheadScale;

665 666
  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
667
    // (So ux, -uy points in the direction the arrowhead points.)
668 669 670 671 672 673
    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;
674 675
    final double arrowheadPointX = radius + ux * radius + -uy * strokeWidth * 2.0 * arrowheadScale;
    final double arrowheadPointY = radius + uy * radius +  ux * strokeWidth * 2.0 * arrowheadScale;
676
    final double arrowheadRadius = strokeWidth * 2.0 * arrowheadScale;
677 678
    final double innerRadius = radius - arrowheadRadius;
    final double outerRadius = radius + arrowheadRadius;
679

680
    final Path path = Path()
681 682
      ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
      ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
683
      ..lineTo(arrowheadPointX, arrowheadPointY)
684
      ..close();
685
    final Paint paint = Paint()
686 687 688 689 690 691 692 693 694
      ..color = valueColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.fill;
    canvas.drawPath(path, paint);
  }

  @override
  void paint(Canvas canvas, Size size) {
    super.paint(canvas, size);
695
    if (arrowheadScale > 0.0) {
696
      paintArrowhead(canvas, size);
697
    }
698 699 700
  }
}

701 702 703
/// 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
704
/// a complete implementation of swipe-to-refresh driven by a [Scrollable]
705 706
/// widget.
///
707 708 709
/// The indicator arc is displayed with [valueColor], an animated value. To
/// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`.
///
710 711
/// See also:
///
712 713
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
714
class RefreshProgressIndicator extends CircularProgressIndicator {
715 716 717
  /// Creates a refresh progress indicator.
  ///
  /// Rather than creating a refresh progress indicator directly, consider using
Adam Barth's avatar
Adam Barth committed
718
  /// a [RefreshIndicator] together with a [Scrollable] widget.
719
  ///
720
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
721
  const RefreshProgressIndicator({
722 723 724 725 726 727 728 729 730
    super.key,
    super.value,
    super.backgroundColor,
    super.color,
    super.valueColor,
    super.strokeWidth = defaultStrokeWidth, // Different default than CircularProgressIndicator.
    super.semanticsLabel,
    super.semanticsValue,
  });
731

732 733 734
  /// Default stroke width.
  static const double defaultStrokeWidth = 2.5;

735 736 737 738 739 740 741 742 743 744
  /// {@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;
745

746
  @override
747
  State<CircularProgressIndicator> createState() => _RefreshProgressIndicatorState();
748 749 750
}

class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
  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;
777

778
  // Always show the indeterminate version of the circular progress indicator.
779
  //
780
  // When value is non-null the sweep of the progress indicator arrow's arc
781 782 783
  // varies from 0 to about 300 degrees.
  //
  // When value is null the arrow animation starting from wherever we left it.
784 785
  @override
  Widget build(BuildContext context) {
786 787 788 789 790 791
    final double? value = widget.value;
    if (value != null) {
      _lastValue = value;
      _controller.value = _convertTween.transform(value)
        * (1333 / 2 / _kIndeterminateCircularDuration);
    }
792 793 794
    return _buildAnimation();
  }

795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811
  @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),
        );
      },
    );
  }

812
  @override
813
  Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
814 815 816 817 818 819 820 821 822 823 824 825 826 827
    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);

828 829 830 831
    final Color backgroundColor =
      widget.backgroundColor ??
      ProgressIndicatorTheme.of(context).refreshBackgroundColor ??
      Theme.of(context).canvasColor;
832

833 834 835 836 837 838 839 840
    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,
841
          color: backgroundColor,
842 843 844
          elevation: 2.0,
          child: Padding(
            padding: const EdgeInsets.all(12.0),
845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860
            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,
                  ),
                ),
861
              ),
862 863 864 865
            ),
          ),
        ),
      ),
866 867 868
    );
  }
}