progress_indicator.dart 18.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

16
// TODO(hansmuller): implement the support for buffer indicator
Hixie's avatar
Hixie committed
17

18
/// A base class for material design progress indicators.
19 20 21 22 23 24 25
///
/// This widget cannot be instantiated directly. For a linear progress
/// indicator, see [LinearProgressIndicator]. For a circular progress indicator,
/// see [CircularProgressIndicator].
///
/// See also:
///
26
///  * <https://material.google.com/components/progress-activity.html>
27
abstract class ProgressIndicator extends StatefulWidget {
28 29 30
  /// Creates a progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
31
  /// progress indicator) or non-null (corresponding to a determinate progress
32
  /// indicator). See [value] for details.
33
  const ProgressIndicator({
34
    Key key,
35 36
    this.value,
    this.backgroundColor,
37
    this.valueColor,
38 39
  }) : super(key: key);

40 41 42 43 44 45 46 47
  /// 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;
48

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

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

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

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

70 71 72 73 74
class _LinearProgressIndicatorPainter extends CustomPainter {
  const _LinearProgressIndicatorPainter({
    this.backgroundColor,
    this.valueColor,
    this.value,
75
    this.animationValue,
76 77
    @required this.textDirection,
  }) : assert(textDirection != null);
78 79 80 81

  final Color backgroundColor;
  final Color valueColor;
  final double value;
82
  final double animationValue;
83
  final TextDirection textDirection;
84

85
  @override
86
  void paint(Canvas canvas, Size size) {
87
    final Paint paint = new Paint()
88
      ..color = backgroundColor
89
      ..style = PaintingStyle.fill;
90
    canvas.drawRect(Offset.zero & size, paint);
91

92
    paint.color = valueColor;
93
    if (value != null) {
94
      final double width = value.clamp(0.0, 1.0) * size.width;
95 96 97 98 99 100 101 102 103 104 105 106

      double left;
      switch (textDirection) {
        case TextDirection.rtl:
          left = size.width - width;
          break;
        case TextDirection.ltr:
          left = 0.0;
          break;
      }

      canvas.drawRect(new Offset(left, 0.0) & new Size(width, size.height), paint);
107
    } else {
108 109 110 111
      final double startX = size.width * (1.5 * animationValue - 0.5);
      final double endX = startX + 0.5 * size.width;
      final double x = startX.clamp(0.0, size.width);
      final double width = endX.clamp(0.0, size.width) - x;
112 113 114 115 116 117 118 119 120 121 122 123

      double left;
      switch (textDirection) {
        case TextDirection.rtl:
          left = size.width - width - x;
          break;
        case TextDirection.ltr:
          left = x;
          break;
      }

      canvas.drawRect(new Offset(left, 0.0) & new Size(width, size.height), paint);
124 125 126
    }
  }

127
  @override
128 129 130 131
  bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) {
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
        || oldPainter.value != value
132 133
        || oldPainter.animationValue != animationValue
        || oldPainter.textDirection != textDirection;
134 135 136
  }
}

137
/// A material design linear progress indicator, also known as a progress bar.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
///
/// 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]
154
///  * <https://material.google.com/components/progress-activity.html#progress-activity-types-of-indicators>
155
class LinearProgressIndicator extends ProgressIndicator {
156 157 158
  /// Creates a linear progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
159
  /// progress indicator) or non-null (corresponding to a determinate progress
160
  /// indicator). See [value] for details.
161
  const LinearProgressIndicator({
162
    Key key,
163
    double value,
164 165 166
    Color backgroundColor,
    Animation<Color> valueColor,
  }) : super(key: key, value: value, backgroundColor: backgroundColor, valueColor: valueColor);
167

168
  @override
169
  _LinearProgressIndicatorState createState() => new _LinearProgressIndicatorState();
170 171
}

172
class _LinearProgressIndicatorState extends State<LinearProgressIndicator> with SingleTickerProviderStateMixin {
173 174 175
  Animation<double> _animation;
  AnimationController _controller;

176
  @override
177 178 179
  void initState() {
    super.initState();
    _controller = new AnimationController(
180 181
      duration: const Duration(milliseconds: 1500),
      vsync: this,
182
    );
183
    _animation = new CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn);
184 185 186 187 188 189 190 191 192 193 194 195

    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();
196 197
  }

198
  @override
199
  void dispose() {
200
    _controller.dispose();
201 202 203
    super.dispose();
  }

204
  Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) {
205
    return new Container(
206
      constraints: const BoxConstraints.tightFor(
207
        width: double.infinity,
208
        height: _kLinearProgressIndicatorHeight,
209 210 211
      ),
      child: new CustomPaint(
        painter: new _LinearProgressIndicatorPainter(
212 213 214
          backgroundColor: widget._getBackgroundColor(context),
          valueColor: widget._getValueColor(context),
          value: widget.value, // may be null
215
          animationValue: animationValue, // ignored if widget.value is not null
216
          textDirection: textDirection,
217 218
        ),
      ),
219 220 221
    );
  }

222
  @override
