Unverified Commit 51227a9a authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Add missing parameters to `RadioListTile` (#120117)

parent 3a514175
......@@ -264,10 +264,12 @@ class Radio<T> extends StatefulWidget {
/// [ThemeData.focusColor] is used.
final Color? focusColor;
/// {@template flutter.material.radio.hoverColor}
/// The color for the radio's [Material] when a pointer is hovering over it.
///
/// If [overlayColor] returns a non-null color in the [MaterialState.hovered]
/// state, it will be used instead.
/// {@endtemplate}
///
/// If null, then the value of [RadioThemeData.overlayColor] is used in the
/// hovered state. If that is also null, then the value of
......@@ -275,7 +277,7 @@ class Radio<T> extends StatefulWidget {
final Color? hoverColor;
/// {@template flutter.material.radio.overlayColor}
/// The color for the checkbox's [Material].
/// The color for the radio's [Material].
///
/// Resolves in the following states:
/// * [MaterialState.pressed].
......
......@@ -162,8 +162,14 @@ class RadioListTile<T> extends StatelessWidget {
required this.value,
required this.groupValue,
required this.onChanged,
this.mouseCursor,
this.toggleable = false,
this.activeColor,
this.fillColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.materialTapTargetSize,
this.title,
this.subtitle,
this.isThreeLine = false,
......@@ -220,6 +226,20 @@ class RadioListTile<T> extends StatelessWidget {
/// ```
final ValueChanged<T?>? onChanged;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.disabled].
///
/// If null, then the value of [RadioThemeData.mouseCursor] is used.
/// If that is also null, then [MaterialStateMouseCursor.clickable] is used.
final MouseCursor? mouseCursor;
/// Set to true if this radio list tile is allowed to be returned to an
/// indeterminate state by selecting it again when selected.
///
......@@ -250,6 +270,45 @@ class RadioListTile<T> extends StatelessWidget {
/// Defaults to [ColorScheme.secondary] of the current [Theme].
final Color? activeColor;
/// The color that fills the radio button.
///
/// Resolves in the following states:
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.disabled].
///
/// If null, then the value of [activeColor] is used in the selected state. If
/// that is also null, then the value of [RadioThemeData.fillColor] is used.
/// If that is also null, then the default value is used.
final MaterialStateProperty<Color?>? fillColor;
/// {@macro flutter.material.radio.materialTapTargetSize}
///
/// Defaults to [MaterialTapTargetSize.shrinkWrap].
final MaterialTapTargetSize? materialTapTargetSize;
/// {@macro flutter.material.radio.hoverColor}
final Color? hoverColor;
/// The color for the radio's [Material].
///
/// Resolves in the following states:
/// * [MaterialState.pressed].
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
///
/// If null, then the value of [activeColor] with alpha [kRadialReactionAlpha]
/// and [hoverColor] is used in the pressed and hovered state. If that is also
/// null, the value of [SwitchThemeData.overlayColor] is used. If that is
/// also null, then the default value is used in the pressed and hovered state.
final MaterialStateProperty<Color?>? overlayColor;
/// {@macro flutter.material.radio.splashRadius}
///
/// If null, then the value of [RadioThemeData.splashRadius] is used. If that
/// is also null, then [kRadialReactionRadius] is used.
final double? splashRadius;
/// The primary content of the list tile.
///
/// Typically a [Text] widget.
......@@ -341,8 +400,13 @@ class RadioListTile<T> extends StatelessWidget {
onChanged: onChanged,
toggleable: toggleable,
activeColor: activeColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
autofocus: autofocus,
fillColor: fillColor,
mouseCursor: mouseCursor,
hoverColor: hoverColor,
overlayColor: overlayColor,
splashRadius: splashRadius,
);
Widget? leading, trailing;
switch (controlAffinity) {
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
......@@ -823,6 +824,423 @@ void main() {
expect(node.hasFocus, isFalse);
});
testWidgets('Radio changes mouse cursor when hovered', (WidgetTester tester) async {
// Test Radio() constructor
await tester.pumpWidget(
wrap(child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RadioListTile<int>(
mouseCursor: SystemMouseCursors.text,
value: 1,
onChanged: (int? v) {},
groupValue: 2,
),
)),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(Radio<int>)));
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
wrap(child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RadioListTile<int>(
value: 1,
onChanged: (int? v) {},
groupValue: 2,
),
)),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(wrap(
child: const MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RadioListTile<int>(
value: 1,
onChanged: null,
groupValue: 2,
),
),
),);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
});
testWidgets('RadioListTile respects fillColor in enabled/disabled states', (WidgetTester tester) async {
const Color activeEnabledFillColor = Color(0xFF000001);
const Color activeDisabledFillColor = Color(0xFF000002);
const Color inactiveEnabledFillColor = Color(0xFF000003);
const Color inactiveDisabledFillColor = Color(0xFF000004);
Color getFillColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return activeDisabledFillColor;
}
return inactiveDisabledFillColor;
}
if (states.contains(MaterialState.selected)) {
return activeEnabledFillColor;
}
return inactiveEnabledFillColor;
}
final MaterialStateProperty<Color> fillColor =
MaterialStateColor.resolveWith(getFillColor);
int? groupValue = 0;
Widget buildApp({required bool enabled}) {
return wrap(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return RadioListTile<int>(
value: 0,
fillColor: fillColor,
onChanged: enabled ? (int? newValue) {
setState(() {
groupValue = newValue;
});
} : null,
groupValue: groupValue,
);
})
);
}
await tester.pumpWidget(buildApp(enabled: true));
// Selected and enabled.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<int>))),
paints
..rect()
..circle(color: activeEnabledFillColor)
..circle(color: activeEnabledFillColor),
);
// Check when the radio isn't selected.
groupValue = 1;
await tester.pumpWidget(buildApp(enabled: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<int>))),
paints
..rect()
..circle(color: inactiveEnabledFillColor, style: PaintingStyle.stroke, strokeWidth: 2.0),
);
// 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.byType(Radio<int>))),
paints
..rect()
..circle(color: activeDisabledFillColor)
..circle(color: activeDisabledFillColor),
);
// 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.byType(Radio<int>))),
paints
..rect()
..circle(color: inactiveDisabledFillColor, style: PaintingStyle.stroke, strokeWidth: 2.0),
);
});
testWidgets('RadioListTile respects fillColor in hovered state', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color hoveredFillColor = Color(0xFF000001);
Color getFillColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveredFillColor;
}
return Colors.transparent;
}
final MaterialStateProperty<Color> fillColor =
MaterialStateColor.resolveWith(getFillColor);
int? groupValue = 0;
Widget buildApp() {
return wrap(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return RadioListTile<int>(
value: 0,
fillColor: fillColor,
onChanged: (int? newValue) {
setState(() {
groupValue = newValue;
});
},
groupValue: groupValue,
);
}),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Radio<int>)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<int>))),
paints
..rect()..circle()
..circle(color: hoveredFillColor),
);
});
testWidgets('RadioListTile respects hoverColor', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
int? groupValue = 0;
final Color? hoverColor = Colors.orange[500];
Widget buildApp({bool enabled = true}) {
return wrap(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return RadioListTile<int>(
value: 0,
onChanged: enabled ? (int? newValue) {
setState(() {
groupValue = newValue;
});
} : null,
hoverColor: hoverColor,
groupValue: groupValue,
);
}),
);
}
await tester.pumpWidget(buildApp());
await tester.pump();
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<int>))),
paints
..rect()
..circle(color: const Color(0xff2196f3))
..circle(color: const Color(0xff2196f3)),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.moveTo(tester.getCenter(find.byType(Radio<int>)));
// Check when the radio isn't selected.
groupValue = 1;
await tester.pumpWidget(buildApp());
await tester.pump();
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<int>))),
paints
..rect()
..circle(color: hoverColor)
);
// Check when the radio is selected, but disabled.
groupValue = 0;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pump();
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<int>))),
paints
..rect()
..circle(color: const Color(0x61000000))
..circle(color: const Color(0x61000000)),
);
});
testWidgets('RadioListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color fillColor = Color(0xFF000000);
const Color activePressedOverlayColor = Color(0xFF000001);
const Color inactivePressedOverlayColor = Color(0xFF000002);
const Color hoverOverlayColor = Color(0xFF000003);
const Color hoverColor = Color(0xFF000005);
Color? getOverlayColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
if (states.contains(MaterialState.selected)) {
return activePressedOverlayColor;
}
return inactivePressedOverlayColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverOverlayColor;
}
return null;
}
Widget buildRadio({bool active = false, bool useOverlay = true}) {
return wrap(
child: RadioListTile<bool>(
value: active,
groupValue: true,
onChanged: (_) { },
fillColor: const MaterialStatePropertyAll<Color>(fillColor),
overlayColor: useOverlay ? MaterialStateProperty.resolveWith(getOverlayColor) : null,
hoverColor: hoverColor,
),
);
}
await tester.pumpWidget(buildRadio(useOverlay: false));
await tester.press(find.byType(Radio<bool>));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<bool>))),
paints..circle()
..circle(
color: fillColor.withAlpha(kRadialReactionAlpha),
radius: 20,
),
reason: 'Default inactive pressed Radio should have overlay color from fillColor',
);
await tester.pumpWidget(buildRadio(active: true, useOverlay: false));
await tester.press(find.byType(Radio<bool>));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<bool>))),
paints..circle()
..circle(
color: fillColor.withAlpha(kRadialReactionAlpha),
radius: 20,
),
reason: 'Default active pressed Radio should have overlay color from fillColor',
);
await tester.pumpWidget(buildRadio());
await tester.press(find.byType(Radio<bool>));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<bool>))),
paints..circle()
..circle(
color: inactivePressedOverlayColor,
radius: 20,
),
reason: 'Inactive pressed Radio should have overlay color: $inactivePressedOverlayColor',
);
await tester.pumpWidget(buildRadio(active: true));
await tester.press(find.byType(Radio<bool>));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<bool>))),
paints..circle()
..circle(
color: activePressedOverlayColor,
radius: 20,
),
reason: 'Active pressed Radio should have overlay color: $activePressedOverlayColor',
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Radio<bool>)));
await tester.pumpAndSettle();
await tester.pumpWidget(Container());
await tester.pumpWidget(buildRadio());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Radio<bool>))),
paints
..circle(
color: hoverOverlayColor,
radius: 20,
),
reason: 'Hovered Radio should use overlay color $hoverOverlayColor over $hoverColor',
);
});
testWidgets('RadioListTile respects splashRadius', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const double splashRadius = 30;
Widget buildApp() {
return wrap(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return RadioListTile<int>(
value: 0,
onChanged: (_) {},
hoverColor: Colors.orange[500],
groupValue: 0,
splashRadius: splashRadius,
);
}),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Radio<int>)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(
find.byWidgetPredicate((Widget widget) => widget is Radio<int>),
)),
paints..circle(color: Colors.orange[500], radius: splashRadius),
);
});
testWidgets('Radio respects materialTapTargetSize', (WidgetTester tester) async {
await tester.pumpWidget(
wrap(child: RadioListTile<bool>(
groupValue: true,
value: true,
onChanged: (bool? newValue) { },
)),
);
// default test
expect(tester.getSize(find.byType(Radio<bool>)), const Size(40.0, 40.0));
await tester.pumpWidget(
wrap(child: RadioListTile<bool>(
materialTapTargetSize: MaterialTapTargetSize.padded,
groupValue: true,
value: true,
onChanged: (bool? newValue) { },
)),
);
expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0));
});
group('feedback', () {
late FeedbackTester feedback;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment