Unverified Commit 5434dd59 authored by Per Classon's avatar Per Classon Committed by GitHub

[Material] Add splash radius property to selection controls (#69197)

parent 8cbc8492
......@@ -66,6 +66,7 @@ class Checkbox extends StatefulWidget {
this.checkColor,
this.focusColor,
this.hoverColor,
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.focusNode,
......@@ -173,6 +174,11 @@ class Checkbox extends StatefulWidget {
/// The color for the checkbox's [Material] when a pointer is hovering over it.
final Color? hoverColor;
/// The splash radius of the circular [Material] ink response.
///
/// If null, then [kRadialReactionRadius] is used.
final double? splashRadius;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
......@@ -237,10 +243,10 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
Size size;
switch (widget.materialTapTargetSize ?? themeData!.materialTapTargetSize) {
case MaterialTapTargetSize.padded:
size = const Size(2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0);
size = const Size(kMinInteractiveDimension, kMinInteractiveDimension);
break;
case MaterialTapTargetSize.shrinkWrap:
size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius);
size = const Size(kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0);
break;
}
size += (widget.visualDensity ?? themeData!.visualDensity).baseSizeAdjustment;
......@@ -273,6 +279,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
inactiveColor: enabled ? themeData!.unselectedWidgetColor : themeData!.disabledColor,
focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor,
splashRadius: widget.splashRadius ?? kRadialReactionRadius,
onChanged: widget.onChanged,
additionalConstraints: additionalConstraints,
vsync: this,
......@@ -295,6 +302,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
required this.inactiveColor,
required this.focusColor,
required this.hoverColor,
required this.splashRadius,
required this.onChanged,
required this.vsync,
required this.additionalConstraints,
......@@ -316,6 +324,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
final Color inactiveColor;
final Color focusColor;
final Color hoverColor;
final double splashRadius;
final ValueChanged<bool?>? onChanged;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
......@@ -329,6 +338,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
splashRadius: splashRadius,
onChanged: onChanged,
vsync: vsync,
additionalConstraints: additionalConstraints,
......@@ -348,6 +358,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
..inactiveColor = inactiveColor
..focusColor = focusColor
..hoverColor = hoverColor
..splashRadius = splashRadius
..onChanged = onChanged
..additionalConstraints = additionalConstraints
..vsync = vsync
......@@ -369,6 +380,7 @@ class _RenderCheckbox extends RenderToggleable {
required Color inactiveColor,
Color? focusColor,
Color? hoverColor,
required double splashRadius,
required BoxConstraints additionalConstraints,
ValueChanged<bool?>? onChanged,
required bool hasFocus,
......@@ -382,6 +394,7 @@ class _RenderCheckbox extends RenderToggleable {
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
splashRadius: splashRadius,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
vsync: vsync,
......
......@@ -30,7 +30,7 @@ const double kTextTabBarHeight = kMinInteractiveDimension;
/// The amount of time theme change animations should last.
const Duration kThemeChangeDuration = Duration(milliseconds: 200);
/// The radius of a circular material ink response in logical pixels.
/// The default radius of a circular material ink response in logical pixels.
const double kRadialReactionRadius = 20.0;
/// The amount of time a circular material ink response should take to expand to its full size.
......
......@@ -114,6 +114,7 @@ class Radio<T> extends StatefulWidget {
this.activeColor,
this.focusColor,
this.hoverColor,
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.focusNode,
......@@ -266,6 +267,11 @@ class Radio<T> extends StatefulWidget {
/// The color for the radio's [Material] when a pointer is hovering over it.
final Color? hoverColor;
/// The splash radius of the circular [Material] ink response.
///
/// If null, then [kRadialReactionRadius] is used.
final double? splashRadius;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
......@@ -333,10 +339,10 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
Size size;
switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) {
case MaterialTapTargetSize.padded:
size = const Size(2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0);
size = const Size(kMinInteractiveDimension, kMinInteractiveDimension);
break;
case MaterialTapTargetSize.shrinkWrap:
size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius);
size = const Size(kMinInteractiveDimension - 8.0, kMinInteractiveDimension - 8.0);
break;
}
size += (widget.visualDensity ?? themeData.visualDensity).baseSizeAdjustment;
......@@ -368,6 +374,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
inactiveColor: _getInactiveColor(themeData),
focusColor: widget.focusColor ?? themeData.focusColor,
hoverColor: widget.hoverColor ?? themeData.hoverColor,
splashRadius: widget.splashRadius ?? kRadialReactionRadius,
onChanged: enabled ? _handleChanged : null,
toggleable: widget.toggleable,
additionalConstraints: additionalConstraints,
......@@ -395,6 +402,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
required this.vsync,
required this.hasFocus,
required this.hovering,
required this.splashRadius,
}) : assert(selected != null),
assert(activeColor != null),
assert(inactiveColor != null),
......@@ -409,6 +417,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
final Color activeColor;
final Color focusColor;
final Color hoverColor;
final double splashRadius;
final ValueChanged<bool?>? onChanged;
final bool toggleable;
final TickerProvider vsync;
......@@ -421,6 +430,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
splashRadius: splashRadius,
onChanged: onChanged,
tristate: toggleable,
vsync: vsync,
......@@ -437,6 +447,7 @@ class _RadioRenderObjectWidget extends LeafRenderObjectWidget {
..inactiveColor = inactiveColor
..focusColor = focusColor
..hoverColor = hoverColor
..splashRadius = splashRadius
..onChanged = onChanged
..tristate = toggleable
..additionalConstraints = additionalConstraints
......@@ -453,6 +464,7 @@ class _RenderRadio extends RenderToggleable {
required Color inactiveColor,
required Color focusColor,
required Color hoverColor,
required double splashRadius,
required ValueChanged<bool?>? onChanged,
required bool tristate,
required BoxConstraints additionalConstraints,
......@@ -465,6 +477,7 @@ class _RenderRadio extends RenderToggleable {
inactiveColor: inactiveColor,
focusColor: focusColor,
hoverColor: hoverColor,
splashRadius: splashRadius,
onChanged: onChanged,
tristate: tristate,
additionalConstraints: additionalConstraints,
......
......@@ -21,9 +21,10 @@ const double _kTrackHeight = 14.0;
const double _kTrackWidth = 33.0;
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kThumbRadius = 10.0;
const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius;
const double _kSwitchHeight = 2 * kRadialReactionRadius + 8.0;
const double _kSwitchHeightCollapsed = 2 * kRadialReactionRadius;
const double _kSwitchMinSize = kMinInteractiveDimension - 8.0;
const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + _kSwitchMinSize;
const double _kSwitchHeight = _kSwitchMinSize + 8.0;
const double _kSwitchHeightCollapsed = _kSwitchMinSize;
enum _SwitchType { material, adaptive }
......@@ -80,6 +81,7 @@ class Switch extends StatefulWidget {
this.mouseCursor,
this.focusColor,
this.hoverColor,
this.splashRadius,
this.focusNode,
this.autofocus = false,
}) : _switchType = _SwitchType.material,
......@@ -114,6 +116,7 @@ class Switch extends StatefulWidget {
this.mouseCursor,
this.focusColor,
this.hoverColor,
this.splashRadius,
this.focusNode,
this.autofocus = false,
}) : assert(autofocus != null),
......@@ -229,6 +232,11 @@ class Switch extends StatefulWidget {
/// The color for the button's [Material] when a pointer is hovering over it.
final Color? hoverColor;
/// The splash radius of the circular [Material] ink response.
///
/// If null, then [kRadialReactionRadius] is used.
final double? splashRadius;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
......@@ -343,6 +351,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
inactiveColor: inactiveThumbColor,
hoverColor: hoverColor,
focusColor: focusColor,
splashRadius: widget.splashRadius ?? kRadialReactionRadius,
activeThumbImage: widget.activeThumbImage,
onActiveThumbImageError: widget.onActiveThumbImageError,
inactiveThumbImage: widget.inactiveThumbImage,
......@@ -413,6 +422,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
required this.inactiveColor,
required this.hoverColor,
required this.focusColor,
required this.splashRadius,
required this.activeThumbImage,
required this.onActiveThumbImageError,
required this.inactiveThumbImage,
......@@ -433,6 +443,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
final Color inactiveColor;
final Color hoverColor;
final Color focusColor;
final double splashRadius;
final ImageProvider? activeThumbImage;
final ImageErrorListener? onActiveThumbImageError;
final ImageProvider? inactiveThumbImage;
......@@ -456,6 +467,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
inactiveColor: inactiveColor,
hoverColor: hoverColor,
focusColor: focusColor,
splashRadius: splashRadius,
activeThumbImage: activeThumbImage,
onActiveThumbImageError: onActiveThumbImageError,
inactiveThumbImage: inactiveThumbImage,
......@@ -480,6 +492,7 @@ class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
..inactiveColor = inactiveColor
..hoverColor = hoverColor
..focusColor = focusColor
..splashRadius = splashRadius
..activeThumbImage = activeThumbImage
..onActiveThumbImageError = onActiveThumbImageError
..inactiveThumbImage = inactiveThumbImage
......@@ -515,6 +528,7 @@ class _RenderSwitch extends RenderToggleable {
required Color inactiveColor,
required Color hoverColor,
required Color focusColor,
required double splashRadius,
required ImageProvider? activeThumbImage,
required ImageErrorListener? onActiveThumbImageError,
required ImageProvider? inactiveThumbImage,
......@@ -545,6 +559,7 @@ class _RenderSwitch extends RenderToggleable {
inactiveColor: inactiveColor,
hoverColor: hoverColor,
focusColor: focusColor,
splashRadius: splashRadius,
onChanged: onChanged,
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
......@@ -668,7 +683,7 @@ class _RenderSwitch extends RenderToggleable {
super.detach();
}
double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
double get _trackInnerLength => size.width - _kSwitchMinSize;
late HorizontalDragGestureRecognizer _drag;
......
......@@ -13,9 +13,6 @@ import 'constants.dart';
// Duration of the animation that moves the toggle from one state to another.
const Duration _kToggleDuration = Duration(milliseconds: 200);
// Radius of the radial reaction over time.
final Animatable<double> _kRadialReactionRadiusTween = Tween<double>(begin: 0.0, end: kRadialReactionRadius);
// Duration of the fade animation for the reaction when focus and hover occur.
const Duration _kReactionFadeDuration = Duration(milliseconds: 50);
......@@ -36,6 +33,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
required Color inactiveColor,
Color? hoverColor,
Color? focusColor,
required double splashRadius,
ValueChanged<bool?>? onChanged,
required BoxConstraints additionalConstraints,
required TickerProvider vsync,
......@@ -52,6 +50,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
_inactiveColor = inactiveColor,
_hoverColor = hoverColor ?? activeColor.withAlpha(kRadialReactionAlpha),
_focusColor = focusColor ?? activeColor.withAlpha(kRadialReactionAlpha),
_splashRadius = splashRadius,
_onChanged = onChanged,
_hasFocus = hasFocus,
_hovering = hovering,
......@@ -331,6 +330,16 @@ abstract class RenderToggleable extends RenderConstrainedBox {
markNeedsPaint();
}
/// The splash radius for the radial reaction.
double get splashRadius => _splashRadius;
double _splashRadius;
set splashRadius(double value) {
if (value == _splashRadius)
return;
_splashRadius = value;
markNeedsPaint();
}
/// Called when the control changes value.
///
/// If the control is tapped, [onChanged] is called immediately with the new
......@@ -457,9 +466,13 @@ abstract class RenderToggleable extends RenderConstrainedBox {
_reactionFocusFade.value,
)!;
final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value)!;
final Animatable<double> radialReactionRadiusTween = Tween<double>(
begin: 0.0,
end: splashRadius,
);
final double reactionRadius = hasFocus || hovering
? kRadialReactionRadius
: _kRadialReactionRadiusTween.evaluate(_reaction);
? splashRadius
: radialReactionRadiusTween.evaluate(_reaction);
if (reactionRadius > 0.0) {
canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
}
......
......@@ -470,6 +470,34 @@ void main() {
);
});
testWidgets('Checkbox with splash radius set', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const double splashRadius = 30;
Widget buildApp() {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: false,
onChanged: (bool? newValue) {},
focusColor: Colors.orange[500],
autofocus: true,
splashRadius: splashRadius,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: Colors.orange[500], radius: splashRadius)
);
});
testWidgets('Checkbox can be hovered and has correct hover color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
......
......@@ -356,6 +356,42 @@ void main() {
);
});
testWidgets('Radio with splash radius set', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const double splashRadius = 30;
Widget buildApp() {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Container(
width: 100,
height: 100,
color: Colors.white,
child: Radio<int>(
value: 0,
onChanged: (int? newValue) {},
focusColor: Colors.orange[500],
autofocus: true,
groupValue: 0,
splashRadius: splashRadius,
),
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
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 is focusable and has correct focus color', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Radio');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
......
......@@ -797,6 +797,34 @@ void main() {
);
});
testWidgets('Switch with splash radius set', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const double splashRadius = 30;
Widget buildApp() {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Switch(
value: true,
onChanged: (bool newValue) {},
focusColor: Colors.orange[500],
autofocus: true,
splashRadius: splashRadius,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints..circle(color: Colors.orange[500], radius: splashRadius)
);
});
testWidgets('Switch can be hovered and has correct hover color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool value = true;
......
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