Introduce CircularProgressIndicator.padding for the updated M3 specs (#159271)

Fix [Add `CircularProgressIndicator` padding to match M3
specs](https://github.com/flutter/flutter/issues/159267)

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Container(
                width: 40,
                height: 40,
                color: Colors.red,
                alignment: Alignment.center,
                child: const Text(
                  '40x40px',
                  style: TextStyle(fontSize: 8, color: Colors.white),
                ),
              ),
              const ColoredBox(
                color: Colors.amber,
                child: CircularProgressIndicator(
                  year2023: false,
                  value: 0.4,
                ),
              ),
              Container(
                width: 48,
                height: 48,
                color: Colors.red,
                alignment: Alignment.center,
                child: const Text(
                  '48x48px',
                  style: TextStyle(fontSize: 10, color: Colors.white),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

```

</details>

### Preview

<img width="454" alt="Screenshot 2024-11-21 at 17 13 25"
src="https://github.com/user-attachments/assets/6f7520f1-a213-4814-8116-6dd996639eec">

### Specs


![Screenshot_2024-11-20_at_12 17
46_PM](https://github.com/user-attachments/assets/3ab005cc-e93a-485a-8470-f80072440948)


## 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].

<!-- Links -->
[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
This commit is contained in:
Taha Tesser 2024-11-22 20:42:57 +02:00 committed by GitHub
parent a77aad7517
commit e21db87817
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 136 additions and 32 deletions

View File

@ -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 {

View File

@ -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,9 +976,11 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> w
: widget.trackGap ??
indicatorTheme.trackGap ??
defaults.trackGap;
return widget._buildSemanticsWrapper(
context: context,
child: ConstrainedBox(
final EdgeInsetsGeometry? effectivePadding = widget.padding
?? indicatorTheme.circularTrackPadding
?? defaults.circularTrackPadding;
Widget result = ConstrainedBox(
constraints: constraints,
child: CustomPaint(
painter: _CircularProgressIndicatorPainter(
@ -987,7 +998,18 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> w
year2023: widget.year2023,
),
),
),
);
if (effectivePadding != null) {
result = Padding(
padding: effectivePadding,
child: result,
);
}
return widget._buildSemanticsWrapper(
context: context,
child: result,
);
}
@ -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 {

View File

@ -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', strokeCap, defaultValue: null));
properties.add(DiagnosticsProperty<BoxConstraints>('constraints', constraints, defaultValue: null));
properties.add(DoubleProperty('trackGap', trackGap, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('circularTrackPadding', circularTrackPadding, defaultValue: null));
}
}

View File

@ -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 {

View File

@ -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<String> 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),