progress_indicator.dart 32.7 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
  Color _getValueColor(BuildContext context, {Color? defaultColor}) {
    return valueColor?.value ??
113 114
      color ??
      ProgressIndicatorTheme.of(context).color ??
115
      defaultColor ??
116 117
      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
    final ProgressIndicatorThemeData defaults = Theme.of(context).useMaterial3
      ? _LinearProgressIndicatorDefaultsM3(context)
      : _LinearProgressIndicatorDefaultsM2(context);

338
    final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context);
339
    final Color trackColor = widget.backgroundColor ??
340
      indicatorTheme.linearTrackColor ??
341 342 343 344
      defaults.linearTrackColor!;
    final double minHeight = widget.minHeight ??
      indicatorTheme.linearMinHeight ??
      defaults.linearMinHeight!;
345

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

366
  @override
367
  Widget build(BuildContext context) {
368
    final TextDirection textDirection = Directionality.of(context);
369

370
    if (widget.value != null) {
371
      return _buildIndicator(context, _controller.value, textDirection);
372
    }
373

374
    return AnimatedBuilder(
375
      animation: _controller.view,
376
      builder: (BuildContext context, Widget? child) {
377
        return _buildIndicator(context, _controller.value, textDirection);
378
      },
379 380 381 382
    );
  }
}

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

400
  final Color? backgroundColor;
401
  final Color valueColor;
402
  final double? value;
403 404
  final double headValue;
  final double tailValue;
405
  final double offsetValue;
406
  final double rotationValue;
407 408 409
  final double strokeWidth;
  final double arcStart;
  final double arcSweep;
410

411 412 413 414 415 416
  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;

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

431
    if (value == null) { // Indeterminate
432
      paint.strokeCap = StrokeCap.square;
433
    }
434

435
    canvas.drawArc(Offset.zero & size, arcStart, arcSweep, false, paint);
436 437
  }

438
  @override
439
  bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) {
440 441
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
442
        || oldPainter.value != value
443 444
        || oldPainter.headValue != headValue
        || oldPainter.tailValue != tailValue
445
        || oldPainter.offsetValue != offsetValue
446 447
        || oldPainter.rotationValue != rotationValue
        || oldPainter.strokeWidth != strokeWidth;
448 449 450
  }
}

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

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

516 517
  final _ActivityIndicatorType _indicatorType;

518 519 520 521 522 523 524 525 526 527
  /// {@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;

528 529 530
  /// The width of the line used to draw the circle.
  final double strokeWidth;

531
  @override
532
  State<CircularProgressIndicator> createState() => _CircularProgressIndicatorState();
533
}
534

535
class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin {
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551
  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));

552
  late AnimationController _controller;
553

554
  @override
555 556
  void initState() {
    super.initState();
557
    _controller = AnimationController(
558
      duration: const Duration(milliseconds: _kIndeterminateCircularDuration),
559
      vsync: this,
560
    );
561
    if (widget.value == null) {
562
      _controller.repeat();
563
    }
564 565 566 567 568
  }

  @override
  void didUpdateWidget(CircularProgressIndicator oldWidget) {
    super.didUpdateWidget(oldWidget);
569
    if (widget.value == null && !_controller.isAnimating) {
570
      _controller.repeat();
571
    } else if (widget.value != null && _controller.isAnimating) {
572
      _controller.stop();
573
    }
574 575
  }

576
  @override
577
  void dispose() {
578
    _controller.dispose();
579 580 581
    super.dispose();
  }

582
  Widget _buildCupertinoIndicator(BuildContext context) {
583 584
    final Color? tickColor = widget.backgroundColor;
    return CupertinoActivityIndicator(key: widget.key, color: tickColor);
585 586 587
  }

  Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
588 589 590
    final ProgressIndicatorThemeData defaults = Theme.of(context).useMaterial3
      ? _CircularProgressIndicatorDefaultsM3(context)
      : _CircularProgressIndicatorDefaultsM2(context);
591 592
    final Color? trackColor = widget.backgroundColor ?? ProgressIndicatorTheme.of(context).circularTrackColor;

