progress_indicator.dart 16.9 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/widgets.dart';
8

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

const double _kLinearProgressIndicatorHeight = 6.0;
13 14
const double _kMinCircularProgressIndicatorSize = 36.0;
const double _kCircularProgressIndicatorStrokeWidth = 4.0;
15

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

18 19 20 21 22 23 24 25 26
/// A base class for material design progress indicators
///
/// This widget cannot be instantiated directly. For a linear progress
/// indicator, see [LinearProgressIndicator]. For a circular progress indicator,
/// see [CircularProgressIndicator].
///
/// See also:
///
///  * <https://www.google.com/design/spec/components/progress-activity.html>
27
abstract class ProgressIndicator extends StatefulWidget {
28 29 30 31 32
  /// Creates a progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
  /// progress indcator) or non-null (corresponding to a determinate progress
  /// indicator). See [value] for details.
33 34
  ProgressIndicator({
    Key key,
35 36 37
    this.value,
    this.backgroundColor,
    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
Hixie's avatar
Hixie committed
64 65
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
66 67 68 69 70
    if (value != null) {
      description.add('${(value.clamp(0.0, 1.0) * 100.0).toStringAsFixed(1)}%');
    } else {
      description.add('<indeterminate>');
    }
Hixie's avatar
Hixie committed
71
  }
72 73
}

74 75 76 77 78
class _LinearProgressIndicatorPainter extends CustomPainter {
  const _LinearProgressIndicatorPainter({
    this.backgroundColor,
    this.valueColor,
    this.value,
79
    this.animationValue
80 81 82 83 84
  });

  final Color backgroundColor;
  final Color valueColor;
  final double value;
85
  final double animationValue;
86

87
  @override
88
  void paint(Canvas canvas, Size size) {
89
    Paint paint = new Paint()
90
      ..color = backgroundColor
91
      ..style = PaintingStyle.fill;
92 93
    canvas.drawRect(Point.origin & size, paint);

94
    paint.color = valueColor;
95 96 97 98
    if (value != null) {
      double width = value.clamp(0.0, 1.0) * size.width;
      canvas.drawRect(Point.origin & new Size(width, size.height), paint);
    } else {
99
      double startX = size.width * (1.5 * animationValue - 0.5);
100 101 102 103 104 105 106
      double endX = startX + 0.5 * size.width;
      double x = startX.clamp(0.0, size.width);
      double width = endX.clamp(0.0, size.width) - x;
      canvas.drawRect(new Point(x, 0.0) & new Size(width, size.height), paint);
    }
  }

107
  @override
108 109 110 111
  bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) {
    return oldPainter.backgroundColor != backgroundColor
        || oldPainter.valueColor != valueColor
        || oldPainter.value != value
112
        || oldPainter.animationValue != animationValue;
113 114 115
  }
}

116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
/// A material design linear progress indicator.
///
/// 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]
///  * <https://www.google.com/design/spec/components/progress-activity.html#progress-activity-types-of-indicators>
134
class LinearProgressIndicator extends ProgressIndicator {
135 136 137 138 139
  /// Creates a linear progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
  /// progress indcator) or non-null (corresponding to a determinate progress
  /// indicator). See [value] for details.
140 141 142 143 144
  LinearProgressIndicator({
    Key key,
    double value
  }) : super(key: key, value: value);

145
  @override
146
  _LinearProgressIndicatorState createState() => new _LinearProgressIndicatorState();
147 148
}

149 150 151 152
class _LinearProgressIndicatorState extends State<LinearProgressIndicator> {
  Animation<double> _animation;
  AnimationController _controller;

153
  @override
154 155 156 157 158 159 160 161
  void initState() {
    super.initState();
    _controller = new AnimationController(
      duration: const Duration(milliseconds: 1500)
    )..repeat();
    _animation = new CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn);
  }

162
  @override
163
  void dispose() {
164
    _controller.dispose();
165 166 167
    super.dispose();
  }

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
  Widget _buildIndicator(BuildContext context, double animationValue) {
    return new Container(
      constraints: new BoxConstraints.tightFor(
        width: double.INFINITY,
        height: _kLinearProgressIndicatorHeight
      ),
      child: new CustomPaint(
        painter: new _LinearProgressIndicatorPainter(
          backgroundColor: config._getBackgroundColor(context),
          valueColor: config._getValueColor(context),
          value: config.value, // may be null
          animationValue: animationValue // ignored if config.value is not null
        )
      )
    );
  }

185
  @override
186 187
  Widget build(BuildContext context) {
    if (config.value != null)
188
      return _buildIndicator(context, _animation.value);
189 190 191 192

    return new AnimatedBuilder(
      animation: _animation,
      builder: (BuildContext context, Widget child) {
193
        return _buildIndicator(context, _animation.value);
194 195 196 197 198
      }
    );
  }
}