223
  Widget build(BuildContext context) {
224 225
    final TextDirection textDirection = Directionality.of(context);

226
    if (widget.value != null)
227
      return _buildIndicator(context, _animation.value, textDirection);
228 229 230 231

    return new AnimatedBuilder(
      animation: _animation,
      builder: (BuildContext context, Widget child) {
232
        return _buildIndicator(context, _animation.value, textDirection);
233
      },
234 235 236 237
    );
  }
}

238
class _CircularProgressIndicatorPainter extends CustomPainter {
239
  static const double _kTwoPI = math.pi * 2.0;
240
  static const double _kEpsilon = .001;
Josh Soref's avatar
Josh Soref committed
241
  // Canvas.drawArc(r, 0, 2*PI) doesn't draw anything, so just get close.
242
  static const double _kSweep = _kTwoPI - _kEpsilon;
243
  static const double _kStartAngle = -math.pi / 2.0;
244

245
  _CircularProgressIndicatorPainter({
246
    this.valueColor,
247 248 249 250 251
    this.value,
    this.headValue,
    this.tailValue,
    this.stepValue,
    this.rotationValue,
252
    this.strokeWidth,
253
  }) : arcStart = value != null
254
         ? _kStartAngle
255
         : _kStartAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 1.7 - stepValue * 0.8 * math.pi,
256 257
       arcSweep = value != null
         ? value.clamp(0.0, 1.0) * _kSweep
258
         : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _kEpsilon);
259 260 261

  final Color valueColor;
  final double value;
262 263 264 265
  final double headValue;
  final double tailValue;
  final int stepValue;
  final double rotationValue;
266 267 268
  final double strokeWidth;
  final double arcStart;
  final double arcSweep;
269

270
  @override
271
  void paint(Canvas canvas, Size size) {
272
    final Paint paint = new Paint()
273
      ..color = valueColor
274
      ..strokeWidth = strokeWidth
275
      ..style = PaintingStyle.stroke;
276

277
    if (value == null) // Indeterminate
278
      paint.strokeCap = StrokeCap.square;
279

280
    canvas.drawArc(Offset.zero & size, arcStart, arcSweep, false, paint);
281 282
  }

283
  @override
284
  bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) {
285 286
    return oldPainter.valueColor != valueColor
        || oldPainter.value != value
287 288 289
        || oldPainter.headValue != headValue
        || oldPainter.tailValue != tailValue
        || oldPainter.stepValue != stepValue
290 291
        || oldPainter.rotationValue != rotationValue
        || oldPainter.strokeWidth != strokeWidth;
292 293 294
  }
}

295 296
/// A material design circular progress indicator, which spins to indicate that
/// the application is busy.
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
///
/// 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]
313
///  * <https://material.google.com/components/progress-activity.html#progress-activity-types-of-indicators>
314
class CircularProgressIndicator extends ProgressIndicator {
315 316 317
  /// Creates a circular progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
318
  /// progress indicator) or non-null (corresponding to a determinate progress
319
  /// indicator). See [value] for details.
320
  const CircularProgressIndicator({
321
    Key key,
322 323
    double value,
    Color backgroundColor,
324 325
    Animation<Color> valueColor,
    this.strokeWidth: 4.0,
326
  }) : super(key: key, value: value, backgroundColor: backgroundColor, valueColor: valueColor);
327

328 329 330
  /// The width of the line used to draw the circle.
  final double strokeWidth;

331
  @override
332
  _CircularProgressIndicatorState createState() => new _CircularProgressIndicatorState();
333
}
334

335
// Tweens used by circular progress indicator
Hixie's avatar
Hixie committed
336
final Animatable<double> _kStrokeHeadTween = new CurveTween(
337
  curve: const Interval(0.0, 0.5, curve: Curves.fastOutSlowIn),
Hixie's avatar
Hixie committed
338
).chain(new CurveTween(
339
  curve: const SawTooth(5),
Hixie's avatar
Hixie committed
340 341 342
));

final Animatable<double> _kStrokeTailTween = new CurveTween(
343
  curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn),
Hixie's avatar
Hixie committed
344
).chain(new CurveTween(
345
  curve: const SawTooth(5),
Hixie's avatar
Hixie committed
346 347 348 349
));

final Animatable<int> _kStepTween = new StepTween(begin: 0, end: 5);

350
final Animatable<double> _kRotationTween = new CurveTween(curve: const SawTooth(5));
351

352
class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin {
353
  AnimationController _controller;
354

355
  @override
356 357
  void initState() {
    super.initState();
358 359 360 361
    _controller = new AnimationController(
      duration: const Duration(milliseconds: 6666),
      vsync: this,
    )..repeat();
362 363
  }

364
  @override
365
  void dispose() {
366
    _controller.dispose();
367 368 369
    super.dispose();
  }

370 371
  Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
    return new Container(
372
      constraints: const BoxConstraints(
373
        minWidth: _kMinCircularProgressIndicatorSize,
374
        minHeight: _kMinCircularProgressIndicatorSize,
375 376 377
      ),
      child: new CustomPaint(
        painter: new _CircularProgressIndicatorPainter(
378 379 380
          valueColor: widget._getValueColor(context),
          value: widget.value, // may be null
          headValue: headValue, // remaining arguments are ignored if widget.value is not null
381 382
          tailValue: tailValue,
          stepValue: stepValue,
383
          rotationValue: rotationValue,
384 385 386
          strokeWidth: widget.strokeWidth,
        ),
      ),
387 388 389
    );
  }