593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
    Widget progressIndicator = Container(
      constraints: const BoxConstraints(
        minWidth: _kMinCircularProgressIndicatorSize,
        minHeight: _kMinCircularProgressIndicatorSize,
      ),
      child: CustomPaint(
        painter: _CircularProgressIndicatorPainter(
          backgroundColor: trackColor,
          valueColor: widget._getValueColor(context, defaultColor: defaults.color),
          value: widget.value, // may be null
          headValue: headValue, // remaining arguments are ignored if widget.value is not null
          tailValue: tailValue,
          offsetValue: offsetValue,
          rotationValue: rotationValue,
          strokeWidth: widget.strokeWidth,
608 609
        ),
      ),
610
    );
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625

    if (Theme.of(context).useMaterial3) {
      progressIndicator = SizedBox(
        height: _CircularProgressIndicatorDefaultsM3.circularProgressIndicatorSize,
        width: _CircularProgressIndicatorDefaultsM3.circularProgressIndicatorSize,
        child: Center(
          child: progressIndicator
        ),
      );
    }

    return widget._buildSemanticsWrapper(
      context: context,
      child: progressIndicator,
    );
626 627
  }

628
  Widget _buildAnimation() {
629
    return AnimatedBuilder(
630
      animation: _controller,
631
      builder: (BuildContext context, Widget? child) {
632
        return _buildMaterialIndicator(
633
          context,
634 635 636 637
          _strokeHeadTween.evaluate(_controller),
          _strokeTailTween.evaluate(_controller),
          _offsetTween.evaluate(_controller),
          _rotationTween.evaluate(_controller),
638
        );
639
      },
640 641
    );
  }
642 643 644

  @override
  Widget build(BuildContext context) {
645 646
    switch (widget._indicatorType) {
      case _ActivityIndicatorType.material:
647
        if (widget.value != null) {
648
          return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0);
649
        }
650 651
        return _buildAnimation();
      case _ActivityIndicatorType.adaptive:
652
        final ThemeData theme = Theme.of(context);
653 654 655 656 657 658 659 660 661
        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:
662
            if (widget.value != null) {
663
              return _buildMaterialIndicator(context, 0.0, 0.0, 0, 0.0);
664
            }
665 666 667
            return _buildAnimation();
        }
    }
668
  }
669
}
670 671 672

class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
  _RefreshProgressIndicatorPainter({
673 674 675 676 677 678 679
    required super.valueColor,
    required super.value,
    required super.headValue,
    required super.tailValue,
    required super.offsetValue,
    required super.rotationValue,
    required super.strokeWidth,
680
    required this.arrowheadScale,
681
  });
682

683 684
  final double arrowheadScale;

685 686
  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
687
    // (So ux, -uy points in the direction the arrowhead points.)
688 689 690 691 692 693
    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;
694 695
    final double arrowheadPointX = radius + ux * radius + -uy * strokeWidth * 2.0 * arrowheadScale;
    final double arrowheadPointY = radius + uy * radius +  ux * strokeWidth * 2.0 * arrowheadScale;
696
    final double arrowheadRadius = strokeWidth * 2.0 * arrowheadScale;
697 698
    final double innerRadius = radius - arrowheadRadius;
    final double outerRadius = radius + arrowheadRadius;
699

700
    final Path path = Path()
701 702
      ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
      ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
703
      ..lineTo(arrowheadPointX, arrowheadPointY)
704
      ..close();
705
    final Paint paint = Paint()
706 707 708 709 710 711 712 713 714
      ..color = valueColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.fill;
    canvas.drawPath(path, paint);
  }

  @override
  void paint(Canvas canvas, Size size) {
    super.paint(canvas, size);
715
    if (arrowheadScale > 0.0) {
716
      paintArrowhead(canvas, size);
717
    }
718 719 720
  }
}

