Add backgroundColor to RadioThemeData (#171326)

Part of https://github.com/flutter/flutter/issues/168787

Follow-up of https://github.com/flutter/flutter/pull/171204

## 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].
- [x] 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:
Valentin Vignal 2025-07-03 02:38:08 +08:00 committed by GitHub
parent 10298edaab
commit bf3e8566eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 26 deletions

View File

@ -88,6 +88,10 @@ class _RadioDefaultsM3 extends RadioThemeData {
@override
VisualDensity get visualDensity => _theme.visualDensity;
@override
WidgetStateProperty<Color> get backgroundColor =>
WidgetStateProperty.all<Color>(Colors.transparent);
}
''';
}

View File

@ -418,6 +418,7 @@ class Radio<T> extends StatefulWidget {
/// {@endtemplate}
final bool? enabled;
/// {@template flutter.material.Radio.backgroundColor}
/// The color of the background of the radio button, in all [WidgetState]s.
///
/// Resolves in the following states:
@ -427,6 +428,7 @@ class Radio<T> extends StatefulWidget {
/// * [WidgetState.disabled].
///
/// If null, then it is transparent in all states.
/// {@endtemplate}
final WidgetStateProperty<Color?>? backgroundColor;
/// The side for the circular border of the radio button, in all
@ -673,11 +675,14 @@ class _RadioPaintState extends State<_RadioPaint> {
radioTheme.fillColor?.resolve(inactiveStates);
final Color effectiveInactiveColor =
inactiveColor ?? defaults.fillColor!.resolve(inactiveStates)!;
// TODO(ValentinVignal): Add backgroundColor to RadioThemeData.
final Color activeBackgroundColor =
widget.backgroundColor?.resolve(activeStates) ?? Colors.transparent;
widget.backgroundColor?.resolve(activeStates) ??
radioTheme.backgroundColor?.resolve(activeStates) ??
defaults.backgroundColor!.resolve(activeStates)!;
final Color inactiveBackgroundColor =
widget.backgroundColor?.resolve(inactiveStates) ?? Colors.transparent;
widget.backgroundColor?.resolve(inactiveStates) ??
radioTheme.backgroundColor?.resolve(inactiveStates) ??
defaults.backgroundColor!.resolve(inactiveStates)!;
final Set<MaterialState> focusedStates =
widget.toggleableState.states..add(MaterialState.focused);
@ -908,6 +913,10 @@ class _RadioDefaultsM2 extends RadioThemeData {
@override
VisualDensity get visualDensity => _theme.visualDensity;
@override
WidgetStateProperty<Color> get backgroundColor =>
WidgetStateProperty.all<Color>(Colors.transparent);
}
// BEGIN GENERATED TOKEN PROPERTIES - Radio<T>
@ -992,6 +1001,10 @@ class _RadioDefaultsM3 extends RadioThemeData {
@override
VisualDensity get visualDensity => _theme.visualDensity;
@override
WidgetStateProperty<Color> get backgroundColor =>
WidgetStateProperty.all<Color>(Colors.transparent);
}
// dart format on

View File

@ -49,6 +49,7 @@ class RadioThemeData with Diagnosticable {
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.backgroundColor,
});
/// {@macro flutter.widget.RawRadio.mouseCursor}
@ -86,6 +87,9 @@ class RadioThemeData with Diagnosticable {
/// default value is the value of [ThemeData.visualDensity].
final VisualDensity? visualDensity;
/// {@macro flutter.material.Radio.backgroundColor}
final WidgetStateProperty<Color?>? backgroundColor;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
RadioThemeData copyWith({
@ -95,6 +99,7 @@ class RadioThemeData with Diagnosticable {
double? splashRadius,
MaterialTapTargetSize? materialTapTargetSize,
VisualDensity? visualDensity,
WidgetStateProperty<Color?>? backgroundColor,
}) {
return RadioThemeData(
mouseCursor: mouseCursor ?? this.mouseCursor,
@ -103,6 +108,7 @@ class RadioThemeData with Diagnosticable {
splashRadius: splashRadius ?? this.splashRadius,
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
visualDensity: visualDensity ?? this.visualDensity,
backgroundColor: backgroundColor ?? this.backgroundColor,
);
}
@ -125,6 +131,12 @@ class RadioThemeData with Diagnosticable {
),
splashRadius: lerpDouble(a?.splashRadius, b?.splashRadius, t),
visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
backgroundColor: WidgetStateProperty.lerp<Color?>(
a?.backgroundColor,
b?.backgroundColor,
t,
Color.lerp,
),
);
}
@ -136,6 +148,7 @@ class RadioThemeData with Diagnosticable {
splashRadius,
materialTapTargetSize,
visualDensity,
backgroundColor,
);
@override
@ -152,7 +165,8 @@ class RadioThemeData with Diagnosticable {
other.overlayColor == overlayColor &&
other.splashRadius == splashRadius &&
other.materialTapTargetSize == materialTapTargetSize &&
other.visualDensity == visualDensity;
other.visualDensity == visualDensity &&
other.backgroundColor == backgroundColor;
}
@override
@ -190,6 +204,13 @@ class RadioThemeData with Diagnosticable {
properties.add(
DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null),
);
properties.add(
DiagnosticsProperty<WidgetStateProperty<Color?>>(
'backgroundColor',
backgroundColor,
defaultValue: null,
),
);
}
}

View File

@ -27,6 +27,7 @@ void main() {
expect(themeData.splashRadius, null);
expect(themeData.materialTapTargetSize, null);
expect(themeData.visualDensity, null);
expect(themeData.backgroundColor, null);
const RadioTheme theme = RadioTheme(data: RadioThemeData(), child: SizedBox());
expect(theme.data.mouseCursor, null);
@ -35,6 +36,7 @@ void main() {
expect(theme.data.splashRadius, null);
expect(theme.data.materialTapTargetSize, null);
expect(theme.data.visualDensity, null);
expect(theme.data.backgroundColor, null);
});
testWidgets('Default RadioThemeData debugFillProperties', (WidgetTester tester) async {
@ -47,18 +49,19 @@ void main() {
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
expect(description, const <String>[]);
});
testWidgets('RadioThemeData implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const RadioThemeData(
mouseCursor: MaterialStatePropertyAll<MouseCursor>(SystemMouseCursors.click),
fillColor: MaterialStatePropertyAll<Color>(Color(0xfffffff0)),
overlayColor: MaterialStatePropertyAll<Color>(Color(0xfffffff1)),
mouseCursor: WidgetStatePropertyAll<MouseCursor>(SystemMouseCursors.click),
fillColor: WidgetStatePropertyAll<Color>(Color(0xfffffff0)),
overlayColor: WidgetStatePropertyAll<Color>(Color(0xfffffff1)),
splashRadius: 1.0,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.standard,
backgroundColor: WidgetStatePropertyAll<Color>(Color(0xfffffff2)),
).debugFillProperties(builder);
final List<String> description =
@ -76,6 +79,7 @@ void main() {
'splashRadius: 1.0',
'materialTapTargetSize: MaterialTapTargetSize.shrinkWrap',
'visualDensity: VisualDensity#00000(h: 0.0, v: 0.0)',
'backgroundColor: WidgetStatePropertyAll(${const Color(0xfffffff2)})',
]),
);
});
@ -88,6 +92,8 @@ void main() {
const Color selectedFillColor = Color(0xfffffff1);
const Color focusOverlayColor = Color(0xfffffff2);
const Color hoverOverlayColor = Color(0xfffffff3);
const Color defaultBackgroundColor = Color(0xfffffff4);
const Color selectedBackgroundColor = Color(0xfffffff5);
const double splashRadius = 1.0;
const MaterialTapTargetSize materialTapTargetSize = MaterialTapTargetSize.shrinkWrap;
const VisualDensity visualDensity = VisualDensity(horizontal: 1, vertical: 1);
@ -96,18 +102,18 @@ void main() {
return MaterialApp(
theme: ThemeData(
radioTheme: RadioThemeData(
mouseCursor: const MaterialStatePropertyAll<MouseCursor>(mouseCursor),
fillColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
mouseCursor: const WidgetStatePropertyAll<MouseCursor>(mouseCursor),
fillColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return selectedFillColor;
}
return defaultFillColor;
}),
overlayColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.focused)) {
overlayColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.focused)) {
return focusOverlayColor;
}
if (states.contains(MaterialState.hovered)) {
if (states.contains(WidgetState.hovered)) {
return hoverOverlayColor;
}
return null;
@ -115,6 +121,12 @@ void main() {
splashRadius: splashRadius,
materialTapTargetSize: materialTapTargetSize,
visualDensity: visualDensity,
backgroundColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return selectedBackgroundColor;
}
return defaultBackgroundColor;
}),
),
),
home: Scaffold(
@ -134,7 +146,7 @@ void main() {
expect(
_getRadioMaterial(tester),
paints
..circle(color: Colors.transparent)
..circle(color: defaultBackgroundColor)
..circle(color: defaultFillColor),
);
// Size from MaterialTapTargetSize.shrinkWrap with added VisualDensity.
@ -146,7 +158,7 @@ void main() {
expect(
_getRadioMaterial(tester),
paints
..circle(color: Colors.transparent)
..circle(color: selectedBackgroundColor)
..circle(color: selectedFillColor),
);
@ -177,6 +189,8 @@ void main() {
const Color themeSelectedFillColor = Color(0xfffffff1);
const Color themeFocusOverlayColor = Color(0xfffffff2);
const Color themeHoverOverlayColor = Color(0xfffffff3);
const Color themeDefaultBackgroundColor = Color(0xfffffff4);
const Color themeSelectedBackgroundColor = Color(0xfffffff5);
const double themeSplashRadius = 1.0;
const MaterialTapTargetSize themeMaterialTapTargetSize = MaterialTapTargetSize.padded;
const VisualDensity themeVisualDensity = VisualDensity.standard;
@ -186,6 +200,8 @@ void main() {
const Color selectedFillColor = Color(0xfffffff1);
const Color focusColor = Color(0xfffffff2);
const Color hoverColor = Color(0xfffffff3);
const Color defaultBackgroundColor = Color(0xfffffff4);
const Color selectedBackgroundColor = Color(0xfffffff5);
const double splashRadius = 2.0;
const MaterialTapTargetSize materialTapTargetSize = MaterialTapTargetSize.shrinkWrap;
const VisualDensity visualDensity = VisualDensity(horizontal: 1, vertical: 1);
@ -194,18 +210,18 @@ void main() {
return MaterialApp(
theme: ThemeData(
radioTheme: RadioThemeData(
mouseCursor: const MaterialStatePropertyAll<MouseCursor>(themeMouseCursor),
fillColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
mouseCursor: const WidgetStatePropertyAll<MouseCursor>(themeMouseCursor),
fillColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return themeSelectedFillColor;
}
return themeDefaultFillColor;
}),
overlayColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.focused)) {
overlayColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.focused)) {
return themeFocusOverlayColor;
}
if (states.contains(MaterialState.hovered)) {
if (states.contains(WidgetState.hovered)) {
return themeHoverOverlayColor;
}
return null;
@ -213,6 +229,12 @@ void main() {
splashRadius: themeSplashRadius,
materialTapTargetSize: themeMaterialTapTargetSize,
visualDensity: themeVisualDensity,
backgroundColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return themeSelectedBackgroundColor;
}
return themeDefaultBackgroundColor;
}),
),
),
home: Scaffold(
@ -222,8 +244,8 @@ void main() {
groupValue: 0,
autofocus: autofocus,
mouseCursor: mouseCursor,
fillColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
fillColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return selectedFillColor;
}
return defaultFillColor;
@ -233,6 +255,12 @@ void main() {
splashRadius: splashRadius,
materialTapTargetSize: materialTapTargetSize,
visualDensity: visualDensity,
backgroundColor: WidgetStateProperty.resolveWith((Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return selectedBackgroundColor;
}
return defaultBackgroundColor;
}),
),
),
);
@ -244,7 +272,7 @@ void main() {
expect(
_getRadioMaterial(tester),
paints
..circle(color: Colors.transparent)
..circle(color: defaultBackgroundColor)
..circle(color: defaultFillColor),
);
// Size from MaterialTapTargetSize.shrinkWrap with added VisualDensity.
@ -256,7 +284,7 @@ void main() {
expect(
_getRadioMaterial(tester),
paints
..circle(color: Colors.transparent)
..circle(color: selectedBackgroundColor)
..circle(color: selectedFillColor),
);