390
  Widget _buildAnimation() {
391
    return new AnimatedBuilder(
392
      animation: _controller,
393
      builder: (BuildContext context, Widget child) {
394 395
        return _buildIndicator(
          context,
396 397 398
          _kStrokeHeadTween.evaluate(_controller),
          _kStrokeTailTween.evaluate(_controller),
          _kStepTween.evaluate(_controller),
399
          _kRotationTween.evaluate(_controller),
400
        );
401
      },
402 403
    );
  }
404 405 406

  @override
  Widget build(BuildContext context) {
407
    if (widget.value != null)
408 409 410
      return _buildIndicator(context, 0.0, 0.0, 0, 0.0);
    return _buildAnimation();
  }
411
}
412 413 414 415 416 417 418 419 420

class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
  _RefreshProgressIndicatorPainter({
    Color valueColor,
    double value,
    double headValue,
    double tailValue,
    int stepValue,
    double rotationValue,
421
    double strokeWidth,
422
    this.arrowheadScale,
423 424 425 426 427 428 429
  }) : super(
    valueColor: valueColor,
    value: value,
    headValue: headValue,
    tailValue: tailValue,
    stepValue: stepValue,
    rotationValue: rotationValue,
430
    strokeWidth: strokeWidth,
431 432
  );

433 434
  final double arrowheadScale;

435 436
  void paintArrowhead(Canvas canvas, Size size) {
    // ux, uy: a unit vector whose direction parallels the base of the arrowhead.
437
    // Note that ux, -uy points in the direction the arrowhead points.
438 439 440 441 442 443
    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;
444 445 446 447 448
    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;
449

450
    final Path path = new Path()
451 452
      ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
      ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
453
      ..lineTo(arrowheadPointX, arrowheadPointY)
454
      ..close();
455
    final Paint paint = new Paint()
456 457 458 459 460 461 462 463 464
      ..color = valueColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.fill;
    canvas.drawPath(path, paint);
  }

  @override
  void paint(Canvas canvas, Size size) {
    super.paint(canvas, size);
465 466
    if (arrowheadScale > 0.0)
      paintArrowhead(canvas, size);
467 468 469
  }
}

470 471 472
/// 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
473
/// a complete implementation of swipe-to-refresh driven by a [Scrollable]
474 475 476 477 478
/// widget.
///
/// See also:
///
///  * [RefreshIndicator]
479
class RefreshProgressIndicator extends CircularProgressIndicator {
480 481 482
  /// Creates a refresh progress indicator.
  ///
  /// Rather than creating a refresh progress indicator directly, consider using
Adam Barth's avatar
Adam Barth committed
483
  /// a [RefreshIndicator] together with a [Scrollable] widget.
484
  const RefreshProgressIndicator({
485 486 487
    Key key,
    double value,
    Color backgroundColor,
488 489
    Animation<Color> valueColor,
    double strokeWidth: 2.0, // Different default than CircularProgressIndicator.
490 491 492 493
  }) : super(
    key: key,
    value: value,
    backgroundColor: backgroundColor,
494 495
    valueColor: valueColor,
    strokeWidth: strokeWidth,
496
  );
497 498 499 500 501 502

  @override
  _RefreshProgressIndicatorState createState() => new _RefreshProgressIndicatorState();
}

class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
503
  static const double _kIndicatorSize = 40.0;
504

505 506 507 508 509 510
  // 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) {
511 512
    if (widget.value != null)
      _controller.value = widget.value / 10.0;
513 514 515 516 517
    else
      _controller.forward();
    return _buildAnimation();
  }

518 519
  @override
  Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
520
    final double arrowheadScale = widget.value == null ? 0.0 : (widget.value * 2.0).clamp(0.0, 1.0);
521 522 523
    return new Container(
      width: _kIndicatorSize,
      height: _kIndicatorSize,
Josh Soref's avatar
Josh Soref committed
524
      margin: const EdgeInsets.all(4.0), // accommodate the shadow
525 526
      child: new Material(
        type: MaterialType.circle,
527
        color: widget.backgroundColor ?? Theme.of(context).canvasColor,
528
        elevation: 2.0,
529 530 531 532
        child: new Padding(
          padding: const EdgeInsets.all(12.0),
          child: new CustomPaint(
            painter: new _RefreshProgressIndicatorPainter(
533
              valueColor: widget._getValueColor(context),
534 535
              value: null, // Draw the indeterminate progress indicator.
              headValue: headValue,
536 537 538
              tailValue: tailValue,
              stepValue: stepValue,
              rotationValue: rotationValue,
539 540 541 542 543 544
              strokeWidth: widget.strokeWidth,
              arrowheadScale: arrowheadScale,
            ),
          ),
        ),
      ),
545 546 547
    );
  }
}