progress_indicator.dart 19.2 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

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

10
import 'material.dart';
11
import 'theme.dart';
12 13

const double _kLinearProgressIndicatorHeight = 6.0;
14
const double _kMinCircularProgressIndicatorSize = 36.0;
15
const int _kIndeterminateLinearDuration = 1800;
16

17
// TODO(hansmuller): implement the support for buffer indicator
Hixie's avatar
Hixie committed
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.google.com/components/progress-activity.html>
28
abstract class ProgressIndicator extends StatefulWidget {
29 30 31
  /// Creates a progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
32
  /// progress indicator) or non-null (corresponding to a determinate progress
33
  /// indicator). See [value] for details.
34
  const ProgressIndicator({
35
    Key key,
36 37
    this.value,
    this.backgroundColor,
38
    this.valueColor,
39 40
  }) : super(key: key);

41 42 43 44 45 46 47 48
  /// If non-null, the value of this progress indicator with 0.0 corresponding
  /// to no progress having been made and 1.0 corresponding to all the progress
  /// having been made.
  ///
  /// If null, this progress indicator is indeterminate, which means the
  /// indicator displays a predetermined animation that does not indicator how
  /// much actual progress is being made.
  final double value;
49

50 51
  /// The progress indicator's background color. The current theme's
  /// [ThemeData.backgroundColor] by default.
52 53 54 55 56
  final Color backgroundColor;

  /// The indicator's color is the animation's value. To specify a constant
  /// color use: `new AlwaysStoppedAnimation<Color>(color)`.
  ///
57
  /// If null, the progress indicator is rendered with the current theme's
58
  /// [ThemeData.accentColor].
59 60 61
  final Animation<Color> valueColor;

  Color _getBackgroundColor(BuildContext context) => backgroundColor ?? Theme.of(context).backgroundColor;
62
  Color _getValueColor(BuildContext context) => valueColor?.value ?? Theme.of(context).accentColor;
63

64
  @override
65 66
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
67
    properties.add(PercentProperty('value', value, showName: false, ifNull: '<indeterminate>'));
Hixie's avatar
Hixie committed
68
  }
69 70
}

71
class _LinearProgressIndicatorPainter extends CustomPainter {
72 73 74 75 76 77 78 79 80 81 82 83 84 85
  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;

86 87
  // The indeterminate progress animation displays two lines whose leading (head)
  // and trailing (tail) endpoints are defined by the following four curves.
88
  static const Curve line1Head = Interval(
89 90
    0.0,
    750.0 / _kIndeterminateLinearDuration,
91
    curve: Cubic(0.2, 0.0, 0.8, 1.0),
92
  );
93
  static const Curve line1Tail = Interval(
94 95
    333.0 / _kIndeterminateLinearDuration,
    (333.0 + 750.0) / _kIndeterminateLinearDuration,
96
    curve: Cubic(0.4, 0.0, 1.0, 1.0),
97
  );
98
  static const Curve line2Head = Interval(
99 100
    1000.0 / _kIndeterminateLinearDuration,
    (1000.0 + 567.0) / _kIndeterminateLinearDuration,
101
    curve: Cubic(0.0, 0.0, 0.65, 1.0),
102
  );
103
  static const Curve line2Tail = Interval(
104 105
    1267.0 / _kIndeterminateLinearDuration,
    (1267.0 + 533.0) / _kIndeterminateLinearDuration,
106
    curve: Cubic(0.10, 0.0, 0.45, 1.0),
107 108
  );

109
  @override
110
  void paint(Canvas canvas, Size size) {
111
    final Paint paint = Paint()
112
      ..color = backgroundColor
113
      ..style = PaintingStyle.fill;
114
    canvas.drawRect(Offset.zero & size, paint);
115

116
    paint.color = valueColor;
117

118 119 120
    void drawBar(double x, double width) {
      if (width <= 0.0)
        return;
121 122 123 124 125 126 127 128 129 130

      double left;
      switch (textDirection) {
        case TextDirection.rtl:
          left = size.width - width - x;
          break;
        case TextDirection.ltr:
          left = x;
          break;
      }
131
      canvas.drawRect(Offset(left, 0.0) & Size(width, size.height), paint);
132
    }
133 134 135 136 137 138 139 140 141 142 143 144 145

    if (value != null) {
      drawBar(0.0, value.clamp(0.0, 1.0) * size.width);
    } else {
      final double x1 = size.width * line1Tail.transform(animationValue);
      final double width1 = size.width * line1Head.transform(animationValue) - x1;

      final double x2 = size.width * line2Tail.transform(animationValue);
      final double width2 = size.width * line2Head.transform(animationValue) - x2;

      drawBar(x1, width1);
      drawBar(x2, width2);
    }
146 147
  }

148
  @override
149 150 151 152
  bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) {
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
        || oldPainter.value != value
153 154
        || oldPainter.animationValue != animationValue
        || oldPainter.textDirection != textDirection;
155 156 157
  }
}

