// 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; 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); } 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) * _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); 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, ), ), ), ), ), ); } }