| // Copyright 2014 The Flutter 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; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'material.dart'; |
| import 'theme.dart'; |
| |
| const double _kLinearProgressIndicatorHeight = 6.0; |
| const double _kMinCircularProgressIndicatorSize = 36.0; |
| const int _kIndeterminateLinearDuration = 1800; |
| |
| // TODO(hansmuller): implement the support for buffer indicator |
| |
| /// 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://material.io/design/components/progress-indicators.html> |
| abstract class ProgressIndicator extends StatefulWidget { |
| /// Creates a progress indicator. |
| /// |
| /// {@template flutter.material.progressIndicator.parameters} |
| /// The [value] argument can either be null for an indeterminate |
| /// progress indicator, or non-null for a determinate progress |
| /// indicator. |
| /// |
| /// ## 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} |
| const ProgressIndicator({ |
| Key key, |
| this.value, |
| this.backgroundColor, |
| this.valueColor, |
| this.semanticsLabel, |
| this.semanticsValue, |
| }) : super(key: key); |
| |
| /// 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. |
| /// |
| /// If null, this progress indicator is indeterminate, which means the |
| /// indicator displays a predetermined animation that does not indicate how |
| /// much actual progress is being made. |
| final double value; |
| |
| /// The progress indicator's background color. |
| /// |
| /// The current theme's [ThemeData.backgroundColor] by default. |
| final Color backgroundColor; |
| |
| /// The progress indicator's color as an animated value. |
| /// |
| /// To specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`. |
| /// |
| /// If null, the progress indicator is rendered with the current theme's |
| /// [ThemeData.accentColor]. |
| final Animation<Color> valueColor; |
| |
| /// {@template flutter.material.progressIndicator.semanticsLabel} |
| /// The [Semantics.label] for this progress indicator. |
| /// |
| /// 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} |
| final String semanticsLabel; |
| |
| /// {@template flutter.material.progressIndicator.semanticsValue} |
| /// The [Semantics.value] for this progress indicator. |
| /// |
| /// 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. |
| /// |
| /// For determinate progress indicators, this will be defaulted to [value] |
| /// expressed as a percentage, i.e. `0.1` will become '10%'. |
| /// {@endtemplate} |
| final String semanticsValue; |
| |
| Color _getBackgroundColor(BuildContext context) => backgroundColor ?? Theme.of(context).backgroundColor; |
| Color _getValueColor(BuildContext context) => valueColor?.value ?? Theme.of(context).accentColor; |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(PercentProperty('value', value, showName: false, ifNull: '<indeterminate>')); |
| } |
| |
| Widget _buildSemanticsWrapper({ |
| @required BuildContext context, |
| @required Widget child, |
| }) { |
| String expandedSemanticsValue = semanticsValue; |
| if (value != null) { |
| expandedSemanticsValue ??= '${(value * 100).round()}%'; |
| } |
| return Semantics( |
| label: semanticsLabel, |
| value: expandedSemanticsValue, |
| child: child, |
| ); |
| } |
| } |
| |
| class _LinearProgressIndicatorPainter extends CustomPainter { |
| 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; |
| |
| // The indeterminate progress animation displays two lines whose leading (head) |
| // and trailing (tail) endpoints are defined by the following four curves. |
| static const Curve line1Head = Interval( |
| 0.0, |
| 750.0 / _kIndeterminateLinearDuration, |
| curve: Cubic(0.2, 0.0, 0.8, 1.0), |
| ); |
| static const Curve line1Tail = Interval( |
| 333.0 / _kIndeterminateLinearDuration, |
| (333.0 + 750.0) / _kIndeterminateLinearDuration, |
| curve: Cubic(0.4, 0.0, 1.0, 1.0), |
| ); |
| static const Curve line2Head = Interval( |
| 1000.0 / _kIndeterminateLinearDuration, |
| (1000.0 + 567.0) / _kIndeterminateLinearDuration, |
| curve: Cubic(0.0, 0.0, 0.65, 1.0), |
| ); |
| static const Curve line2Tail = Interval( |
| 1267.0 / _kIndeterminateLinearDuration, |
| (1267.0 + 533.0) / _kIndeterminateLinearDuration, |
| curve: Cubic(0.10, 0.0, 0.45, 1.0), |
| ); |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| final Paint paint = Paint() |
| ..color = backgroundColor |
| ..style = PaintingStyle.fill; |
| canvas.drawRect(Offset.zero & size, paint); |
| |
| paint.color = valueColor; |
| |
| void drawBar(double x, double width) { |
| if (width <= 0.0) |
| return; |
| |
| double left; |
| switch (textDirection) { |
| case TextDirection.rtl: |
| left = size.width - width - x; |
| break; |
| case TextDirection.ltr: |
| left = x; |
| break; |
| } |
| canvas.drawRect(Offset(left, 0.0) & Size(width, size.height), paint); |
| } |
| |
| if (value != null) { |
| drawBar(0.0, value.clamp(0.0, 1.0) * size.width as double); |
| } 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); |
| } |
| } |
| |
| @override |
| bool shouldRepaint(_LinearProgressIndicatorPainter oldPainter) { |
| return oldPainter.backgroundColor != backgroundColor |
| || oldPainter.valueColor != valueColor |
| || oldPainter.value != value |
| || oldPainter.animationValue != animationValue |
| || oldPainter.textDirection != textDirection; |
| } |
| } |
| |
| /// A material design linear progress indicator, also known as a progress bar. |
| /// |
| /// 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]. |
| /// |
| /// The indicator line is displayed with [valueColor], an animated value. To |
| /// specify a constant color value use: `AlwaysStoppedAnimation<Color>(color)`. |
| /// |
| /// See also: |
| /// |
| /// * [CircularProgressIndicator], which shows progress along a circular arc. |
| /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] |
| /// when the underlying vertical scrollable is overscrolled. |
| /// * <https://material.io/design/components/progress-indicators.html#linear-progress-indicators> |
| class LinearProgressIndicator extends ProgressIndicator { |
| /// Creates a linear progress indicator. |
| /// |
| /// {@macro flutter.material.progressIndicator.parameters} |
| const LinearProgressIndicator({ |
| Key key, |
| double value, |
| Color backgroundColor, |
| Animation<Color> valueColor, |
| String semanticsLabel, |
| String semanticsValue, |
| }) : super( |
| key: key, |
| value: value, |
| backgroundColor: backgroundColor, |
| valueColor: valueColor, |
| semanticsLabel: semanticsLabel, |
| semanticsValue: semanticsValue, |
| ); |
| |
| @override |
| _LinearProgressIndicatorState createState() => _LinearProgressIndicatorState(); |
| } |
| |
| class _LinearProgressIndicatorState extends State<LinearProgressIndicator> with SingleTickerProviderStateMixin { |
| AnimationController _controller; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _controller = AnimationController( |
| duration: const Duration(milliseconds: _kIndeterminateLinearDuration), |
| vsync: this, |
| ); |
| 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(); |
| } |
| |
| @override |
| void dispose() { |
| _controller.dispose(); |
| super.dispose(); |
| } |
| |
| Widget _buildIndicator(BuildContext context, double animationValue, TextDirection textDirection) { |
| return widget._buildSemanticsWrapper( |
| context: context, |
| child: Container( |
| constraints: const BoxConstraints( |
| minWidth: double.infinity, |
| minHeight: _kLinearProgressIndicatorHeight, |
| ), |
| child: CustomPaint( |
| painter: _LinearProgressIndicatorPainter( |
| backgroundColor: widget._getBackgroundColor(context), |
| valueColor: widget._getValueColor(context), |
| value: widget.value, // may be null |
| animationValue: animationValue, // ignored if widget.value is not null |
| textDirection: textDirection, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final TextDirection textDirection = Directionality.of(context); |
| |
| if (widget.value != null) |
| return _buildIndicator(context, _controller.value, textDirection); |
| |
| return AnimatedBuilder( |
| animation: _controller.view, |
| builder: (BuildContext context, Widget child) { |
| return _buildIndicator(context, _controller.value, textDirection); |
| }, |
| ); |
| } |
| } |
| |
| class _CircularProgressIndicatorPainter extends CustomPainter { |
| _CircularProgressIndicatorPainter({ |
| this.backgroundColor, |
| this.valueColor, |
| this.value, |
| this.headValue, |
| this.tailValue, |
| this.stepValue, |
| this.rotationValue, |
| this.strokeWidth, |
| }) : arcStart = value != null |
| ? _startAngle |
| : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 1.7 - stepValue * 0.8 * math.pi, |
| arcSweep = value != null |
| ? (value.clamp(0.0, 1.0) as double) * _sweep |
| : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon); |
| |
| final Color backgroundColor; |
| final Color valueColor; |
| final double value; |
| final double headValue; |
| final double tailValue; |
| final int stepValue; |
| final double rotationValue; |
| final double strokeWidth; |
| final double arcStart; |
| final double arcSweep; |
| |
| 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; |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| final Paint paint = Paint() |
| ..color = valueColor |
| ..strokeWidth = strokeWidth |
| ..style = PaintingStyle.stroke; |
| if (backgroundColor != null) { |
| final Paint backgroundPaint = Paint() |
| ..color = backgroundColor |
| ..strokeWidth = strokeWidth |
| ..style = PaintingStyle.stroke; |
| canvas.drawArc(Offset.zero & size, 0, _sweep, false, backgroundPaint); |
| } |
| |
| if (value == null) // Indeterminate |
| paint.strokeCap = StrokeCap.square; |
| |
| canvas.drawArc(Offset.zero & size, arcStart, arcSweep, false, paint); |
| } |
| |
| @override |
| bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) { |
| return oldPainter.backgroundColor != backgroundColor |
| || oldPainter.valueColor != valueColor |
| || oldPainter.value != value |
| || oldPainter.headValue != headValue |
| || oldPainter.tailValue != tailValue |
| || oldPainter.stepValue != stepValue |
| || oldPainter.rotationValue != rotationValue |
| || oldPainter.strokeWidth != strokeWidth; |
| } |
| } |
| |
| /// A material design circular progress indicator, which spins to indicate that |
| /// the application is busy. |
| /// |
| /// 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]. |
| /// |
| /// The indicator arc is displayed with [valueColor], an animated value. To |
| /// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`. |
| /// |
| /// See also: |
| /// |
| /// * [LinearProgressIndicator], which displays progress along a line. |
| /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] |
| /// when the underlying vertical scrollable is overscrolled. |
| /// * <https://material.io/design/components/progress-indicators.html#circular-progress-indicators> |
| class CircularProgressIndicator extends ProgressIndicator { |
| /// Creates a circular progress indicator. |
| /// |
| /// {@macro flutter.material.progressIndicator.parameters} |
| const CircularProgressIndicator({ |
| Key key, |
| double value, |
| Color backgroundColor, |
| Animation<Color> valueColor, |
| this.strokeWidth = 4.0, |
| String semanticsLabel, |
| String semanticsValue, |
| }) : super( |
| key: key, |
| value: value, |
| backgroundColor: backgroundColor, |
| valueColor: valueColor, |
| semanticsLabel: semanticsLabel, |
| semanticsValue: semanticsValue, |
| ); |
| |
| /// The width of the line used to draw the circle. |
| final double strokeWidth; |
| |
| @override |
| _CircularProgressIndicatorState createState() => _CircularProgressIndicatorState(); |
| } |
| |
| // Tweens used by circular progress indicator |
| final Animatable<double> _kStrokeHeadTween = CurveTween( |
| curve: const Interval(0.0, 0.5, curve: Curves.fastOutSlowIn), |
| ).chain(CurveTween( |
| curve: const SawTooth(5), |
| )); |
| |
| final Animatable<double> _kStrokeTailTween = CurveTween( |
| curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), |
| ).chain(CurveTween( |
| curve: const SawTooth(5), |
| )); |
| |
| final Animatable<int> _kStepTween = StepTween(begin: 0, end: 5); |
| |
| final Animatable<double> _kRotationTween = CurveTween(curve: const SawTooth(5)); |
| |
| class _CircularProgressIndicatorState extends State<CircularProgressIndicator> with SingleTickerProviderStateMixin { |
| AnimationController _controller; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _controller = AnimationController( |
| duration: const Duration(seconds: 5), |
| vsync: this, |
| ); |
| 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(); |
| } |
| |
| @override |
| void dispose() { |
| _controller.dispose(); |
| super.dispose(); |
| } |
| |
| Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) { |
| return widget._buildSemanticsWrapper( |
| context: context, |
| child: Container( |
| constraints: const BoxConstraints( |
| minWidth: _kMinCircularProgressIndicatorSize, |
| minHeight: _kMinCircularProgressIndicatorSize, |
| ), |
| child: CustomPaint( |
| painter: _CircularProgressIndicatorPainter( |
| backgroundColor: widget.backgroundColor, |
| valueColor: widget._getValueColor(context), |
| value: widget.value, // may be null |
| headValue: headValue, // remaining arguments are ignored if widget.value is not null |
| tailValue: tailValue, |
| stepValue: stepValue, |
| rotationValue: rotationValue, |
| strokeWidth: widget.strokeWidth, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Widget _buildAnimation() { |
| return AnimatedBuilder( |
| animation: _controller, |
| builder: (BuildContext context, Widget child) { |
| return _buildIndicator( |
| context, |
| _kStrokeHeadTween.evaluate(_controller), |
| _kStrokeTailTween.evaluate(_controller), |
| _kStepTween.evaluate(_controller), |
| _kRotationTween.evaluate(_controller), |
| ); |
| }, |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| if (widget.value != null) |
| return _buildIndicator(context, 0.0, 0.0, 0, 0.0); |
| return _buildAnimation(); |
| } |
| } |
| |
| class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter { |
| _RefreshProgressIndicatorPainter({ |
| Color valueColor, |
| double value, |
| double headValue, |
| double tailValue, |
| int stepValue, |
| double rotationValue, |
| double strokeWidth, |
| this.arrowheadScale, |
| }) : super( |
| valueColor: valueColor, |
| value: value, |
| headValue: headValue, |
| tailValue: tailValue, |
| stepValue: stepValue, |
| rotationValue: rotationValue, |
| strokeWidth: strokeWidth, |
| ); |
| |
| final double arrowheadScale; |
| |
| void paintArrowhead(Canvas canvas, Size size) { |
| // ux, uy: a unit vector whose direction parallels the base of the arrowhead. |
| // (So ux, -uy points in the direction the arrowhead points.) |
| 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; |
| 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; |
| |
| final Path path = Path() |
| ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius) |
| ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius) |
| ..lineTo(arrowheadPointX, arrowheadPointY) |
| ..close(); |
| final Paint paint = Paint() |
| ..color = valueColor |
| ..strokeWidth = strokeWidth |
| ..style = PaintingStyle.fill; |
| canvas.drawPath(path, paint); |
| } |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| super.paint(canvas, size); |
| if (arrowheadScale > 0.0) |
| paintArrowhead(canvas, size); |
| } |
| } |
| |
| /// 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. |
| /// |
| /// The indicator arc is displayed with [valueColor], an animated value. To |
| /// specify a constant color use: `AlwaysStoppedAnimation<Color>(color)`. |
| /// |
| /// See also: |
| /// |
| /// * [RefreshIndicator], which automatically displays a [CircularProgressIndicator] |
| /// when the underlying vertical scrollable is overscrolled. |
| class RefreshProgressIndicator extends CircularProgressIndicator { |
| /// Creates a refresh progress indicator. |
| /// |
| /// Rather than creating a refresh progress indicator directly, consider using |
| /// a [RefreshIndicator] together with a [Scrollable] widget. |
| /// |
| /// {@macro flutter.material.progressIndicator.parameters} |
| const RefreshProgressIndicator({ |
| Key key, |
| double value, |
| Color backgroundColor, |
| Animation<Color> valueColor, |
| double strokeWidth = 2.0, // Different default than CircularProgressIndicator. |
| String semanticsLabel, |
| String semanticsValue, |
| }) : super( |
| key: key, |
| value: value, |
| backgroundColor: backgroundColor, |
| valueColor: valueColor, |
| strokeWidth: strokeWidth, |
| semanticsLabel: semanticsLabel, |
| semanticsValue: semanticsValue, |
| ); |
| |
| @override |
| _RefreshProgressIndicatorState createState() => _RefreshProgressIndicatorState(); |
| } |
| |
| class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { |
| static const double _indicatorSize = 40.0; |
| |
| // 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 (widget.value != null) |
| _controller.value = widget.value / 10.0; |
| else if (!_controller.isAnimating) |
| _controller.repeat(); |
| return _buildAnimation(); |
| } |
| |
| @override |
| Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) { |
| final double arrowheadScale = widget.value == null ? 0.0 : ((widget.value * 2.0).clamp(0.0, 1.0) as double); |
| 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, |
| color: widget.backgroundColor ?? Theme.of(context).canvasColor, |
| elevation: 2.0, |
| child: Padding( |
| padding: const EdgeInsets.all(12.0), |
| child: CustomPaint( |
| painter: _RefreshProgressIndicatorPainter( |
| valueColor: widget._getValueColor(context), |
| value: null, // Draw the indeterminate progress indicator. |
| headValue: headValue, |
| tailValue: tailValue, |
| stepValue: stepValue, |
| rotationValue: rotationValue, |
| strokeWidth: widget.strokeWidth, |
| arrowheadScale: arrowheadScale, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| } |