158
/// A material design linear progress indicator, also known as a progress bar.
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
///
/// 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].
///
/// See also:
///
///  * [CircularProgressIndicator]
175
///  * <https://material.google.com/components/progress-activity.html#progress-activity-types-of-indicators>
176
class LinearProgressIndicator extends ProgressIndicator {
177 178 179
  /// Creates a linear progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
180
  /// progress indicator) or non-null (corresponding to a determinate progress
181
  /// indicator). See [value] for details.
182
  const LinearProgressIndicator({
183
    Key key,
184
    double value,
185 186 187
    Color backgroundColor,
    Animation<Color> valueColor,
  }) : super(key: key, value: value, backgroundColor: backgroundColor, valueColor: valueColor);
188

189
  @override
190
  _LinearProgressIndicatorState createState() => _LinearProgressIndicatorState();
191 192
}

193
class _LinearProgressIndicatorState extends State<LinearProgressIndicator> with SingleTickerProviderStateMixin {
194 195
  AnimationController _controller;

196
  @override
197 198
  void initState() {
    super.initState();
199
    _controller = AnimationController(
200
      duration: const Duration(milliseconds: _kIndeterminateLinearDuration),
201
      vsync: this,
202 203 204 205 206 207 208 209 210 211 212 213
    );
    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();
214 215
  }

216
  @override
217
  void dispose() {
218
    _controller.dispose();
219 220 221
    super.dispose();
  }

222
  Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) {
223
    return Container(
224
      constraints: const BoxConstraints.tightFor(
225
        width: double.infinity,
226
        height: _kLinearProgressIndicatorHeight,
227
      ),
228 229
      child: CustomPaint(
        painter: _LinearProgressIndicatorPainter(
230 231 232
          backgroundColor: widget._getBackgroundColor(context),
          valueColor: widget._getValueColor(context),
          value: widget.value, // may be null
233
          animationValue: animationValue, // ignored if widget.value is not null
234
          textDirection: textDirection,
235 236
        ),
      ),
237 238 239
    );
  }

240
  @override
241
  Widget build(BuildContext context) {
242 243
    final TextDirection textDirection = Directionality.of(context);

244
    if (widget.value != null)
245
      return _buildIndicator(context, _controller.value, textDirection);
246

247
    return AnimatedBuilder(
248
      animation: _controller.view,
249
      builder: (BuildContext context, Widget child) {
250
        return _buildIndicator(context, _controller.value, textDirection);
251
      },
252 253 254 255
    );
  }
}

256
class _CircularProgressIndicatorPainter extends CustomPainter {
257
  _CircularProgressIndicatorPainter({
258
    this.valueColor,
259 260 261 262 263
    this.value,
    this.headValue,
    this.tailValue,
    this.stepValue,
    this.rotationValue,
264
    this.strokeWidth,
265
  }) : arcStart = value != null
266 267
         ? _startAngle
         : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 1.7 - stepValue * 0.8 * math.pi,
268
       arcSweep = value != null
269 270
         ? value.clamp(0.0, 1.0) * _sweep
         : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon);
271 272 273

  final Color valueColor;
  final double value;
274 275 276 277
  final double headValue;
  final double tailValue;
  final int stepValue;
  final double rotationValue;
278 279 280
  final double strokeWidth;
  final double arcStart;
  final double arcSweep;
281

282 283 284 285 286 287
  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;

288
  @override