721 722 723
/// 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
724
/// a complete implementation of swipe-to-refresh driven by a [Scrollable]
725 726
/// widget.
///
727 728 729
/// The indicator arc is displayed with [valueColor], an animated value. To
/// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`.
///
730 731
/// See also:
///
732 733
///  * [RefreshIndicator], which automatically displays a [CircularProgressIndicator]
///    when the underlying vertical scrollable is overscrolled.
734
class RefreshProgressIndicator extends CircularProgressIndicator {
735 736 737
  /// Creates a refresh progress indicator.
  ///
  /// Rather than creating a refresh progress indicator directly, consider using
Adam Barth's avatar
Adam Barth committed
738
  /// a [RefreshIndicator] together with a [Scrollable] widget.
739
  ///
740
  /// {@macro flutter.material.ProgressIndicator.ProgressIndicator}
741
  const RefreshProgressIndicator({
742 743 744 745 746 747 748 749 750
    super.key,
    super.value,
    super.backgroundColor,
    super.color,
    super.valueColor,
    super.strokeWidth = defaultStrokeWidth, // Different default than CircularProgressIndicator.
    super.semanticsLabel,
    super.semanticsValue,
  });
751

752 753 754
  /// Default stroke width.
  static const double defaultStrokeWidth = 2.5;

755 756 757 758 759 760 761 762 763 764
  /// {@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;
765

766
  @override
767
  State<CircularProgressIndicator> createState() => _RefreshProgressIndicatorState();
768 769 770
}

class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796
  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;
797

798
  // Always show the indeterminate version of the circular progress indicator.
799
  //
800
  // When value is non-null the sweep of the progress indicator arrow's arc
801 802 803
  // varies from 0 to about 300 degrees.
  //
  // When value is null the arrow animation starting from wherever we left it.
804 805
  @override
  Widget build(BuildContext context) {
806 807 808 809 810 811
    final double? value = widget.value;
    if (value != null) {
      _lastValue = value;
      _controller.value = _convertTween.transform(value)
        * (1333 / 2 / _kIndeterminateCircularDuration);
    }
812 813 814
    return _buildAnimation();
  }

815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831
  @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),
        );
      },
    );
  }

832
  @override
833
  Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) {
834 835 836 837 838 839 840 841 842 843 844 845 846 847
    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);

848 849 850 851
    final Color backgroundColor =
      widget.backgroundColor ??
      ProgressIndicatorTheme.of(context).refreshBackgroundColor ??
      Theme.of(context).canvasColor;
852

853 854 855 856 857 858 859 860
    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,
861
          color: backgroundColor,
862 863 864
          elevation: 2.0,
          child: Padding(
            padding: const EdgeInsets.all(12.0),
865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
            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,
                  ),
                ),
881
              ),
882 883 884 885
            ),
          ),
        ),
      ),
886 887 888
    );
  }
}
889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954

// Hand coded defaults based on Material Design 2.
class _CircularProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData {
  _CircularProgressIndicatorDefaultsM2(this.context);

  final BuildContext context;
  late final ColorScheme _colors = Theme.of(context).colorScheme;

  @override
  Color get color => _colors.primary;
}

class _LinearProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData {
  _LinearProgressIndicatorDefaultsM2(this.context);

  final BuildContext context;
  late final ColorScheme _colors = Theme.of(context).colorScheme;

  @override
  Color get color => _colors.primary;

  @override
  Color get linearTrackColor => _colors.background;

  @override
  double get linearMinHeight => 4.0;
}

// BEGIN GENERATED TOKEN PROPERTIES - ProgressIndicator

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

// Token database version: v0_132

class _CircularProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData {
  _CircularProgressIndicatorDefaultsM3(this.context);

  final BuildContext context;
  late final ColorScheme _colors = Theme.of(context).colorScheme;

  static const double circularProgressIndicatorSize = 48.0;

  @override
  Color get color => _colors.primary;
}

class _LinearProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData {
  _LinearProgressIndicatorDefaultsM3(this.context);

  final BuildContext context;
  late final ColorScheme _colors = Theme.of(context).colorScheme;

  @override
  Color get color => _colors.primary;

  @override
  Color get linearTrackColor => _colors.surfaceVariant;

  @override
  double get linearMinHeight => 4.0;
}

// END GENERATED TOKEN PROPERTIES - ProgressIndicator