diff --git a/dev/tools/gen_defaults/lib/progress_indicator_template.dart b/dev/tools/gen_defaults/lib/progress_indicator_template.dart index d7a1effaef..8c04d76a19 100644 --- a/dev/tools/gen_defaults/lib/progress_indicator_template.dart +++ b/dev/tools/gen_defaults/lib/progress_indicator_template.dart @@ -32,12 +32,15 @@ class _Circular${blockName}DefaultsM3 extends ProgressIndicatorThemeData { @override BoxConstraints get constraints => const BoxConstraints( - minWidth: 48.0, - minHeight: 48.0, + minWidth: 40.0, + minHeight: 40.0, ); @override double? get trackGap => ${getToken('md.comp.progress-indicator.active-indicator-track-space')}; + + @override + EdgeInsetsGeometry? get circularTrackPadding => const EdgeInsets.all(4.0); } 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 c8fd9aef7c..b92922244b 100644 --- a/packages/flutter/lib/src/material/progress_indicator.dart +++ b/packages/flutter/lib/src/material/progress_indicator.dart @@ -745,6 +745,7 @@ class CircularProgressIndicator extends ProgressIndicator { 'This feature was deprecated after v3.27.0-0.1.pre.' ) this.year2023 = true, + this.padding, }) : _indicatorType = _ActivityIndicatorType.material; /// Creates an adaptive progress indicator that is a @@ -774,6 +775,7 @@ class CircularProgressIndicator extends ProgressIndicator { 'This feature was deprecated after v3.27.0-0.2.pre.' ) this.year2023 = true, + this.padding, }) : _indicatorType = _ActivityIndicatorType.adaptive; final _ActivityIndicatorType _indicatorType; @@ -854,6 +856,13 @@ class CircularProgressIndicator extends ProgressIndicator { ) final bool year2023; + /// The padding around the indicator track. + /// + /// If null, then the [ProgressIndicatorThemeData.circularTrackPadding] will be + /// used. If that is null and [year2023] is false, then defaults to `EdgeInsets.all(4.0)` + /// padding. Otherwise, defaults to zero padding. + final EdgeInsetsGeometry? padding; + /// The indicator stroke is drawn fully inside of the indicator path. /// /// This is a constant for use with [strokeAlign]. @@ -967,28 +976,41 @@ class _CircularProgressIndicatorState extends State w : widget.trackGap ?? indicatorTheme.trackGap ?? defaults.trackGap; - return widget._buildSemanticsWrapper( - context: context, - child: ConstrainedBox( - constraints: constraints, - child: CustomPaint( - painter: _CircularProgressIndicatorPainter( - 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: strokeWidth, - strokeAlign: strokeAlign, - strokeCap: strokeCap, - trackGap: trackGap, - year2023: widget.year2023, - ), + final EdgeInsetsGeometry? effectivePadding = widget.padding + ?? indicatorTheme.circularTrackPadding + ?? defaults.circularTrackPadding; + + Widget result = ConstrainedBox( + constraints: constraints, + child: CustomPaint( + painter: _CircularProgressIndicatorPainter( + 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: strokeWidth, + strokeAlign: strokeAlign, + strokeCap: strokeCap, + trackGap: trackGap, + year2023: widget.year2023, ), ), ); + + if (effectivePadding != null) { + result = Padding( + padding: effectivePadding, + child: result, + ); + } + + return widget._buildSemanticsWrapper( + context: context, + child: result, + ); } Widget _buildAnimation() { @@ -1396,12 +1418,15 @@ class _CircularProgressIndicatorDefaultsM3 extends ProgressIndicatorThemeData { @override BoxConstraints get constraints => const BoxConstraints( - minWidth: 48.0, - minHeight: 48.0, + minWidth: 40.0, + minHeight: 40.0, ); @override double? get trackGap => 4.0; + + @override + EdgeInsetsGeometry? get circularTrackPadding => const EdgeInsets.all(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 31df16a02b..2a6f7f4eb2 100644 --- a/packages/flutter/lib/src/material/progress_indicator_theme.dart +++ b/packages/flutter/lib/src/material/progress_indicator_theme.dart @@ -47,6 +47,7 @@ class ProgressIndicatorThemeData with Diagnosticable { this.strokeCap, this.constraints, this.trackGap, + this.circularTrackPadding, }); /// The color of the [ProgressIndicator]'s indicator. @@ -110,6 +111,9 @@ class ProgressIndicatorThemeData with Diagnosticable { /// is false, then no track gap will be drawn. final double? trackGap; + /// Overrides the padding of the [CircularProgressIndicator]. + final EdgeInsetsGeometry? circularTrackPadding; + /// Creates a copy of this object but with the given fields replaced with the /// new values. ProgressIndicatorThemeData copyWith({ @@ -126,6 +130,7 @@ class ProgressIndicatorThemeData with Diagnosticable { StrokeCap? strokeCap, BoxConstraints? constraints, double? trackGap, + EdgeInsetsGeometry? circularTrackPadding, }) { return ProgressIndicatorThemeData( color: color ?? this.color, @@ -141,6 +146,7 @@ class ProgressIndicatorThemeData with Diagnosticable { strokeCap : strokeCap ?? this.strokeCap, constraints: constraints ?? this.constraints, trackGap : trackGap ?? this.trackGap, + circularTrackPadding: circularTrackPadding ?? this.circularTrackPadding, ); } @@ -165,6 +171,7 @@ class ProgressIndicatorThemeData with Diagnosticable { strokeCap : t < 0.5 ? a?.strokeCap : b?.strokeCap, constraints: BoxConstraints.lerp(a?.constraints, b?.constraints, t), trackGap : lerpDouble(a?.trackGap, b?.trackGap, t), + circularTrackPadding: EdgeInsetsGeometry.lerp(a?.circularTrackPadding, b?.circularTrackPadding, t), ); } @@ -183,6 +190,7 @@ class ProgressIndicatorThemeData with Diagnosticable { strokeCap, constraints, trackGap, + circularTrackPadding, ); @override @@ -206,7 +214,8 @@ class ProgressIndicatorThemeData with Diagnosticable { && other.strokeWidth == strokeWidth && other.strokeCap == strokeCap && other.constraints == constraints - && other.trackGap == trackGap; + && other.trackGap == trackGap + && other.circularTrackPadding == circularTrackPadding; } @override @@ -225,6 +234,7 @@ class ProgressIndicatorThemeData with Diagnosticable { properties.add(DiagnosticsProperty('strokeCap', strokeCap, defaultValue: null)); properties.add(DiagnosticsProperty('constraints', constraints, defaultValue: null)); properties.add(DoubleProperty('trackGap', trackGap, defaultValue: null)); + properties.add(DiagnosticsProperty('circularTrackPadding', circularTrackPadding, defaultValue: null)); } } diff --git a/packages/flutter/test/material/progress_indicator_test.dart b/packages/flutter/test/material/progress_indicator_test.dart index 103e9396e8..d38988e801 100644 --- a/packages/flutter/test/material/progress_indicator_test.dart +++ b/packages/flutter/test/material/progress_indicator_test.dart @@ -1566,6 +1566,7 @@ void main() { }); testWidgets('Default determinate CircularProgressIndicator when year2023 is false', (WidgetTester tester) async { + const EdgeInsetsGeometry padding = EdgeInsets.all(4.0); await tester.pumpWidget(MaterialApp( theme: theme, home: const Center( @@ -1576,13 +1577,23 @@ void main() { ), )); - expect(tester.getSize(find.byType(CircularProgressIndicator)), equals(const Size(48, 48))); + final Size indicatorBoxSize = tester.getSize(find.descendant( + of: find.byType(CircularProgressIndicator), + matching: find.byType(ConstrainedBox), + )); + expect( + tester.getSize(find.byType(CircularProgressIndicator)), + equals(Size( + indicatorBoxSize.width + padding.horizontal, + indicatorBoxSize.height + padding.vertical, + )), + ); expect( find.byType(CircularProgressIndicator), paints // Track. ..arc( - rect: const Rect.fromLTRB(2.0, 2.0, 46.0, 46.0), + rect: const Rect.fromLTRB(2.0, 2.0, 38.0, 38.0), color: theme.colorScheme.secondaryContainer, strokeWidth: 4.0, strokeCap: StrokeCap.round, @@ -1590,7 +1601,7 @@ void main() { ) // Active indicator. ..arc( - rect: const Rect.fromLTRB(2.0, 2.0, 46.0, 46.0), + rect: const Rect.fromLTRB(2.0, 2.0, 38.0, 38.0), color: theme.colorScheme.primary, strokeWidth: 4.0, strokeCap: StrokeCap.round, @@ -1604,6 +1615,7 @@ void main() { }); testWidgets('Default indeterminate CircularProgressIndicator when year2023 is false', (WidgetTester tester) async { + const EdgeInsetsGeometry padding = EdgeInsets.all(4.0); await tester.pumpWidget(MaterialApp( theme: theme, home: const Center(child: CircularProgressIndicator(year2023: false)), @@ -1612,13 +1624,23 @@ void main() { // Advance the animation. await tester.pump(const Duration(milliseconds: 200)); - expect(tester.getSize(find.byType(CircularProgressIndicator)), equals(const Size(48, 48))); + final Size indicatorBoxSize = tester.getSize(find.descendant( + of: find.byType(CircularProgressIndicator), + matching: find.byType(ConstrainedBox), + )); + expect( + tester.getSize(find.byType(CircularProgressIndicator)), + equals(Size( + indicatorBoxSize.width + padding.horizontal, + indicatorBoxSize.height + padding.vertical, + )), + ); expect( find.byType(CircularProgressIndicator), paints // Active indicator. ..arc( - rect: const Rect.fromLTRB(2.0, 2.0, 46.0, 46.0), + rect: const Rect.fromLTRB(2.0, 2.0, 38.0, 38.0), color: theme.colorScheme.primary, strokeWidth: 4.0, strokeCap: StrokeCap.round, @@ -1709,6 +1731,33 @@ void main() { expect(tester.getSize(find.byType(CircularProgressIndicator)), equals(size)); }); + + testWidgets('CircularProgressIndicator padding can be customized', (WidgetTester tester) async { + const EdgeInsetsGeometry padding = EdgeInsets.all(12.0); + await tester.pumpWidget( + const MaterialApp( + home: Center( + child: CircularProgressIndicator( + padding: padding, + year2023: false, + value: 0.5, + ), + ), + ) + ); + + final Size indicatorBoxSize = tester.getSize(find.descendant( + of: find.byType(CircularProgressIndicator), + matching: find.byType(ConstrainedBox), + )); + expect( + tester.getSize(find.byType(CircularProgressIndicator)), + equals(Size( + indicatorBoxSize.width + padding.horizontal, + indicatorBoxSize.height + padding.vertical, + )), + ); + }); } 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 8f85a64ab0..5f1b153cce 100644 --- a/packages/flutter/test/material/progress_indicator_theme_test.dart +++ b/packages/flutter/test/material/progress_indicator_theme_test.dart @@ -40,6 +40,7 @@ void main() { strokeCap: StrokeCap.butt, constraints: BoxConstraints.tightFor(width: 80.0, height: 80.0), trackGap: 16.0, + circularTrackPadding: EdgeInsets.all(12.0), ).debugFillProperties(builder); final List description = builder.properties @@ -60,7 +61,8 @@ void main() { 'strokeAlign: 1.0', 'strokeCap: StrokeCap.butt', 'constraints: BoxConstraints(w=80.0, h=80.0)', - 'trackGap: 16.0' + 'trackGap: 16.0', + 'circularTrackPadding: EdgeInsets.all(12.0)' ])); }); @@ -237,6 +239,7 @@ void main() { const double strokeAlign = BorderSide.strokeAlignOutside; const StrokeCap strokeCap = StrokeCap.butt; const BoxConstraints constraints = BoxConstraints.tightFor(width: 80.0, height: 80.0); + const EdgeInsets padding = EdgeInsets.all(14.0); final ThemeData theme = ThemeData( progressIndicatorTheme: const ProgressIndicatorThemeData( color: color, @@ -245,6 +248,7 @@ void main() { strokeAlign: strokeAlign, strokeCap: strokeCap, constraints: constraints, + circularTrackPadding: padding, ), ); await tester.pumpWidget( @@ -262,7 +266,10 @@ void main() { expect( tester.getSize(find.byType(CircularProgressIndicator)), - equals(Size(constraints.maxWidth, constraints.maxHeight)), + equals(Size( + constraints.maxWidth + padding.horizontal, + constraints.maxHeight + padding.vertical, + )), ); expect( find.byType(CircularProgressIndicator), @@ -294,6 +301,7 @@ void main() { const StrokeCap strokeCap = StrokeCap.butt; const BoxConstraints constraints = BoxConstraints.tightFor(width: 80.0, height: 80.0); const double trackGap = 12.0; + const EdgeInsets padding = EdgeInsets.all(18.0); final ThemeData theme = ThemeData( progressIndicatorTheme: const ProgressIndicatorThemeData( color: color, @@ -303,6 +311,7 @@ void main() { strokeCap: strokeCap, constraints: constraints, trackGap: trackGap, + circularTrackPadding: padding, ), ); await tester.pumpWidget( @@ -319,9 +328,17 @@ void main() { ), ); + final Size indicatorBoxSize = tester.getSize(find.descendant( + of: find.byType(CircularProgressIndicator), + matching: find.byType(ConstrainedBox), + )); + expect(indicatorBoxSize, constraints.biggest); expect( tester.getSize(find.byType(CircularProgressIndicator)), - equals(Size(constraints.maxWidth, constraints.maxHeight)), + equals(Size( + indicatorBoxSize.width + padding.horizontal, + indicatorBoxSize.height + padding.vertical, + )), ); expect( find.byType(CircularProgressIndicator),