289
  void paint(Canvas canvas, Size size) {
290
    final Paint paint = Paint()
291
      ..color = valueColor
292
      ..strokeWidth = strokeWidth
293
      ..style = PaintingStyle.stroke;
294

295
    if (value == null) // Indeterminate
296
      paint.strokeCap = StrokeCap.square;
297

298
    canvas.drawArc(Offset.zero & size, arcStart, arcSweep, false, paint);
299 300
  }

301
  @override
302
  bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) {
303 304
    return oldPainter.valueColor != valueColor
        || oldPainter.value != value
305 306 307
        || oldPainter.headValue != headValue
        || oldPainter.tailValue != tailValue
        || oldPainter.stepValue != stepValue
308 309
        || oldPainter.rotationValue != rotationValue
        || oldPainter.strokeWidth != strokeWidth;
310 311 312
  }
}

313 314
/// A material design circular progress indicator, which spins to indicate that
/// the application is busy.
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
///
/// 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].
///
/// See also:
///
///  * [LinearProgressIndicator]
331
///  * <https://material.google.com/components/progress-activity.html#progress-activity-types-of-indicators>
332
class CircularProgressIndicator extends ProgressIndicator {
333 334 335
  /// Creates a circular progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
336
  /// progress indicator) or non-null (corresponding to a determinate progress
337
  /// indicator). See [value] for details.
338
  const CircularProgressIndicator({
339
    Key key,
340 341
    double value,
    Color backgroundColor,
342
    Animation<Color> valueColor,
343
    this.strokeWidth = 4.0,
344
  }) : super(key: key, value: value, backgroundColor: backgroundColor, valueColor: valueColor);
345

346 347 348
  /// The width of the line used to draw the circle.
  final double strokeWidth;

349
  @override
350
  _CircularProgressIndicatorState createState() => _CircularProgressIndicatorState();
351
}
352

353
// Tweens used by circular progress indicator
354
final Animatable<double> _kStrokeHeadTween = CurveTween(
355
  curve: const Interval(0.0, 0.5, curve: Curves.fastOutSlowIn),
356
).chain(CurveTween(
357
  curve: const SawTooth(5),
Hixie's avatar
Hixie committed
358 359
));

360
final Animatable<double> _kStrokeTailTween = CurveTween(
361
  curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn),
362
).chain(CurveTween(
363
  curve: const SawTooth(5),
Hixie's avatar
Hixie committed
364 365
));

366
final Animatable<int> _kStepTween = StepTween(begin: 0, end: 5);
Hixie's avatar
Hixie committed
367

368
final Animatable<double> _kRotationTween = CurveTween(curve: const SawTooth(5));
369

370
class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin {
371
  AnimationController _controller;
372

373
  @override
374 375
  void initState() {
    super.initState();
376
    _controller = AnimationController(
377
      duration: const Duration(seconds: 5),
378
      vsync: this,
379 380 381 382 383 384 385 386 387 388 389 390
    );
    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();
391 392
  }

393
  @override
394
  void dispose() {
395
    _controller.dispose();
396 397 398
    super.dispose();
  }

399
  Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
400
    return Container(
401
      constraints: const BoxConstraints(
402
        minWidth: _kMinCircularProgressIndicatorSize,
403
        minHeight: _kMinCircularProgressIndicatorSize,
404
      ),
405 406
      child: CustomPaint(
        painter: _CircularProgressIndicatorPainter(
407 408 409
          valueColor: widget._getValueColor(context),
          value: widget.value, // may be null
          headValue: headValue, // remaining arguments are ignored if widget.value is not null
410 411
          tailValue: tailValue,
          stepValue: stepValue,
412
          rotationValue: rotationValue,
413 414 415
          strokeWidth: widget.strokeWidth,
        ),
      ),
416 417 418
    );
  }

419
  Widget _buildAnimation() {
420
    return AnimatedBuilder(
421
      animation: _controller,
422
      builder: (BuildContext context, Widget child) {
423 424
        return _buildIndicator(
          context,
425 426 427
          _kStrokeHeadTween.evaluate(_controller),
          _kStrokeTailTween.evaluate(_controller),
          _kStepTween.evaluate(_controller),
428
          _kRotationTween.evaluate(_controller),
429
        );
430
      },
431 432
    );
  }