199
class _CircularProgressIndicatorPainter extends CustomPainter {
200 201
  static const double _kTwoPI = math.PI * 2.0;
  static const double _kEpsilon = .001;
202
  // Canavs.drawArc(r, 0, 2*PI) doesn't draw anything, so just get close.
203 204
  static const double _kSweep = _kTwoPI - _kEpsilon;
  static const double _kStartAngle = -math.PI / 2.0;
205

206
  _CircularProgressIndicatorPainter({
207
    this.valueColor,
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
    double value,
    double headValue,
    double tailValue,
    int stepValue,
    double rotationValue,
    this.strokeWidth
  }) : this.value = value,
       this.headValue = headValue,
       this.tailValue = tailValue,
       this.stepValue = stepValue,
       this.rotationValue = rotationValue,
       arcStart = value != null
         ? _kStartAngle
         : _kStartAngle + tailValue * 3 / 2 * math.PI + rotationValue * math.PI * 1.7 - stepValue * 0.8 * math.PI,
       arcSweep = value != null
         ? value.clamp(0.0, 1.0) * _kSweep
         : math.max(headValue * 3 / 2 * math.PI - tailValue * 3 / 2 * math.PI, _kEpsilon);
225 226 227

  final Color valueColor;
  final double value;
228 229 230 231
  final double headValue;
  final double tailValue;
  final int stepValue;
  final double rotationValue;
232 233 234
  final double strokeWidth;
  final double arcStart;
  final double arcSweep;
235

236
  @override
237
  void paint(Canvas canvas, Size size) {
238
    Paint paint = new Paint()
239
      ..color = valueColor
240
      ..strokeWidth = strokeWidth
241
      ..style = PaintingStyle.stroke;
242

243
    if (value == null) // Indeterminate
244
      paint.strokeCap = StrokeCap.square;
245

246 247 248
    Path path = new Path()
      ..arcTo(Point.origin & size, arcStart, arcSweep, false);
    canvas.drawPath(path, paint);
249 250
  }

251
  @override
252
  bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) {
253 254
    return oldPainter.valueColor != valueColor
        || oldPainter.value != value
255 256 257
        || oldPainter.headValue != headValue
        || oldPainter.tailValue != tailValue
        || oldPainter.stepValue != stepValue
258 259
        || oldPainter.rotationValue != rotationValue
        || oldPainter.strokeWidth != strokeWidth;
260 261 262
  }
}

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
/// A material design circular progress indicator.
///
/// 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]
///  * <https://www.google.com/design/spec/components/progress-activity.html#progress-activity-types-of-indicators>
281
class CircularProgressIndicator extends ProgressIndicator {
282 283 284 285 286
  /// Creates a circular progress indicator.
  ///
  /// The [value] argument can be either null (corresponding to an indeterminate
  /// progress indcator) or non-null (corresponding to a determinate progress
  /// indicator). See [value] for details.
287 288
  CircularProgressIndicator({
    Key key,
289 290 291 292
    double value,
    Color backgroundColor,
    Animation<Color> valueColor
  }) : super(key: key, value: value, backgroundColor: backgroundColor, valueColor: valueColor);
293

294
  @override
295
  _CircularProgressIndicatorState createState() => new _CircularProgressIndicatorState();
296
}
297

298
// Tweens used by circular progress indicator
Hixie's avatar
Hixie committed
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
final Animatable<double> _kStrokeHeadTween = new CurveTween(
  curve: new Interval(0.0, 0.5, curve: Curves.fastOutSlowIn)
).chain(new CurveTween(
  curve: new SawTooth(5)
));

final Animatable<double> _kStrokeTailTween = new CurveTween(
  curve: new Interval(0.5, 1.0, curve: Curves.fastOutSlowIn)
).chain(new CurveTween(
  curve: new SawTooth(5)
));

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

final Animatable<double> _kRotationTween = new CurveTween(curve: new SawTooth(5));
314 315

class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
316
  AnimationController _controller;
317

318
  @override
319 320
  void initState() {
    super.initState();
321
    _controller = _buildController();
322 323
  }

324
  @override
325
  void dispose() {
326
    _controller.dispose();
327 328 329
    super.dispose();
  }

330 331 332 333
  AnimationController _buildController() {
    return new AnimationController(duration: const Duration(milliseconds: 6666))..repeat();
  }

334 335 336 337 338 339 340 341 342 343 344 345 346
  Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
    return new Container(
      constraints: new BoxConstraints(
        minWidth: _kMinCircularProgressIndicatorSize,
        minHeight: _kMinCircularProgressIndicatorSize
      ),
      child: new CustomPaint(
        painter: new _CircularProgressIndicatorPainter(
          valueColor: config._getValueColor(context),
          value: config.value, // may be null
          headValue: headValue, // remaining arguments are ignored if config.value is not null
          tailValue: tailValue,
          stepValue: stepValue,
347 348
          rotationValue: rotationValue,
          strokeWidth: _kCircularProgressIndicatorStrokeWidth
349 350 351 352 353
        )
      )
    );
  }

