From 917eaab62f6e093ae37d8efd8bdb3795f5bbc311 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 14:38:43 +0200 Subject: [PATCH 1/9] Add custom YaruProgressIndicator --- lib/src/yaru_linear_progress_indicator.dart | 91 ---- lib/src/yaru_progress_indicator.dart | 552 ++++++++++++++++++++ lib/yaru_widgets.dart | 2 +- 3 files changed, 553 insertions(+), 92 deletions(-) delete mode 100644 lib/src/yaru_linear_progress_indicator.dart create mode 100644 lib/src/yaru_progress_indicator.dart diff --git a/lib/src/yaru_linear_progress_indicator.dart b/lib/src/yaru_linear_progress_indicator.dart deleted file mode 100644 index dc9347481..000000000 --- a/lib/src/yaru_linear_progress_indicator.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; - -class YaruLinearProgressIndicator extends StatelessWidget { - /// Creates a Yaru linear progress indicator. - /// - /// The [value] argument can either be null for an indeterminate - /// progress indicator, or a non-null value between 0.0 and 1.0 for a - /// determinate progress indicator. - /// - /// 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. - const YaruLinearProgressIndicator( - {Key? key, - required this.value, - this.color, - this.valueColor, - this.semanticsLabel, - this.semanticsValue}) - : assert(value == null || (value >= 0 && value <= 1)), - 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. - /// The value have to be clamped in the range 0.0-1.0. - /// - /// 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 color. - /// - /// This is only used if [ProgressIndicator.valueColor] is null. - /// If [ProgressIndicator.color] is also null, then the ambient - /// [ProgressIndicatorThemeData.color] will be used. If that - /// is null then the current theme's [ColorScheme.primary] will - /// be used by default. - final Color? color; - - /// The progress indicator's color as an animated value. - /// - /// If null, the progress indicator is rendered with [color]. If that is null, - /// then it will use the ambient [ProgressIndicatorThemeData.color]. If that - /// is also null then it defaults to the current theme's [ColorScheme.primary]. - final Animation? valueColor; - - /// The [SemanticsProperties.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. - final String? semanticsLabel; - - /// The [SemanticsProperties.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 - /// [ProgressIndicator.value] expressed as a percentage, i.e. `0.1` will - /// become '10%'. - final String? semanticsValue; - - Color _getCurrentColor(BuildContext context) { - return valueColor?.value ?? - color ?? - ProgressIndicatorTheme.of(context).color ?? - Theme.of(context).colorScheme.primary; - } - - @override - Widget build(BuildContext context) { - return ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(3)), // half of progress height - child: LinearProgressIndicator( - value: value, - color: color, - valueColor: valueColor, - backgroundColor: _getCurrentColor(context).withAlpha(50), - minHeight: 6, - semanticsLabel: semanticsLabel, - semanticsValue: semanticsValue, - ), - ); - } -} diff --git a/lib/src/yaru_progress_indicator.dart b/lib/src/yaru_progress_indicator.dart new file mode 100644 index 000000000..75fa55642 --- /dev/null +++ b/lib/src/yaru_progress_indicator.dart @@ -0,0 +1,552 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +const double _kMinCircularProgressIndicatorSize = 36.0; +const double _kDefaultCircularProgressStrokeWidth = 6; +const double _kDefaultLinearProgressMinHeight = 6; +const int _kIndeterminateAnimationDuration = 8000; +const Curve _kIndeterminateAnimationCurve = + Cubic(.35, .75, .65, .25); // Kind of `Curves.slowMiddle` curve + +abstract class _YaruProgressIndicator extends StatefulWidget { + /// Creates a Yaru progress indicator. + /// + /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} + const _YaruProgressIndicator({ + Key? key, + this.value, + this.backgroundColor, + this.color, + 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. + /// The value will be clamped to be in the range 0.0-1.0. + /// + /// 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. + /// + /// It is up to the subclass to implement this in whatever way makes sense + /// for the given use case. See the subclass documentation for details. + final Color? backgroundColor; + + /// {@macro flutter.progress_indicator.ProgressIndicator.color} + final Color? color; + + /// The progress indicator's color as an animated value. + /// + /// If null, the progress indicator is rendered with [color]. If that is null, + /// then it will use the ambient [ProgressIndicatorThemeData.color]. If that + /// is also null then it defaults to the current theme's [ColorScheme.primary]. + final Animation? valueColor; + + /// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel} + final String? semanticsLabel; + + /// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue} + final String? semanticsValue; + + Color _getValueColor(BuildContext context) { + return valueColor?.value ?? + color ?? + ProgressIndicatorTheme.of(context).color ?? + Theme.of(context).colorScheme.primary; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(PercentProperty('value', value, + showName: false, ifNull: '')); + } + + 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 YaruLinearProgressIndicator extends _YaruProgressIndicator { + /// Creates a Yaru circular progress indicator. + /// + /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} + const YaruLinearProgressIndicator({ + Key? key, + double? value, + this.minHeight = _kDefaultLinearProgressMinHeight, + Color? color, + Animation? valueColor, + String? semanticsLabel, + String? semanticsValue, + }) : assert(minHeight > 0), + super( + key: key, + value: value, + color: color, + valueColor: valueColor, + semanticsLabel: semanticsLabel, + semanticsValue: semanticsValue); + + /// {@macro flutter.material.LinearProgressIndicator.minHeight} + final double minHeight; + + @override + _YaruLinearProgressIndicatorState createState() => + _YaruLinearProgressIndicatorState(); +} + +class _YaruLinearProgressIndicatorState + extends State + with SingleTickerProviderStateMixin { + static final Animatable _positionTween = Tween(begin: 0.0, end: 6.0) + .chain(CurveTween(curve: _kIndeterminateAnimationCurve)); + static final Animatable _speedTween = Tween(begin: -1.0, end: 1.0) + .chain(CurveTween(curve: _kIndeterminateAnimationCurve)); + + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: + const Duration(milliseconds: _kIndeterminateAnimationDuration), + vsync: this); + + if (widget.value == null) { + _controller.repeat(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(YaruLinearProgressIndicator oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value == null && !_controller.isAnimating) { + _controller.repeat(); + } else if (widget.value != null && _controller.isAnimating) { + _controller.stop(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.value != null) { + return _buildContainer( + context, + CustomPaint( + painter: _DeterminateYaruLinearProgressIndicatorPainter( + widget.value!, + widget._getValueColor(context), + Directionality.of(context)))); + } + + return AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget? child) { + return _buildContainer( + context, + ClipRRect( + borderRadius: + BorderRadius.all(Radius.circular(widget.minHeight)), + child: CustomPaint( + painter: _IndeterminateYaruLinearProgressIndicatorPainter( + widget._getValueColor(context), + _positionTween.evaluate(_controller), + _speedTween.evaluate(_controller).abs(), + Directionality.of(context)), + )), + ); + }); + } + + Widget _buildContainer(BuildContext context, Widget child) { + return widget._buildSemanticsWrapper( + context: context, + child: Container( + constraints: BoxConstraints( + minWidth: double.infinity, + minHeight: widget.minHeight, + ), + child: child)); + } +} + +class _IndeterminateYaruLinearProgressIndicatorPainter extends CustomPainter { + const _IndeterminateYaruLinearProgressIndicatorPainter( + this.color, this.position, this.speed, this.textDirection) + : super(); + + final Color color; + final double position; + final double speed; + final TextDirection textDirection; + + @override + void paint(Canvas canvas, Size size) { + final realPosition = position - position.truncate(); + final strokeWidth = size.width / 6; + final y = size.height / 2; + + final fillPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + if (textDirection == TextDirection.rtl) { + canvas.scale(-1, 1); + canvas.translate(-size.width, 0); + } + + for (var i = 0; i < 3; i++) { + final x1 = size.width * realPosition + + (strokeWidth * i / 2 + (strokeWidth * i * speed)); + final x2 = size.width * realPosition + + strokeWidth + + (strokeWidth * i / 2 + (strokeWidth * i * speed)); + + canvas.drawLine(Offset(x1, y), Offset(x2, y), + _getGradientPaint(color, x1, strokeWidth, size.height, speed)); + canvas.drawCircle(Offset(x2, y), size.height / 2, fillPaint); + + // If a line overflow on the right, redraw it on the left + if (x2 > size.width) { + canvas.drawLine( + Offset(x1 - size.width, y), + Offset(x2 - size.width, y), + _getGradientPaint( + color, x1 - size.width, strokeWidth, size.height, speed)); + canvas.drawCircle( + Offset(x2 - size.width, y), size.height / 2, fillPaint); + } + } + } + + Paint _getGradientPaint( + Color color, double x, double width, double height, double speed) { + final gradient = LinearGradient(colors: [ + color.withAlpha(0), + color.withAlpha(20 + (230 * speed).toInt()), + color.withAlpha(150 + (100 * speed).toInt()) + ], stops: const [ + 0.15, + 0.75, + 1 + ]); + + return Paint() + ..shader = gradient.createShader(Rect.fromLTWH(x, 0.0, width, height)) + ..strokeWidth = height + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + } + + @override + bool shouldRepaint( + _IndeterminateYaruLinearProgressIndicatorPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.position != position || + oldDelegate.speed != speed || + oldDelegate.textDirection != textDirection; + } +} + +class _DeterminateYaruLinearProgressIndicatorPainter extends CustomPainter { + const _DeterminateYaruLinearProgressIndicatorPainter( + this.value, this.color, this.textDirection) + : super(); + + final double value; + final Color color; + final TextDirection textDirection; + + @override + void paint(Canvas canvas, Size size) { + final backgroundHeight = size.height > 2 ? size.height - 2 : size.height; + final backgroundPaint = Paint() + ..color = color.withOpacity(.25) + ..strokeWidth = backgroundHeight + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + final strokePaint = Paint() + ..color = color + ..strokeWidth = size.height + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + + if (textDirection == TextDirection.rtl) { + canvas.scale(-1, 1); + canvas.translate(-size.width, 0); + } + + canvas.drawLine( + Offset(backgroundHeight / 2, size.height / 2), + Offset(size.width - backgroundHeight / 2, size.height / 2), + backgroundPaint); + canvas.drawLine( + Offset(size.height / 2, size.height / 2), + Offset((size.width - size.height / 2) * value, size.height / 2), + strokePaint); + } + + @override + bool shouldRepaint( + _DeterminateYaruLinearProgressIndicatorPainter oldDelegate) { + return oldDelegate.value != value || + oldDelegate.color != color || + oldDelegate.textDirection != textDirection; + } +} + +class YaruCircularProgressIndicator extends _YaruProgressIndicator { + /// Creates a Yaru circular progress indicator. + /// + /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} + const YaruCircularProgressIndicator({ + Key? key, + double? value, + this.strokeWidth = _kDefaultCircularProgressStrokeWidth, + Color? color, + Animation? valueColor, + String? semanticsLabel, + String? semanticsValue, + }) : super( + key: key, + value: value, + color: color, + valueColor: valueColor, + semanticsLabel: semanticsLabel, + semanticsValue: semanticsValue); + + /// The width of the line used to draw the circle. + final double strokeWidth; + + @override + _YaruCircularProgressIndicatorState createState() => + _YaruCircularProgressIndicatorState(); +} + +class _YaruCircularProgressIndicatorState + extends State + with SingleTickerProviderStateMixin { + static final Animatable _rotationTween = + Tween(begin: 0.0, end: math.pi * 12) + .chain(CurveTween(curve: _kIndeterminateAnimationCurve)); + static final Animatable _speedTween = Tween(begin: -1.0, end: 1.0) + .chain(CurveTween(curve: _kIndeterminateAnimationCurve)); + + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: + const Duration(milliseconds: _kIndeterminateAnimationDuration), + vsync: this); + + if (widget.value == null) { + _controller.repeat(); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(YaruCircularProgressIndicator oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value == null && !_controller.isAnimating) { + _controller.repeat(); + } else if (widget.value != null && _controller.isAnimating) { + _controller.stop(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.value != null) { + return _buildContainer( + context, + CustomPaint( + painter: _DeterminateYaruCircularProgressIndicatorPainter( + widget.value!, + widget._getValueColor(context), + widget.strokeWidth, + Directionality.of(context)))); + } + return AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget? child) { + return _buildContainer( + context, + CustomPaint( + painter: _IndeterminateYaruCircularProgressIndicatorPainter( + widget._getValueColor(context), + widget.strokeWidth, + _rotationTween.evaluate(_controller), + _speedTween.evaluate(_controller).abs(), + Directionality.of(context)), + )); + }); + } + + Widget _buildContainer(BuildContext context, Widget child) { + return widget._buildSemanticsWrapper( + context: context, + child: Container( + constraints: const BoxConstraints( + minWidth: _kMinCircularProgressIndicatorSize, + minHeight: _kMinCircularProgressIndicatorSize, + ), + child: child)); + } +} + +class _IndeterminateYaruCircularProgressIndicatorPainter extends CustomPainter { + const _IndeterminateYaruCircularProgressIndicatorPainter(this.color, + this.strokeWidth, this.rotationAngle, this.speed, this.textDirection) + : super(); + + final Color color; + final double strokeWidth; + final double rotationAngle; + final double speed; + final TextDirection textDirection; + + @override + void paint(Canvas canvas, Size size) { + const circleThird = math.pi * 2 / 3; + + final gradient = SweepGradient( + startAngle: 0.0 + rotationAngle, + endAngle: circleThird + rotationAngle, + tileMode: TileMode.repeated, + colors: [ + color.withAlpha(0), + color.withAlpha(20 + (230 * speed).toInt()), + color.withAlpha(250) + ], + stops: const [ + 0.15, + 0.75, + 1 + ]); + + final center = Offset(size.width / 2, size.height / 2); + final radius = + math.min(size.width / 2, size.height / 2) - (strokeWidth / 2); + const sweepAngle = circleThird; + final rect = Rect.fromCircle(center: center, radius: radius); + + final gradientPaint = Paint() + ..shader = gradient.createShader(rect) + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth; + final fillPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + if (textDirection == TextDirection.rtl) { + canvas.scale(-1, 1); + canvas.translate(-size.width, 0); + } + + for (var i = 0; i < 3; i++) { + final startAngle = rotationAngle + circleThird * i; + canvas.drawArc(rect, startAngle, sweepAngle, false, gradientPaint); + } + + // Draw circles after arcs, so they look on top + for (var i = 0; i < 3; i++) { + final startAngle = rotationAngle + circleThird * i; + canvas.drawCircle( + Offset(math.cos(startAngle) * radius + center.dx, + math.sin(startAngle) * radius + center.dy), + strokeWidth / 2, + fillPaint); + } + } + + @override + bool shouldRepaint( + _IndeterminateYaruCircularProgressIndicatorPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth || + oldDelegate.rotationAngle != rotationAngle || + oldDelegate.speed != speed || + oldDelegate.textDirection != textDirection; + } +} + +class _DeterminateYaruCircularProgressIndicatorPainter extends CustomPainter { + const _DeterminateYaruCircularProgressIndicatorPainter( + this.value, this.color, this.width, this.textDirection) + : super(); + + final double value; + final Color color; + final double width; + final TextDirection textDirection; + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = math.min(size.width / 2, size.height / 2) - (width / 2); + const startAngle = -math.pi / 2; + final sweepAngle = math.pi * 2 * value; + final rect = Rect.fromCircle(center: center, radius: radius); + + if (textDirection == TextDirection.rtl) { + canvas.scale(-1, 1); + canvas.translate(-size.width, 0); + } + + final backgroundPaint = Paint() + ..color = color.withOpacity(.25) + ..strokeWidth = width > 2 ? width - 2 : width + ..style = PaintingStyle.stroke; + final strokePaint = Paint() + ..color = color + ..strokeWidth = width + ..style = PaintingStyle.stroke; + + canvas.drawArc(rect, 0, math.pi * 2, false, backgroundPaint); + canvas.drawArc(rect, startAngle, sweepAngle, false, strokePaint); + } + + @override + bool shouldRepaint( + _DeterminateYaruCircularProgressIndicatorPainter oldDelegate) { + return oldDelegate.value != value || + oldDelegate.color != color || + oldDelegate.width != width || + oldDelegate.textDirection != textDirection; + } +} diff --git a/lib/yaru_widgets.dart b/lib/yaru_widgets.dart index 41a14761c..553ddd522 100644 --- a/lib/yaru_widgets.dart +++ b/lib/yaru_widgets.dart @@ -9,7 +9,7 @@ export 'src/yaru_color_picker_button.dart'; export 'src/yaru_dialog_title.dart'; export 'src/yaru_expansion_panel_list.dart'; export 'src/yaru_extra_option_row.dart'; -export 'src/yaru_linear_progress_indicator.dart'; +export 'src/yaru_progress_indicator.dart'; export 'src/yaru_master_detail_page.dart'; export 'src/yaru_narrow_layout.dart'; export 'src/yaru_option_button.dart'; From a74085cc5e06af3b2eb03d94fe546aad5ba0e1b3 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 14:38:52 +0200 Subject: [PATCH 2/9] Adapt tests --- ...yaru_circular_progress_indicator_test.dart | 37 +++++++++++++++++++ test/yaru_linear_progress_indicator_test.dart | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 test/yaru_circular_progress_indicator_test.dart diff --git a/test/yaru_circular_progress_indicator_test.dart b/test/yaru_circular_progress_indicator_test.dart new file mode 100644 index 000000000..1ca934bcf --- /dev/null +++ b/test/yaru_circular_progress_indicator_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:yaru_widgets/src/yaru_progress_indicator.dart'; + +void main() { + testWidgets('- YaruCircularProgressIndicator Test', + (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold( + body: YaruCircularProgressIndicator( + value: 0.5, + color: Colors.redAccent, + semanticsLabel: "Semantic Label", + semanticsValue: "Semantic Value", + ), + ), + )); + + final linearProgressIndicatorFinder = find.byWidgetPredicate( + (widget) => widget is CircularProgressIndicator && widget.value == 0.5, + ); + final semanticValueFinder = find.byWidgetPredicate( + (widget) => + widget is CircularProgressIndicator && + widget.semanticsValue == "Semantic Value", + ); + final semanticLabelFinder = find.byWidgetPredicate( + (widget) => + widget is CircularProgressIndicator && + widget.semanticsLabel == "Semantic Label", + ); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(linearProgressIndicatorFinder, findsOneWidget); + expect(semanticLabelFinder, findsOneWidget); + expect(semanticValueFinder, findsOneWidget); + }); +} diff --git a/test/yaru_linear_progress_indicator_test.dart b/test/yaru_linear_progress_indicator_test.dart index befe08fd1..e6a2e59e1 100644 --- a/test/yaru_linear_progress_indicator_test.dart +++ b/test/yaru_linear_progress_indicator_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:yaru_widgets/src/yaru_linear_progress_indicator.dart'; +import 'package:yaru_widgets/src/yaru_progress_indicator.dart'; void main() { testWidgets('- YaruLinearProgressIndicator Test', From 321756c25b0ba508d6db472711d9e912f0f56545 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 14:39:01 +0200 Subject: [PATCH 3/9] Adapt example --- example/lib/example_page_items.dart | 25 +++++++++++++++++++++---- example/lib/widgets/dummy_section.dart | 2 +- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/example/lib/example_page_items.dart b/example/lib/example_page_items.dart index ec1a63a5d..f7a879c69 100644 --- a/example/lib/example_page_items.dart +++ b/example/lib/example_page_items.dart @@ -27,13 +27,30 @@ final examplePageItems = [ builder: (_) => ExtraOptionRowPage(), ), YaruPageItem( - titleBuilder: (context) => Text('YaruLinearProgressIndicator'), + titleBuilder: (context) => Text('YaruProgressIndicator'), iconData: YaruIcons.download, builder: (_) => YaruPage( children: [ - YaruLinearProgressIndicator( - value: 50 / 100, - ) + Padding( + padding: const EdgeInsets.only(top: 25), + child: YaruCircularProgressIndicator(), + ), + Padding( + padding: const EdgeInsets.only(top: 25), + child: YaruCircularProgressIndicator( + value: .75, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 25), + child: YaruLinearProgressIndicator(), + ), + Padding( + padding: const EdgeInsets.only(top: 25), + child: YaruLinearProgressIndicator( + value: .75, + ), + ), ], ), ), diff --git a/example/lib/widgets/dummy_section.dart b/example/lib/widgets/dummy_section.dart index 6e70c34f8..5ae148214 100644 --- a/example/lib/widgets/dummy_section.dart +++ b/example/lib/widgets/dummy_section.dart @@ -11,7 +11,7 @@ class DummySection extends StatelessWidget { return YaruSection( headline: 'Headline', headerWidget: SizedBox( - child: CircularProgressIndicator(), + child: YaruCircularProgressIndicator(strokeWidth: 3), height: 20, width: 20, ), From bd860c443768e468e228b0e841c73ddf3b140736 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 14:44:48 +0200 Subject: [PATCH 4/9] Remove unused backgroundColor property --- lib/src/yaru_progress_indicator.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/src/yaru_progress_indicator.dart b/lib/src/yaru_progress_indicator.dart index 75fa55642..696aa6287 100644 --- a/lib/src/yaru_progress_indicator.dart +++ b/lib/src/yaru_progress_indicator.dart @@ -16,7 +16,6 @@ abstract class _YaruProgressIndicator extends StatefulWidget { const _YaruProgressIndicator({ Key? key, this.value, - this.backgroundColor, this.color, this.valueColor, this.semanticsLabel, @@ -33,12 +32,6 @@ abstract class _YaruProgressIndicator extends StatefulWidget { /// much actual progress is being made. final double? value; - /// The progress indicator's background color. - /// - /// It is up to the subclass to implement this in whatever way makes sense - /// for the given use case. See the subclass documentation for details. - final Color? backgroundColor; - /// {@macro flutter.progress_indicator.ProgressIndicator.color} final Color? color; From 11a03dfb83248244433912b2c18ab47db5c7c4e7 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 14:48:44 +0200 Subject: [PATCH 5/9] Fix tests --- test/yaru_circular_progress_indicator_test.dart | 7 ++++--- test/yaru_linear_progress_indicator_test.dart | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/yaru_circular_progress_indicator_test.dart b/test/yaru_circular_progress_indicator_test.dart index 1ca934bcf..0c320dd7c 100644 --- a/test/yaru_circular_progress_indicator_test.dart +++ b/test/yaru_circular_progress_indicator_test.dart @@ -17,16 +17,17 @@ void main() { )); final linearProgressIndicatorFinder = find.byWidgetPredicate( - (widget) => widget is CircularProgressIndicator && widget.value == 0.5, + (widget) => + widget is YaruCircularProgressIndicator && widget.value == 0.5, ); final semanticValueFinder = find.byWidgetPredicate( (widget) => - widget is CircularProgressIndicator && + widget is YaruCircularProgressIndicator && widget.semanticsValue == "Semantic Value", ); final semanticLabelFinder = find.byWidgetPredicate( (widget) => - widget is CircularProgressIndicator && + widget is YaruCircularProgressIndicator && widget.semanticsLabel == "Semantic Label", ); expect(find.byType(CircularProgressIndicator), findsOneWidget); diff --git a/test/yaru_linear_progress_indicator_test.dart b/test/yaru_linear_progress_indicator_test.dart index e6a2e59e1..9ceaa18ca 100644 --- a/test/yaru_linear_progress_indicator_test.dart +++ b/test/yaru_linear_progress_indicator_test.dart @@ -17,16 +17,16 @@ void main() { )); final linearProgressIndicatorFinder = find.byWidgetPredicate( - (widget) => widget is LinearProgressIndicator && widget.value == 0.5, + (widget) => widget is YaruLinearProgressIndicator && widget.value == 0.5, ); final semanticValueFinder = find.byWidgetPredicate( (widget) => - widget is LinearProgressIndicator && + widget is YaruLinearProgressIndicator && widget.semanticsValue == "Semantic Value", ); final semanticLabelFinder = find.byWidgetPredicate( (widget) => - widget is LinearProgressIndicator && + widget is YaruLinearProgressIndicator && widget.semanticsLabel == "Semantic Label", ); expect(find.byType(LinearProgressIndicator), findsOneWidget); From 3f5dda65f5836e8a9922eae529df73d72d41a88e Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 20:20:13 +0200 Subject: [PATCH 6/9] Fix comment (circular -> linear) --- lib/src/yaru_progress_indicator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/yaru_progress_indicator.dart b/lib/src/yaru_progress_indicator.dart index 696aa6287..98e59c374 100644 --- a/lib/src/yaru_progress_indicator.dart +++ b/lib/src/yaru_progress_indicator.dart @@ -79,7 +79,7 @@ abstract class _YaruProgressIndicator extends StatefulWidget { } class YaruLinearProgressIndicator extends _YaruProgressIndicator { - /// Creates a Yaru circular progress indicator. + /// Creates a Yaru linear progress indicator. /// /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} const YaruLinearProgressIndicator({ From ca24705ab5d66bfc55781f0de99e8692e3c60d2c Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 20:57:50 +0200 Subject: [PATCH 7/9] Fix test --- test/yaru_circular_progress_indicator_test.dart | 2 +- test/yaru_linear_progress_indicator_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/yaru_circular_progress_indicator_test.dart b/test/yaru_circular_progress_indicator_test.dart index 0c320dd7c..7d1cdb62d 100644 --- a/test/yaru_circular_progress_indicator_test.dart +++ b/test/yaru_circular_progress_indicator_test.dart @@ -30,7 +30,7 @@ void main() { widget is YaruCircularProgressIndicator && widget.semanticsLabel == "Semantic Label", ); - expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(find.byType(YaruCircularProgressIndicator), findsOneWidget); expect(linearProgressIndicatorFinder, findsOneWidget); expect(semanticLabelFinder, findsOneWidget); expect(semanticValueFinder, findsOneWidget); diff --git a/test/yaru_linear_progress_indicator_test.dart b/test/yaru_linear_progress_indicator_test.dart index 9ceaa18ca..820c8c88c 100644 --- a/test/yaru_linear_progress_indicator_test.dart +++ b/test/yaru_linear_progress_indicator_test.dart @@ -29,7 +29,7 @@ void main() { widget is YaruLinearProgressIndicator && widget.semanticsLabel == "Semantic Label", ); - expect(find.byType(LinearProgressIndicator), findsOneWidget); + expect(find.byType(YaruLinearProgressIndicator), findsOneWidget); expect(linearProgressIndicatorFinder, findsOneWidget); expect(semanticLabelFinder, findsOneWidget); expect(semanticValueFinder, findsOneWidget); From 9f7c9a0f2b2c2e8ad58e6a4eaebe4696d72ddcc6 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 22:50:31 +0200 Subject: [PATCH 8/9] Fix comments - remove @macro --- lib/src/yaru_progress_indicator.dart | 43 +++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/src/yaru_progress_indicator.dart b/lib/src/yaru_progress_indicator.dart index 98e59c374..f31691750 100644 --- a/lib/src/yaru_progress_indicator.dart +++ b/lib/src/yaru_progress_indicator.dart @@ -12,7 +12,17 @@ const Curve _kIndeterminateAnimationCurve = abstract class _YaruProgressIndicator extends StatefulWidget { /// Creates a Yaru progress indicator. /// - /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} + /// {@template yaru.widget.YaruProgressIndicator.YaruProgressIndicator} + /// The [value] argument can either be null for an indeterminate + /// progress indicator, or a non-null value between 0.0 and 1.0 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 _YaruProgressIndicator({ Key? key, this.value, @@ -32,7 +42,13 @@ abstract class _YaruProgressIndicator extends StatefulWidget { /// much actual progress is being made. final double? value; - /// {@macro flutter.progress_indicator.ProgressIndicator.color} + /// The progress indicator's color. + /// + /// This is only used if [ProgressIndicator.valueColor] is null. + /// If [ProgressIndicator.color] is also null, then the ambient + /// [ProgressIndicatorThemeData.color] will be used. If that + /// is null then the current theme's [ColorScheme.primary] will + /// be used by default. final Color? color; /// The progress indicator's color as an animated value. @@ -42,10 +58,23 @@ abstract class _YaruProgressIndicator extends StatefulWidget { /// is also null then it defaults to the current theme's [ColorScheme.primary]. final Animation? valueColor; - /// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel} + /// The [SemanticsProperties.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. final String? semanticsLabel; - /// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue} + /// The [SemanticsProperties.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 + /// [ProgressIndicator.value] expressed as a percentage, i.e. `0.1` will + /// become '10%'. final String? semanticsValue; Color _getValueColor(BuildContext context) { @@ -81,7 +110,7 @@ abstract class _YaruProgressIndicator extends StatefulWidget { class YaruLinearProgressIndicator extends _YaruProgressIndicator { /// Creates a Yaru linear progress indicator. /// - /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} + /// {@macro yaru.widget.YaruProgressIndicator.YaruProgressIndicator} const YaruLinearProgressIndicator({ Key? key, double? value, @@ -99,7 +128,7 @@ class YaruLinearProgressIndicator extends _YaruProgressIndicator { semanticsLabel: semanticsLabel, semanticsValue: semanticsValue); - /// {@macro flutter.material.LinearProgressIndicator.minHeight} + /// The minimum height of the line used to draw the linear indicator (default: 6). final double minHeight; @override @@ -317,7 +346,7 @@ class _DeterminateYaruLinearProgressIndicatorPainter extends CustomPainter { class YaruCircularProgressIndicator extends _YaruProgressIndicator { /// Creates a Yaru circular progress indicator. /// - /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} + /// {@macro yaru.widget.YaruProgressIndicator.YaruProgressIndicator} const YaruCircularProgressIndicator({ Key? key, double? value, From 3917e8ef4e696ce01a4e68b78957e6fc778da537 Mon Sep 17 00:00:00 2001 From: Paul Kepinski Date: Wed, 11 May 2022 22:52:14 +0200 Subject: [PATCH 9/9] Fix out of bound values drawing --- lib/src/yaru_progress_indicator.dart | 41 +++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/src/yaru_progress_indicator.dart b/lib/src/yaru_progress_indicator.dart index f31691750..8e99cb5ea 100644 --- a/lib/src/yaru_progress_indicator.dart +++ b/lib/src/yaru_progress_indicator.dart @@ -307,7 +307,16 @@ class _DeterminateYaruLinearProgressIndicatorPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final backgroundHeight = size.height > 2 ? size.height - 2 : size.height; + final revisedValue = value >= 0 && value <= 1 + ? value + : value < 0 + ? 0 + : 1; + final candidateBackgroundHeight = (size.height / 3 * 2).truncate(); + final backgroundHeight = + (candidateBackgroundHeight + (candidateBackgroundHeight.isEven ? 0 : 1)) + .toDouble(); + final backgroundPaint = Paint() ..color = color.withOpacity(.25) ..strokeWidth = backgroundHeight @@ -324,14 +333,21 @@ class _DeterminateYaruLinearProgressIndicatorPainter extends CustomPainter { canvas.translate(-size.width, 0); } - canvas.drawLine( - Offset(backgroundHeight / 2, size.height / 2), - Offset(size.width - backgroundHeight / 2, size.height / 2), - backgroundPaint); - canvas.drawLine( - Offset(size.height / 2, size.height / 2), - Offset((size.width - size.height / 2) * value, size.height / 2), - strokePaint); + final y = size.height / 2; + + canvas.drawLine(Offset(backgroundHeight / 2, y), + Offset(size.width - backgroundHeight / 2, y), backgroundPaint); + + if (size.width * revisedValue > size.height) { + canvas.drawLine(Offset(y, y), Offset((size.width - y) * revisedValue, y), + strokePaint); + } else if (revisedValue > 0) { + final fillPaint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + canvas.drawCircle(Offset(y, y), y, fillPaint); + } } @override @@ -539,10 +555,15 @@ class _DeterminateYaruCircularProgressIndicatorPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + final revisedValue = value >= 0 && value <= 1 + ? value + : value < 0 + ? 0 + : 1; final center = Offset(size.width / 2, size.height / 2); final radius = math.min(size.width / 2, size.height / 2) - (width / 2); const startAngle = -math.pi / 2; - final sweepAngle = math.pi * 2 * value; + final sweepAngle = math.pi * 2 * revisedValue; final rect = Rect.fromCircle(center: center, radius: radius); if (textDirection == TextDirection.rtl) {