433 434 435

  @override
  Widget build(BuildContext context) {
436
    if (widget.value != null)
437 438 439
      return _buildIndicator(context, 0.0, 0.0, 0, 0.0);
    return _buildAnimation();
  }
440
}
441 442 443 444 445 446 447 448 449

class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
  _RefreshProgressIndicatorPainter({
    Color valueColor,
    double value,
    double headValue,
    double tailValue,
    int stepValue,
    double rotationValue,
450
    double strokeWidth,
451
    this.arrowheadScale,
452 453 454 455 456 457 458
  }) : super(
    valueColor: valueColor,
    value: value,
    headValue: headValue,
    tailValue: tailValue,
    stepValue: stepValue,
    rotationValue: rotationValue,
459
    strokeWidth: strokeWidth,
460 461
  );

462 463
  final double arrowheadScale;

464 465
  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
466
    // (So ux, -uy points in the direction the arrowhead points.)
467 468 469 470 471 472
    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;
473 474 475 476 477
    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;
478

479
    final Path path = Path()
480 481
      ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
      ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
482
      ..lineTo(arrowheadPointX, arrowheadPointY)
483
      ..close();
484
    final Paint paint = Paint()
485 486 487 488 489 490 491 492 493
      ..color = valueColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.fill;
    canvas.drawPath(path, paint);
  }

  @override
  void paint(Canvas canvas, Size size) {
    super.paint(canvas, size);
494 495
    if (arrowheadScale > 0.0)
      paintArrowhead(canvas, size);
496 497 498
  }
}

499 500 501
/// 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
502
/// a complete implementation of swipe-to-refresh driven by a [Scrollable]
503 504 505 506 507
/// widget.
///
/// See also:
///
///  * [RefreshIndicator]
508
class RefreshProgressIndicator extends CircularProgressIndicator {
509 510 511
  /// Creates a refresh progress indicator.
  ///
  /// Rather than creating a refresh progress indicator directly, consider using
Adam Barth's avatar
Adam Barth committed
512
  /// a [RefreshIndicator] together with a [Scrollable] widget.
513
  const RefreshProgressIndicator({
514 515 516
    Key key,
    double value,
    Color backgroundColor,
517
    Animation<Color> valueColor,
518
    double strokeWidth = 2.0, // Different default than CircularProgressIndicator.
519 520 521 522
  }) : super(
    key: key,
    value: value,
    backgroundColor: backgroundColor,
523 524
    valueColor: valueColor,
    strokeWidth: strokeWidth,
525
  );
526 527

  @override
528
  _RefreshProgressIndicatorState createState() => _RefreshProgressIndicatorState();
529 530 531
}

class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
532
  static const double _indicatorSize = 40.0;
533

534 535 536 537 538 539
  // 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) {
540 541
    if (widget.value != null)
      _controller.value = widget.value / 10.0;
542 543
    else if (!_controller.isAnimating)
      _controller.repeat();
544 545 546
    return _buildAnimation();
  }

547 548
  @override
  Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
549
    final double arrowheadScale = widget.value == null ? 0.0 : (widget.value * 2.0).clamp(0.0, 1.0);
550
    return Container(
551 552
      width: _indicatorSize,
      height: _indicatorSize,
Josh Soref's avatar
Josh Soref committed
553
      margin: const EdgeInsets.all(4.0), // accommodate the shadow
554
      child: Material(
555
        type: MaterialType.circle,
556
        color: widget.backgroundColor ?? Theme.of(context).canvasColor,
557
        elevation: 2.0,
558
        child: Padding(
559
          padding: const EdgeInsets.all(12.0),
560 561
          child: CustomPaint(
            painter: _RefreshProgressIndicatorPainter(
562
              valueColor: widget._getValueColor(context),
563 564
              value: null, // Draw the indeterminate progress indicator.
              headValue: headValue,
565 566 567
              tailValue: tailValue,
              stepValue: stepValue,
              rotationValue: rotationValue,
568 569 570 571 572 573
              strokeWidth: widget.strokeWidth,
              arrowheadScale: arrowheadScale,
            ),
          ),
        ),
      ),
574 575 576
    );
  }
}