354
  Widget _buildAnimation() {
355
    return new AnimatedBuilder(
356
      animation: _controller,
357
      builder: (BuildContext context, Widget child) {
358 359
        return _buildIndicator(
          context,
360 361 362 363
          _kStrokeHeadTween.evaluate(_controller),
          _kStrokeTailTween.evaluate(_controller),
          _kStepTween.evaluate(_controller),
          _kRotationTween.evaluate(_controller)
364
        );
365 366 367
      }
    );
  }
368 369 370 371 372 373 374

  @override
  Widget build(BuildContext context) {
    if (config.value != null)
      return _buildIndicator(context, 0.0, 0.0, 0, 0.0);
    return _buildAnimation();
  }
375
}
376 377 378 379 380 381 382 383 384

class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
  _RefreshProgressIndicatorPainter({
    Color valueColor,
    double value,
    double headValue,
    double tailValue,
    int stepValue,
    double rotationValue,
385 386
    double strokeWidth,
    this.arrowheadScale
387 388 389 390 391 392 393 394 395 396
  }) : super(
    valueColor: valueColor,
    value: value,
    headValue: headValue,
    tailValue: tailValue,
    stepValue: stepValue,
    rotationValue: rotationValue,
    strokeWidth: strokeWidth
  );

397 398
  final double arrowheadScale;

399 400
  void paintArrowhead(Canvas canvas, Size size) {
    // ux, uy: a unit vector whose direction parallels the base of the arrowhead.
401
    // Note that ux, -uy points in the direction the arrowhead points.
402 403 404 405 406 407
    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;
408 409 410 411 412
    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;
413 414 415 416

    Path path = new Path()
      ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
      ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
417
      ..lineTo(arrowheadPointX, arrowheadPointY)
418 419 420 421 422 423 424 425 426 427 428
      ..close();
    Paint paint = new Paint()
      ..color = valueColor
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.fill;
    canvas.drawPath(path, paint);
  }

  @override
  void paint(Canvas canvas, Size size) {
    super.paint(canvas, size);
429 430
    if (arrowheadScale > 0.0)
      paintArrowhead(canvas, size);
431 432 433
  }
}

434 435 436 437 438 439 440 441 442
/// An indicator for the progress of refreshing the contents of a widget.
///
/// Typically used for swipe-to-refresh interactions. See [RefreshIndicator] for
/// a complete implementation of swipe-to-refresh driven by a [Scrollable]
/// widget.
///
/// See also:
///
///  * [RefreshIndicator]
443
class RefreshProgressIndicator extends CircularProgressIndicator {
444 445 446 447
  /// Creates a refresh progress indicator.
  ///
  /// Rather than creating a refresh progress indicator directly, consider using
  /// a [RefreshIndicator] together with a [Scrollable] widget.
448 449 450 451 452
  RefreshProgressIndicator({
    Key key,
    double value,
    Color backgroundColor,
    Animation<Color> valueColor
453 454 455 456 457 458
  }) : super(
    key: key,
    value: value,
    backgroundColor: backgroundColor,
    valueColor: valueColor
  );
459 460 461 462 463 464 465 466

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

class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
  static double _kIndicatorSize = 40.0;

467 468 469 470 471 472 473 474 475 476 477 478 479
  // 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) {
    if (config.value != null)
      _controller.value = config.value / 10.0;
    else
      _controller.forward();
    return _buildAnimation();
  }

480 481
  @override
  Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
482
    final double arrowheadScale = config.value == null ? 0.0 : (config.value * 2.0).clamp(0.0, 1.0);
483 484 485 486 487 488
    return new Container(
      width: _kIndicatorSize,
      height: _kIndicatorSize,
      margin: const EdgeInsets.all(4.0), // acommodate the shadow
      child: new Material(
        type: MaterialType.circle,
489
        color: config.backgroundColor ?? Theme.of(context).canvasColor,
490 491 492 493 494 495
        elevation: 2,
        child: new Padding(
          padding: const EdgeInsets.all(12.0),
          child: new CustomPaint(
            painter: new _RefreshProgressIndicatorPainter(
              valueColor: config._getValueColor(context),
496 497
              value: null, // Draw the indeterminate progress indicator.
              headValue: headValue,
498 499 500
              tailValue: tailValue,
              stepValue: stepValue,
              rotationValue: rotationValue,
501 502
              strokeWidth: 2.0,
              arrowheadScale: arrowheadScale
503 504 505 506 507 508 509
            )
          )
        )
      )
    );
  }
}