From 1686fa7eb4eaeca0955719668e8316a3b0a3a3af Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Wed, 20 Nov 2024 01:23:26 +0200 Subject: [PATCH] Update Material 3 `CircularProgressIndicator` for new visual style (#158104) Related [Update both `ProgressIndicator` for Material 3 redesign](https://github.com/flutter/flutter/issues/141340) Fixes [Issue: Cannot theme progress indicators, many properties missing](https://github.com/flutter/flutter/issues/131690) Fixes [Cannot override default `CircularProgressIndicator` size](https://github.com/flutter/flutter/issues/158106) ### Code sample
expand to view the code sample ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( // progressIndicatorTheme: const ProgressIndicatorThemeData( // constraints: BoxConstraints.tightFor(width: 100, height: 100), // strokeWidth: 12 // ), ), home: Scaffold( appBar: AppBar(title: const Text('CircularProgressIndicator')), body: const Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ CircularProgressIndicator(year2023: false, value: 0.5), CircularProgressIndicator(year2023: false), ], ), ), ), ); } } ```
### Preview Screenshot 2024-11-04 at 16 01 57 ### New custom `CircularProgressIndicator.constraints` and stroke width Screenshot 2024-11-04 at 16 02 40 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/progress_indicator_template.dart | 20 +- .../lib/src/material/progress_indicator.dart | 272 ++++++++++++++---- .../material/progress_indicator_theme.dart | 45 ++- .../material/progress_indicator_test.dart | 166 ++++++++++- .../progress_indicator_theme_test.dart | 164 ++++++++++- 5 files changed, 607 insertions(+), 60 deletions(-) diff --git a/dev/tools/gen_defaults/lib/progress_indicator_template.dart b/dev/tools/gen_defaults/lib/progress_indicator_template.dart index 82e1a8b13b..d7a1effaef 100644 --- a/dev/tools/gen_defaults/lib/progress_indicator_template.dart +++ b/dev/tools/gen_defaults/lib/progress_indicator_template.dart @@ -12,16 +12,32 @@ class ProgressIndicatorTemplate extends TokenTemplate { @override String generate() => ''' class _Circular${blockName}DefaultsM3 extends ProgressIndicatorThemeData { - _Circular${blockName}DefaultsM3(this.context); + _Circular${blockName}DefaultsM3(this.context, { required this.indeterminate }); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; + final bool indeterminate; @override Color get color => ${componentColor('md.comp.progress-indicator.active-indicator')}; @override - Color get circularTrackColor => ${componentColor('md.comp.progress-indicator.track')}; + Color? get circularTrackColor => indeterminate ? null : ${componentColor('md.comp.progress-indicator.track')}; + + @override + double get strokeWidth => ${getToken('md.comp.progress-indicator.track.thickness')}; + + @override + double? get strokeAlign => CircularProgressIndicator.strokeAlignInside; + + @override + BoxConstraints get constraints => const BoxConstraints( + minWidth: 48.0, + minHeight: 48.0, + ); + + @override + double? get trackGap => ${getToken('md.comp.progress-indicator.active-indicator-track-space')}; } class _Linear${blockName}DefaultsM3 extends ProgressIndicatorThemeData { diff --git a/packages/flutter/lib/src/material/progress_indicator.dart b/packages/flutter/lib/src/material/progress_indicator.dart index 0b454bda51..c8fd9aef7c 100644 --- a/packages/flutter/lib/src/material/progress_indicator.dart +++ b/packages/flutter/lib/src/material/progress_indicator.dart @@ -17,7 +17,6 @@ import 'material.dart'; import 'progress_indicator_theme.dart'; import 'theme.dart'; -const double _kMinCircularProgressIndicatorSize = 36.0; const int _kIndeterminateLinearDuration = 1800; const int _kIndeterminateCircularDuration = 1333 * 2222; @@ -550,7 +549,7 @@ class _LinearProgressIndicatorState extends State with class _CircularProgressIndicatorPainter extends CustomPainter { _CircularProgressIndicatorPainter({ - this.backgroundColor, + this.trackColor, required this.valueColor, required this.value, required this.headValue, @@ -560,6 +559,8 @@ class _CircularProgressIndicatorPainter extends CustomPainter { required this.strokeWidth, required this.strokeAlign, this.strokeCap, + this.trackGap, + this.year2023 = true, }) : arcStart = value != null ? _startAngle : _startAngle + tailValue * 3 / 2 * math.pi + rotationValue * math.pi * 2.0 + offsetValue * 0.5 * math.pi, @@ -567,7 +568,7 @@ class _CircularProgressIndicatorPainter extends CustomPainter { ? clampDouble(value, 0.0, 1.0) * _sweep : math.max(headValue * 3 / 2 * math.pi - tailValue * 3 / 2 * math.pi, _epsilon); - final Color? backgroundColor; + final Color? trackColor; final Color valueColor; final double? value; final double headValue; @@ -579,6 +580,8 @@ class _CircularProgressIndicatorPainter extends CustomPainter { final double arcStart; final double arcSweep; final StrokeCap? strokeCap; + final double? trackGap; + final bool year2023; static const double _twoPi = math.pi * 2.0; static const double _epsilon = .001; @@ -601,27 +604,57 @@ class _CircularProgressIndicatorPainter extends CustomPainter { size.width - strokeOffset * 2, size.height - strokeOffset * 2, ); + final bool hasGap = trackGap != null && trackGap! > 0; - if (backgroundColor != null) { + if (trackColor != null) { final Paint backgroundPaint = Paint() - ..color = backgroundColor! + ..color = trackColor! ..strokeWidth = strokeWidth + ..strokeCap = strokeCap ?? StrokeCap.round ..style = PaintingStyle.stroke; - canvas.drawArc( - arcBaseOffset & arcActualSize, - 0, - _sweep, - false, - backgroundPaint, - ); + // If hasGap is true, draw the background arc with a gap. + if (hasGap && value! > _epsilon) { + final double arcRadius = arcActualSize.shortestSide / 2; + final double strokeRadius = strokeWidth / arcRadius; + final double gapRadius = trackGap! / arcRadius; + final double startGap = strokeRadius + gapRadius; + final double endGap = value! < _epsilon ? startGap : startGap * 2; + final double startSweep = (-math.pi / 2.0) + startGap; + final double endSweep = math.max(0.0, _twoPi - clampDouble(value!, 0.0, 1.0) * _twoPi - endGap); + // Flip the canvas for the background arc. + canvas.save(); + canvas.scale(-1, 1); + canvas.translate(-size.width, 0); + canvas.drawArc( + arcBaseOffset & arcActualSize, + startSweep, + endSweep, + false, + backgroundPaint, + ); + // Restore the canvas to draw the foreground arc. + canvas.restore(); + } else { + canvas.drawArc( + arcBaseOffset & arcActualSize, + 0, + _sweep, + false, + backgroundPaint, + ); + } } - if (value == null && strokeCap == null) { - // Indeterminate - paint.strokeCap = StrokeCap.square; + if (year2023) { + if (value == null && strokeCap == null) { + // Indeterminate + paint.strokeCap = StrokeCap.square; + } else { + // Butt when determinate (value != null) && strokeCap == null; + paint.strokeCap = strokeCap ?? StrokeCap.butt; + } } else { - // Butt when determinate (value != null) && strokeCap == null; - paint.strokeCap = strokeCap ?? StrokeCap.butt; + paint.strokeCap = strokeCap ?? StrokeCap.round; } canvas.drawArc( @@ -635,7 +668,7 @@ class _CircularProgressIndicatorPainter extends CustomPainter { @override bool shouldRepaint(_CircularProgressIndicatorPainter oldPainter) { - return oldPainter.backgroundColor != backgroundColor + return oldPainter.trackColor != trackColor || oldPainter.valueColor != valueColor || oldPainter.value != value || oldPainter.headValue != headValue @@ -644,7 +677,9 @@ class _CircularProgressIndicatorPainter extends CustomPainter { || oldPainter.rotationValue != rotationValue || oldPainter.strokeWidth != strokeWidth || oldPainter.strokeAlign != strokeAlign - || oldPainter.strokeCap != strokeCap; + || oldPainter.strokeCap != strokeCap + || oldPainter.trackGap != trackGap + || oldPainter.year2023 != year2023; } } @@ -698,19 +733,28 @@ class CircularProgressIndicator extends ProgressIndicator { super.backgroundColor, super.color, super.valueColor, - this.strokeWidth = 4.0, - this.strokeAlign = strokeAlignCenter, + this.strokeWidth, + this.strokeAlign, super.semanticsLabel, super.semanticsValue, this.strokeCap, + this.constraints, + this.trackGap, + @Deprecated( + 'Use ProgressIndicatorTheme to customize the ProgressIndicator appearance. ' + 'This feature was deprecated after v3.27.0-0.1.pre.' + ) + this.year2023 = true, }) : _indicatorType = _ActivityIndicatorType.material; /// Creates an adaptive progress indicator that is a - /// [CupertinoActivityIndicator] in [TargetPlatform.iOS] & [TargetPlatform.macOS] and [CircularProgressIndicator] in - /// material theme/non-Apple platforms. + /// [CupertinoActivityIndicator] on [TargetPlatform.iOS] & + /// [TargetPlatform.macOS] and a [CircularProgressIndicator] in material + /// theme/non-Apple platforms. /// - /// The [value], [valueColor], [strokeWidth], [semanticsLabel], and - /// [semanticsValue] will be ignored in iOS & macOS. + /// The [valueColor], [strokeWidth], [strokeAlign], [strokeCap], + /// [semanticsLabel], [semanticsValue], [trackGap], [year2023] will be + /// ignored on iOS & macOS. /// /// {@macro flutter.material.ProgressIndicator.ProgressIndicator} const CircularProgressIndicator.adaptive({ @@ -722,7 +766,14 @@ class CircularProgressIndicator extends ProgressIndicator { super.semanticsLabel, super.semanticsValue, this.strokeCap, - this.strokeAlign = strokeAlignCenter, + this.strokeAlign, + this.constraints, + this.trackGap, + @Deprecated( + 'Use ProgressIndicatorTheme to customize the ProgressIndicator appearance. ' + 'This feature was deprecated after v3.27.0-0.2.pre.' + ) + this.year2023 = true, }) : _indicatorType = _ActivityIndicatorType.adaptive; final _ActivityIndicatorType _indicatorType; @@ -738,16 +789,19 @@ class CircularProgressIndicator extends ProgressIndicator { Color? get backgroundColor => super.backgroundColor; /// The width of the line used to draw the circle. - final double strokeWidth; + final double? strokeWidth; /// The relative position of the stroke on a [CircularProgressIndicator]. /// /// Values typically range from -1.0 ([strokeAlignInside], inside stroke) /// to 1.0 ([strokeAlignOutside], outside stroke), /// without any bound constraints (e.g., a value of -2.0 is not typical, but allowed). - /// A value of 0 ([strokeAlignCenter], default) will center the border + /// A value of 0 ([strokeAlignCenter]) will center the border /// on the edge of the widget. - final double strokeAlign; + /// + /// If [year2023] is true, then the default value is [strokeAlignCenter]. + /// Otherwise, the default value is [strokeAlignInside]. + final double? strokeAlign; /// The progress indicator's line ending. /// @@ -770,6 +824,36 @@ class CircularProgressIndicator extends ProgressIndicator { /// degrees and end at 275 degrees. final StrokeCap? strokeCap; + /// Defines minimum and maximum sizes for a [CircularProgressIndicator]. + /// + /// If null, then the [ProgressIndicatorThemeData.constraints] will be used. + /// Otherwise, defaults to a minimum width and height of 36 pixels. + final BoxConstraints? constraints; + + /// The gap between the active indicator and the background track. + /// + /// If [year2023] is false or [ThemeData.useMaterial3] is false, then no track + /// gap will be drawn. + /// + /// Set [trackGap] to 0 to hide the track gap. + /// + /// If null, then the [ProgressIndicatorThemeData.trackGap] will be used. + /// If that is null, then defaults to 4. + final double? trackGap; + + /// When true, the [CircularProgressIndicator] will use the 2023 Material 3 + /// Design appearance. + /// + /// Defaults to true. If false, the [CircularProgressIndicator] will use the + /// latest Material 3 Design appearance, which was introduced in December 2023. + /// + /// If [ThemeData.useMaterial3] is false, then this property is ignored. + @Deprecated( + 'Use ProgressIndicatorTheme to customize the ProgressIndicator appearance. ' + 'This feature was deprecated after v3.27.0-0.2.pre.' + ) + final bool year2023; + /// The indicator stroke is drawn fully inside of the indicator path. /// /// This is a constant for use with [strokeAlign]. @@ -857,30 +941,50 @@ class _CircularProgressIndicatorState extends State w } Widget _buildMaterialIndicator(BuildContext context, double headValue, double tailValue, double offsetValue, double rotationValue) { - final ProgressIndicatorThemeData defaults = Theme.of(context).useMaterial3 - ? _CircularProgressIndicatorDefaultsM3(context) - : _CircularProgressIndicatorDefaultsM2(context); - final Color? trackColor = widget.backgroundColor ?? ProgressIndicatorTheme.of(context).circularTrackColor; - + final ProgressIndicatorThemeData defaults = switch (Theme.of(context).useMaterial3) { + true => widget.year2023 + ? _CircularProgressIndicatorDefaultsM3Year2023(context, indeterminate: widget.value == null) + : _CircularProgressIndicatorDefaultsM3(context, indeterminate: widget.value == null), + false => _CircularProgressIndicatorDefaultsM2(context, indeterminate: widget.value == null), + }; + final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); + final Color? trackColor = widget.backgroundColor + ?? indicatorTheme.circularTrackColor + ?? defaults.circularTrackColor; + final double strokeWidth = widget.strokeWidth + ?? indicatorTheme.strokeWidth + ?? defaults.strokeWidth!; + final double strokeAlign = widget.strokeAlign + ?? indicatorTheme.strokeAlign + ?? defaults.strokeAlign!; + final StrokeCap? strokeCap = widget.strokeCap + ?? indicatorTheme.strokeCap; + final BoxConstraints constraints = widget.constraints + ?? indicatorTheme.constraints + ?? defaults.constraints!; + final double? trackGap = widget.year2023 + ? null + : widget.trackGap ?? + indicatorTheme.trackGap ?? + defaults.trackGap; return widget._buildSemanticsWrapper( context: context, child: ConstrainedBox( - constraints: const BoxConstraints( - minWidth: _kMinCircularProgressIndicatorSize, - minHeight: _kMinCircularProgressIndicatorSize, - ), + constraints: constraints, child: CustomPaint( painter: _CircularProgressIndicatorPainter( - backgroundColor: trackColor, + trackColor: trackColor, valueColor: widget._getValueColor(context, defaultColor: defaults.color), value: widget.value, // may be null headValue: headValue, // remaining arguments are ignored if widget.value is not null tailValue: tailValue, offsetValue: offsetValue, rotationValue: rotationValue, - strokeWidth: widget.strokeWidth, - strokeAlign: widget.strokeAlign, - strokeCap: widget.strokeCap, + strokeWidth: strokeWidth, + strokeAlign: strokeAlign, + strokeCap: strokeCap, + trackGap: trackGap, + year2023: widget.year2023, ), ), ), @@ -1128,10 +1232,22 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { final double opacity = valueColor.opacity; valueColor = valueColor.withOpacity(1.0); - final Color backgroundColor = - widget.backgroundColor ?? - ProgressIndicatorTheme.of(context).refreshBackgroundColor ?? - Theme.of(context).canvasColor; + final ProgressIndicatorThemeData defaults = switch (Theme.of(context).useMaterial3) { + true => _CircularProgressIndicatorDefaultsM3Year2023(context, indeterminate: value == null), + false => _CircularProgressIndicatorDefaultsM2(context, indeterminate: value == null), + }; + final ProgressIndicatorThemeData indicatorTheme = ProgressIndicatorTheme.of(context); + final Color backgroundColor = widget.backgroundColor + ?? indicatorTheme.refreshBackgroundColor + ?? Theme.of(context).canvasColor; + final double strokeWidth = widget.strokeWidth + ?? indicatorTheme.strokeWidth + ?? defaults.strokeWidth!; + final double strokeAlign = widget.strokeAlign + ?? indicatorTheme.strokeAlign + ?? defaults.strokeAlign!; + final StrokeCap? strokeCap = widget.strokeCap + ?? indicatorTheme.strokeCap; return widget._buildSemanticsWrapper( context: context, @@ -1157,10 +1273,10 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { tailValue: tailValue, offsetValue: offsetValue, rotationValue: rotationValue, - strokeWidth: widget.strokeWidth, - strokeAlign: widget.strokeAlign, + strokeWidth: strokeWidth, + strokeAlign: strokeAlign, arrowheadScale: arrowheadScale, - strokeCap: widget.strokeCap, + strokeCap: strokeCap, ), ), ), @@ -1175,13 +1291,26 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { // Hand coded defaults based on Material Design 2. class _CircularProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData { - _CircularProgressIndicatorDefaultsM2(this.context); + _CircularProgressIndicatorDefaultsM2(this.context, { required this.indeterminate }); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; + final bool indeterminate; @override Color get color => _colors.primary; + + @override + double? get strokeWidth => 4.0; + + @override + double? get strokeAlign => CircularProgressIndicator.strokeAlignCenter; + + @override + BoxConstraints get constraints => const BoxConstraints( + minWidth: 36.0, + minHeight: 36.0, + ); } class _LinearProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData { @@ -1200,6 +1329,29 @@ class _LinearProgressIndicatorDefaultsM2 extends ProgressIndicatorThemeData { double get linearMinHeight => 4.0; } +class _CircularProgressIndicatorDefaultsM3Year2023 extends ProgressIndicatorThemeData { + _CircularProgressIndicatorDefaultsM3Year2023(this.context, { required this.indeterminate }); + + final BuildContext context; + late final ColorScheme _colors = Theme.of(context).colorScheme; + final bool indeterminate; + + @override + Color get color => _colors.primary; + + @override + double get strokeWidth => 4.0; + + @override + double? get strokeAlign => CircularProgressIndicator.strokeAlignCenter; + + @override + BoxConstraints get constraints => const BoxConstraints( + minWidth: 36.0, + minHeight: 36.0, + ); +} + class _LinearProgressIndicatorDefaultsM3Year2023 extends ProgressIndicatorThemeData { _LinearProgressIndicatorDefaultsM3Year2023(this.context); @@ -1224,16 +1376,32 @@ class _LinearProgressIndicatorDefaultsM3Year2023 extends ProgressIndicatorThemeD // dev/tools/gen_defaults/bin/gen_defaults.dart. class _CircularProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData { - _CircularProgressIndicatorDefaultsM3(this.context); + _CircularProgressIndicatorDefaultsM3(this.context, { required this.indeterminate }); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; + final bool indeterminate; @override Color get color => _colors.primary; @override - Color get circularTrackColor => _colors.secondaryContainer; + Color? get circularTrackColor => indeterminate ? null : _colors.secondaryContainer; + + @override + double get strokeWidth => 4.0; + + @override + double? get strokeAlign => CircularProgressIndicator.strokeAlignInside; + + @override + BoxConstraints get constraints => const BoxConstraints( + minWidth: 48.0, + minHeight: 48.0, + ); + + @override + double? get trackGap => 4.0; } class _LinearProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData { diff --git a/packages/flutter/lib/src/material/progress_indicator_theme.dart b/packages/flutter/lib/src/material/progress_indicator_theme.dart index f93cd215e1..31df16a02b 100644 --- a/packages/flutter/lib/src/material/progress_indicator_theme.dart +++ b/packages/flutter/lib/src/material/progress_indicator_theme.dart @@ -42,6 +42,10 @@ class ProgressIndicatorThemeData with Diagnosticable { this.borderRadius, this.stopIndicatorColor, this.stopIndicatorRadius, + this.strokeWidth, + this.strokeAlign, + this.strokeCap, + this.constraints, this.trackGap, }); @@ -85,7 +89,22 @@ class ProgressIndicatorThemeData with Diagnosticable { /// is false, then no stop indicator will be drawn. final double? stopIndicatorRadius; - /// Overrides the gap between the [LinearProgressIndicator]. + /// Overrides the stroke width of the [CircularProgressIndicator]. + final double? strokeWidth; + + /// Overrides the stroke align of the [CircularProgressIndicator]. + final double? strokeAlign; + + /// Overrides the stroke cap of the [CircularProgressIndicator]. + final StrokeCap? strokeCap; + + /// Overrides the constraints of the [CircularProgressIndicator]. + final BoxConstraints? constraints; + + /// Overrides the active indicator and the background track. + /// + /// If [CircularProgressIndicator.year2023] is false or [ThemeData.useMaterial3] + /// is false, then no track gap will be drawn. /// /// If [LinearProgressIndicator.year2023] is false or [ThemeData.useMaterial3] /// is false, then no track gap will be drawn. @@ -102,6 +121,10 @@ class ProgressIndicatorThemeData with Diagnosticable { BorderRadiusGeometry? borderRadius, Color? stopIndicatorColor, double? stopIndicatorRadius, + double? strokeWidth, + double? strokeAlign, + StrokeCap? strokeCap, + BoxConstraints? constraints, double? trackGap, }) { return ProgressIndicatorThemeData( @@ -113,6 +136,10 @@ class ProgressIndicatorThemeData with Diagnosticable { borderRadius : borderRadius ?? this.borderRadius, stopIndicatorColor : stopIndicatorColor ?? this.stopIndicatorColor, stopIndicatorRadius : stopIndicatorRadius ?? this.stopIndicatorRadius, + strokeWidth : strokeWidth ?? this.strokeWidth, + strokeAlign : strokeAlign ?? this.strokeAlign, + strokeCap : strokeCap ?? this.strokeCap, + constraints: constraints ?? this.constraints, trackGap : trackGap ?? this.trackGap, ); } @@ -133,6 +160,10 @@ class ProgressIndicatorThemeData with Diagnosticable { borderRadius : BorderRadiusGeometry.lerp(a?.borderRadius, b?.borderRadius, t), stopIndicatorColor : Color.lerp(a?.stopIndicatorColor, b?.stopIndicatorColor, t), stopIndicatorRadius : lerpDouble(a?.stopIndicatorRadius, b?.stopIndicatorRadius, t), + strokeWidth : lerpDouble(a?.strokeWidth, b?.strokeWidth, t), + strokeAlign : lerpDouble(a?.strokeAlign, b?.strokeAlign, t), + strokeCap : t < 0.5 ? a?.strokeCap : b?.strokeCap, + constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t), trackGap : lerpDouble(a?.trackGap, b?.trackGap, t), ); } @@ -147,6 +178,10 @@ class ProgressIndicatorThemeData with Diagnosticable { borderRadius, stopIndicatorColor, stopIndicatorRadius, + strokeAlign, + strokeWidth, + strokeCap, + constraints, trackGap, ); @@ -167,6 +202,10 @@ class ProgressIndicatorThemeData with Diagnosticable { && other.borderRadius == borderRadius && other.stopIndicatorColor == stopIndicatorColor && other.stopIndicatorRadius == stopIndicatorRadius + && other.strokeAlign == strokeAlign + && other.strokeWidth == strokeWidth + && other.strokeCap == strokeCap + && other.constraints == constraints && other.trackGap == trackGap; } @@ -181,6 +220,10 @@ class ProgressIndicatorThemeData with Diagnosticable { properties.add(DiagnosticsProperty('borderRadius', borderRadius, defaultValue: null)); properties.add(ColorProperty('stopIndicatorColor', stopIndicatorColor, defaultValue: null)); properties.add(DoubleProperty('stopIndicatorRadius', stopIndicatorRadius, defaultValue: null)); + properties.add(DoubleProperty('strokeWidth', strokeWidth, defaultValue: null)); + properties.add(DoubleProperty('strokeAlign', strokeAlign, defaultValue: null)); + properties.add(DiagnosticsProperty('strokeCap', strokeCap, defaultValue: null)); + properties.add(DiagnosticsProperty('constraints', constraints, defaultValue: null)); properties.add(DoubleProperty('trackGap', trackGap, defaultValue: null)); } } diff --git a/packages/flutter/test/material/progress_indicator_test.dart b/packages/flutter/test/material/progress_indicator_test.dart index e5558b07b3..103e9396e8 100644 --- a/packages/flutter/test/material/progress_indicator_test.dart +++ b/packages/flutter/test/material/progress_indicator_test.dart @@ -1217,11 +1217,10 @@ void main() { expect((wrappedTheme as ProgressIndicatorTheme).data, themeData); }); - testWidgets('default size of CircularProgressIndicator is 36x36 - M3', (WidgetTester tester) async { + testWidgets('Material3 - Default size of CircularProgressIndicator', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - theme: theme.copyWith(useMaterial3: true), - home: const Scaffold( + const MaterialApp( + home: Scaffold( body: Material( child: CircularProgressIndicator(), ), @@ -1232,6 +1231,20 @@ void main() { expect(tester.getSize(find.byType(CircularProgressIndicator)), const Size(36, 36)); }); + testWidgets('Material3 - Default size of CircularProgressIndicator when year2023 is false', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Material( + child: CircularProgressIndicator(year2023: false), + ), + ), + ), + ); + + expect(tester.getSize(find.byType(CircularProgressIndicator)), const Size(48, 48)); + }); + testWidgets('RefreshProgressIndicator using fields correctly', (WidgetTester tester) async { Future pumpIndicator(RefreshProgressIndicator indicator) { return tester.pumpWidget(Theme(data: theme, child: indicator)); @@ -1551,6 +1564,151 @@ void main() { ), ); }); + + testWidgets('Default determinate CircularProgressIndicator when year2023 is false', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: theme, + home: const Center( + child: CircularProgressIndicator( + year2023: false, + value: 0.5, + ), + ), + )); + + expect(tester.getSize(find.byType(CircularProgressIndicator)), equals(const Size(48, 48))); + expect( + find.byType(CircularProgressIndicator), + paints + // Track. + ..arc( + rect: const Rect.fromLTRB(2.0, 2.0, 46.0, 46.0), + color: theme.colorScheme.secondaryContainer, + strokeWidth: 4.0, + strokeCap: StrokeCap.round, + style: PaintingStyle.stroke, + ) + // Active indicator. + ..arc( + rect: const Rect.fromLTRB(2.0, 2.0, 46.0, 46.0), + color: theme.colorScheme.primary, + strokeWidth: 4.0, + strokeCap: StrokeCap.round, + style: PaintingStyle.stroke, + ), + ); + await expectLater( + find.byType(CircularProgressIndicator), + matchesGoldenFile('circular_progress_indicator_determinate_year2023_false.png'), + ); + }); + + testWidgets('Default indeterminate CircularProgressIndicator when year2023 is false', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: theme, + home: const Center(child: CircularProgressIndicator(year2023: false)), + )); + + // Advance the animation. + await tester.pump(const Duration(milliseconds: 200)); + + expect(tester.getSize(find.byType(CircularProgressIndicator)), equals(const Size(48, 48))); + expect( + find.byType(CircularProgressIndicator), + paints + // Active indicator. + ..arc( + rect: const Rect.fromLTRB(2.0, 2.0, 46.0, 46.0), + color: theme.colorScheme.primary, + strokeWidth: 4.0, + strokeCap: StrokeCap.round, + style: PaintingStyle.stroke, + ), + ); + await expectLater( + find.byType(CircularProgressIndicator), + matchesGoldenFile('circular_progress_indicator_indeterminate_year2023_false.png'), + ); + }); + + testWidgets('CircularProgressIndicator track gap can be adjusted when year2023 is false', (WidgetTester tester) async { + Widget buildIndicator({ double? trackGap }) { + return MaterialApp( + home: Center( + child: CircularProgressIndicator( + year2023: false, + trackGap: trackGap, + value: 0.5, + ), + ), + ); + } + + await tester.pumpWidget(buildIndicator()); + await expectLater( + find.byType(CircularProgressIndicator), + matchesGoldenFile('circular_progress_indicator_default_track_gap_year2023_false.png'), + ); + + await tester.pumpWidget(buildIndicator(trackGap: 12.0)); + await expectLater( + find.byType(CircularProgressIndicator), + matchesGoldenFile('circular_progress_indicator_custom_track_gap_year2023_false.png'), + ); + + await tester.pumpWidget(buildIndicator(trackGap: 0.0)); + await expectLater( + find.byType(CircularProgressIndicator), + matchesGoldenFile('circular_progress_indicator_no_track_gap_year2023_false.png'), + ); + }); + + testWidgets('Can override CircularProgressIndicator stroke cap when year2023 is false', (WidgetTester tester) async { + const StrokeCap strokeCap = StrokeCap.square; + await tester.pumpWidget( + const MaterialApp( + home: Center( + child: CircularProgressIndicator( + year2023: false, + strokeCap: strokeCap, + value: 0.5, + ), + ), + ) + ); + + expect( + find.byType(CircularProgressIndicator), + paints + // Track. + ..arc(strokeCap: strokeCap) + // Active indicator. + ..arc(strokeCap: strokeCap) + ); + await expectLater( + find.byType(CircularProgressIndicator), + matchesGoldenFile('circular_progress_indicator_custom_stroke_cap_year2023_false.png'), + ); + }); + + testWidgets('CircularProgressIndicator.constraints can override default size', (WidgetTester tester) async { + const Size size = Size(64, 64); + await tester.pumpWidget( + MaterialApp( + home: Center( + child: CircularProgressIndicator( + constraints: BoxConstraints( + minWidth: size.width, + minHeight: size.height + ), + value: 0.5, + ), + ), + ) + ); + + expect(tester.getSize(find.byType(CircularProgressIndicator)), equals(size)); + }); } class _RefreshProgressIndicatorGolden extends StatefulWidget { diff --git a/packages/flutter/test/material/progress_indicator_theme_test.dart b/packages/flutter/test/material/progress_indicator_theme_test.dart index 2e74f9beae..8f85a64ab0 100644 --- a/packages/flutter/test/material/progress_indicator_theme_test.dart +++ b/packages/flutter/test/material/progress_indicator_theme_test.dart @@ -2,7 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// reduced-test-set: +// This file is run as part of a reduced test set in CI on Mac and Windows +// machines. +@Tags(['reduced-test-set']) +library; + import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -17,6 +24,46 @@ void main() { expect(identical(ProgressIndicatorThemeData.lerp(data, data, 0.5), data), true); }); + testWidgets('ProgressIndicatorThemeData implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const ProgressIndicatorThemeData( + color: Color(0XFF0000F1), + linearTrackColor: Color(0XFF0000F2), + linearMinHeight: 25.0, + circularTrackColor: Color(0XFF0000F3), + refreshBackgroundColor: Color(0XFF0000F4), + borderRadius: BorderRadius.all(Radius.circular(8.0)), + stopIndicatorColor: Color(0XFF0000F5), + stopIndicatorRadius: 10.0, + strokeWidth: 8.0, + strokeAlign: BorderSide.strokeAlignOutside, + strokeCap: StrokeCap.butt, + constraints: BoxConstraints.tightFor(width: 80.0, height: 80.0), + trackGap: 16.0, + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, equalsIgnoringHashCodes([ + 'color: Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.9451, colorSpace: ColorSpace.sRGB)', + 'linearTrackColor: Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.9490, colorSpace: ColorSpace.sRGB)', + 'linearMinHeight: 25.0', + 'circularTrackColor: Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.9529, colorSpace: ColorSpace.sRGB)', + 'refreshBackgroundColor: Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.9569, colorSpace: ColorSpace.sRGB)', + 'borderRadius: BorderRadius.circular(8.0)', + 'stopIndicatorColor: Color(alpha: 1.0000, red: 0.0000, green: 0.0000, blue: 0.9608, colorSpace: ColorSpace.sRGB)', + 'stopIndicatorRadius: 10.0', + 'strokeWidth: 8.0', + 'strokeAlign: 1.0', + 'strokeCap: StrokeCap.butt', + 'constraints: BoxConstraints(w=80.0, h=80.0)', + 'trackGap: 16.0' + ])); + }); + testWidgets('Can theme LinearProgressIndicator using ProgressIndicatorTheme', (WidgetTester tester) async { const Color color = Color(0XFF00FF00); const Color linearTrackColor = Color(0XFFFF0000); @@ -62,7 +109,7 @@ void main() { ); }); - testWidgets('Can theme LinearProgressIndicator with year2023 to false', (WidgetTester tester) async { + testWidgets('Can theme LinearProgressIndicator when year2023 to false', (WidgetTester tester) async { const Color color = Color(0XFF00FF00); const Color linearTrackColor = Color(0XFFFF0000); const double linearMinHeight = 25.0; @@ -182,4 +229,119 @@ void main() { ), ); }); + + testWidgets('Can theme CircularProgressIndicator using ProgressIndicatorTheme', (WidgetTester tester) async { + const Color color = Color(0XFFFF0000); + const Color circularTrackColor = Color(0XFF0000FF); + const double strokeWidth = 8.0; + const double strokeAlign = BorderSide.strokeAlignOutside; + const StrokeCap strokeCap = StrokeCap.butt; + const BoxConstraints constraints = BoxConstraints.tightFor(width: 80.0, height: 80.0); + final ThemeData theme = ThemeData( + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: color, + circularTrackColor: circularTrackColor, + strokeWidth: strokeWidth, + strokeAlign: strokeAlign, + strokeCap: strokeCap, + constraints: constraints, + ), + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Scaffold( + body: Center( + child: CircularProgressIndicator( + value: 0.5, + ), + ), + ), + ), + ); + + expect( + tester.getSize(find.byType(CircularProgressIndicator)), + equals(Size(constraints.maxWidth, constraints.maxHeight)), + ); + expect( + find.byType(CircularProgressIndicator), + paints + // Track. + ..arc( + color: circularTrackColor, + strokeWidth: strokeWidth, + strokeCap: strokeCap, + ) + // Active indicator. + ..arc( + color: color, + strokeWidth: strokeWidth, + strokeCap: strokeCap, + ), + ); + await expectLater( + find.byType(CircularProgressIndicator), + matchesGoldenFile('circular_progress_indicator_theme.png'), + ); + }); + + testWidgets('Can theme CircularProgressIndicator when year2023 to false', (WidgetTester tester) async { + const Color color = Color(0XFFFF0000); + const Color circularTrackColor = Color(0XFF0000FF); + const double strokeWidth = 8.0; + const double strokeAlign = BorderSide.strokeAlignOutside; + const StrokeCap strokeCap = StrokeCap.butt; + const BoxConstraints constraints = BoxConstraints.tightFor(width: 80.0, height: 80.0); + const double trackGap = 12.0; + final ThemeData theme = ThemeData( + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: color, + circularTrackColor: circularTrackColor, + strokeWidth: strokeWidth, + strokeAlign: strokeAlign, + strokeCap: strokeCap, + constraints: constraints, + trackGap: trackGap, + ), + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Scaffold( + body: Center( + child: CircularProgressIndicator( + year2023: false, + value: 0.5, + ), + ), + ), + ), + ); + + expect( + tester.getSize(find.byType(CircularProgressIndicator)), + equals(Size(constraints.maxWidth, constraints.maxHeight)), + ); + expect( + find.byType(CircularProgressIndicator), + paints + // Track. + ..arc( + color: circularTrackColor, + strokeWidth: strokeWidth, + strokeCap: strokeCap, + ) + // Active indicator. + ..arc( + color: color, + strokeWidth: strokeWidth, + strokeCap: strokeCap, + ), + ); + await expectLater( + find.byType(CircularProgressIndicator), + matchesGoldenFile('circular_progress_indicator_theme_year2023_false.png'), + ); + }); }