mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add backgroundColor to Radio (#169415)
Part of https://github.com/flutter/flutter/issues/168787 Adds `backgroundColor` property to `Radio` This code sample is now possible: <details> <summary>Example</summary> ```dart // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; void main() { runApp(const MyWidget()); } class MyWidget extends StatefulWidget { const MyWidget({super.key}); @override State<MyWidget> createState() => _MyWidgetState(); } class _MyWidgetState extends State<MyWidget> { bool? _value; @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Column( children: <Widget>[ Radio<bool>( value: true, groupValue: _value, toggleable: true, onChanged: (bool? value) { setState(() { // Toggle the value when the radio button is pressed _value = value; }); }, activeColor: Colors.red, fillColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) { if (states.contains(MaterialState.selected)) { return Colors.green; } return Colors.blue; // Default color when not selected }), backgroundColor: MaterialStateProperty.resolveWith<Color>(( Set<MaterialState> states, ) { if (states.contains(MaterialState.selected)) { return Colors.orange.withOpacity(0.5); } return Colors.purple.withOpacity(0.5); // Default background color when not selected }), ), Radio<bool>( value: false, groupValue: _value, toggleable: true, onChanged: (bool? value) { setState(() { // Toggle the value when the radio button is pressed _value = value; }); }, ), ], ), ), ); } } ``` </details> https://github.com/user-attachments/assets/d1a9d422-89f6-4b28-bb6c-add6ead13a21 ## 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:
parent
85867b0911
commit
b086fe7b79
@ -106,6 +106,7 @@ class Radio<T> extends StatefulWidget {
|
||||
this.autofocus = false,
|
||||
this.enabled,
|
||||
this.groupRegistry,
|
||||
this.backgroundColor,
|
||||
}) : _radioType = _RadioType.material,
|
||||
useCupertinoCheckmarkStyle = false;
|
||||
|
||||
@ -153,6 +154,7 @@ class Radio<T> extends StatefulWidget {
|
||||
this.useCupertinoCheckmarkStyle = false,
|
||||
this.enabled,
|
||||
this.groupRegistry,
|
||||
this.backgroundColor,
|
||||
}) : _radioType = _RadioType.adaptive;
|
||||
|
||||
/// {@macro flutter.widget.RawRadio.value}
|
||||
@ -398,6 +400,17 @@ class Radio<T> extends StatefulWidget {
|
||||
/// {@endtemplate}
|
||||
final bool? enabled;
|
||||
|
||||
/// The color of the background of the radio button, in all [WidgetState]s.
|
||||
///
|
||||
/// Resolves in the following states:
|
||||
/// * [WidgetState.selected].
|
||||
/// * [WidgetState.hovered].
|
||||
/// * [WidgetState.focused].
|
||||
/// * [WidgetState.disabled].
|
||||
///
|
||||
/// If null, then it is transparent in all states.
|
||||
final WidgetStateProperty<Color?>? backgroundColor;
|
||||
|
||||
@override
|
||||
State<Radio<T>> createState() => _RadioState<T>();
|
||||
}
|
||||
@ -503,6 +516,7 @@ class _RadioState<T> extends State<Radio<T>> {
|
||||
splashRadius: widget.splashRadius,
|
||||
visualDensity: widget.visualDensity,
|
||||
materialTapTargetSize: widget.materialTapTargetSize,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -538,6 +552,7 @@ class _RadioPaint extends StatefulWidget {
|
||||
required this.splashRadius,
|
||||
required this.visualDensity,
|
||||
required this.materialTapTargetSize,
|
||||
required this.backgroundColor,
|
||||
});
|
||||
|
||||
final ToggleableStateMixin toggleableState;
|
||||
@ -549,6 +564,7 @@ class _RadioPaint extends StatefulWidget {
|
||||
final double? splashRadius;
|
||||
final VisualDensity? visualDensity;
|
||||
final MaterialTapTargetSize? materialTapTargetSize;
|
||||
final MaterialStateProperty<Color?>? backgroundColor;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _RadioPaintState();
|
||||
@ -598,6 +614,11 @@ 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;
|
||||
final Color inactiveBackgroundColor =
|
||||
widget.backgroundColor?.resolve(inactiveStates) ?? Colors.transparent;
|
||||
|
||||
final Set<MaterialState> focusedStates =
|
||||
widget.toggleableState.states..add(MaterialState.focused);
|
||||
@ -675,24 +696,52 @@ class _RadioPaintState extends State<_RadioPaint> {
|
||||
..isFocused = widget.toggleableState.states.contains(MaterialState.focused)
|
||||
..isHovered = widget.toggleableState.states.contains(MaterialState.hovered)
|
||||
..activeColor = effectiveActiveColor
|
||||
..inactiveColor = effectiveInactiveColor,
|
||||
..inactiveColor = effectiveInactiveColor
|
||||
..activeBackgroundColor = activeBackgroundColor
|
||||
..inactiveBackgroundColor = inactiveBackgroundColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioPainter extends ToggleablePainter {
|
||||
Color get inactiveBackgroundColor => _inactiveBackgroundColor!;
|
||||
Color? _inactiveBackgroundColor;
|
||||
set inactiveBackgroundColor(Color? value) {
|
||||
if (_inactiveBackgroundColor == value) {
|
||||
return;
|
||||
}
|
||||
_inactiveBackgroundColor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Color get activeBackgroundColor => _activeBackgroundColor!;
|
||||
Color? _activeBackgroundColor;
|
||||
set activeBackgroundColor(Color? value) {
|
||||
if (_activeBackgroundColor == value) {
|
||||
return;
|
||||
}
|
||||
_activeBackgroundColor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero));
|
||||
|
||||
final Offset center = (Offset.zero & size).center;
|
||||
|
||||
// Outer circle
|
||||
// Background
|
||||
final Paint paint =
|
||||
Paint()
|
||||
..color = Color.lerp(inactiveColor, activeColor, position.value)!
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0;
|
||||
..color = Color.lerp(inactiveBackgroundColor, activeBackgroundColor, position.value)!
|
||||
..style = PaintingStyle.fill;
|
||||
canvas.drawCircle(center, _kOuterRadius, paint);
|
||||
|
||||
// Outer circle
|
||||
paint
|
||||
..color = Color.lerp(inactiveColor, activeColor, position.value)!
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0;
|
||||
canvas.drawCircle(center, _kOuterRadius, paint);
|
||||
|
||||
// Inner circle
|
||||
|
||||
@ -2096,4 +2096,210 @@ void main() {
|
||||
);
|
||||
focusNode.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Radio button background color resolves in enabled/disabled states', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
const Color activeEnabledBackgroundColor = Color(0xFF000001);
|
||||
const Color activeDisabledBackgroundColor = Color(0xFF000002);
|
||||
const Color inactiveEnabledBackgroundColor = Color(0xFF000003);
|
||||
const Color inactiveDisabledBackgroundColor = Color(0xFF000004);
|
||||
|
||||
Color getBackgroundColor(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return activeDisabledBackgroundColor;
|
||||
}
|
||||
return inactiveDisabledBackgroundColor;
|
||||
}
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return activeEnabledBackgroundColor;
|
||||
}
|
||||
return inactiveEnabledBackgroundColor;
|
||||
}
|
||||
|
||||
final MaterialStateProperty<Color> backgroundColor = MaterialStateColor.resolveWith(
|
||||
getBackgroundColor,
|
||||
);
|
||||
|
||||
int? groupValue = 0;
|
||||
const Key radioKey = Key('radio');
|
||||
Widget buildApp({required bool enabled}) {
|
||||
return MaterialApp(
|
||||
theme: theme,
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: Colors.white,
|
||||
child: RadioGroup<int>(
|
||||
groupValue: groupValue,
|
||||
onChanged: (int? newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
},
|
||||
child: Radio<int>(
|
||||
key: radioKey,
|
||||
value: 0,
|
||||
fillColor: backgroundColor,
|
||||
enabled: enabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp(enabled: true));
|
||||
|
||||
// Selected and enabled.
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0),
|
||||
)
|
||||
..circle(color: activeEnabledBackgroundColor),
|
||||
);
|
||||
|
||||
// Check when the radio isn't selected.
|
||||
groupValue = 1;
|
||||
await tester.pumpWidget(buildApp(enabled: true));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0),
|
||||
)
|
||||
..circle(color: inactiveEnabledBackgroundColor),
|
||||
);
|
||||
|
||||
// Check when the radio is selected, but disabled.
|
||||
groupValue = 0;
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0),
|
||||
)
|
||||
..circle(color: activeDisabledBackgroundColor),
|
||||
);
|
||||
|
||||
// Check when the radio is unselected and disabled.
|
||||
groupValue = 1;
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0),
|
||||
)
|
||||
..circle(color: inactiveDisabledBackgroundColor),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Radio fill color resolves in hovered/focused states', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'radio');
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
const Color hoveredBackgroundColor = Color(0xFF000001);
|
||||
const Color focusedBackgroundColor = Color(0xFF000002);
|
||||
|
||||
Color getBackgroundColor(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return hoveredBackgroundColor;
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return focusedBackgroundColor;
|
||||
}
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
final MaterialStateProperty<Color> backgroundColor = MaterialStateColor.resolveWith(
|
||||
getBackgroundColor,
|
||||
);
|
||||
|
||||
int? groupValue = 0;
|
||||
const Key radioKey = Key('radio');
|
||||
final ThemeData theme = ThemeData();
|
||||
Widget buildApp() {
|
||||
return MaterialApp(
|
||||
theme: theme,
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: Colors.white,
|
||||
child: RadioGroup<int>(
|
||||
groupValue: groupValue,
|
||||
onChanged: (int? newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
},
|
||||
child: Radio<int>(
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
key: radioKey,
|
||||
value: 0,
|
||||
fillColor: backgroundColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasPrimaryFocus, isTrue);
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect()
|
||||
..circle(color: theme.colorScheme.primary.withOpacity(0.1))
|
||||
..circle(color: focusedBackgroundColor),
|
||||
);
|
||||
|
||||
// Start hovering
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
addTearDown(gesture.removePointer);
|
||||
await gesture.moveTo(tester.getCenter(find.byKey(radioKey)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
Material.of(tester.element(find.byKey(radioKey))),
|
||||
paints
|
||||
..rect(
|
||||
color: const Color(0xffffffff),
|
||||
rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0),
|
||||
)
|
||||
..circle(color: theme.colorScheme.primary.withOpacity(0.08))
|
||||
..circle(color: hoveredBackgroundColor),
|
||||
);
|
||||
|
||||
focusNode.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user