Skip to content

Commit 989f574

Browse files
author
Jonah Williams
authored
Add flag to ThemeData to expand tap targets of certain material widgets (#18369)
1 parent 6172e23 commit 989f574

26 files changed

Lines changed: 1104 additions & 134 deletions

packages/flutter/lib/src/material/button.dart

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/foundation.dart';
6+
import 'package:flutter/rendering.dart';
67
import 'package:flutter/widgets.dart';
78

89
import 'button_theme.dart';
@@ -11,6 +12,7 @@ import 'constants.dart';
1112
import 'ink_well.dart';
1213
import 'material.dart';
1314
import 'theme.dart';
15+
import 'theme_data.dart';
1416

1517
/// Creates a button based on [Semantics], [Material], and [InkWell]
1618
/// widgets.
@@ -38,13 +40,14 @@ class RawMaterialButton extends StatefulWidget {
3840
this.elevation = 2.0,
3941
this.highlightElevation = 8.0,
4042
this.disabledElevation = 0.0,
41-
this.outerPadding,
4243
this.padding = EdgeInsets.zero,
4344
this.constraints = const BoxConstraints(minWidth: 88.0, minHeight: 36.0),
4445
this.shape = const RoundedRectangleBorder(),
4546
this.animationDuration = kThemeChangeDuration,
47+
MaterialTapTargetSize materialTapTargetSize,
4648
this.child,
47-
}) : assert(shape != null),
49+
}) : this.materialTapTargetSize = materialTapTargetSize ?? MaterialTapTargetSize.padded,
50+
assert(shape != null),
4851
assert(elevation != null),
4952
assert(highlightElevation != null),
5053
assert(disabledElevation != null),
@@ -58,10 +61,6 @@ class RawMaterialButton extends StatefulWidget {
5861
/// If this is set to null, the button will be disabled, see [enabled].
5962
final VoidCallback onPressed;
6063

61-
/// Padding to increase the size of the gesture detector which doesn't
62-
/// increase the visible material of the button.
63-
final EdgeInsets outerPadding;
64-
6564
/// Called by the underlying [InkWell] widget's [InkWell.onHighlightChanged]
6665
/// callback.
6766
final ValueChanged<bool> onHighlightChanged;
@@ -138,6 +137,15 @@ class RawMaterialButton extends StatefulWidget {
138137
/// property to a non-null value.
139138
bool get enabled => onPressed != null;
140139

140+
/// Configures the minimum size of the tap target.
141+
///
142+
/// Defaults to [MaterialTapTargetSize.padded].
143+
///
144+
/// See also:
145+
///
146+
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
147+
final MaterialTapTargetSize materialTapTargetSize;
148+
141149
@override
142150
_RawMaterialButtonState createState() => new _RawMaterialButtonState();
143151
}
@@ -186,18 +194,23 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
186194
),
187195
),
188196
);
189-
190-
if (widget.outerPadding != null) {
191-
result = new GestureDetector(
192-
behavior: HitTestBehavior.translucent,
193-
excludeFromSemantics: true,
194-
onTap: widget.onPressed,
195-
child: new Padding(
196-
padding: widget.outerPadding,
197-
child: result
198-
),
199-
);
197+
BoxConstraints constraints;
198+
switch (widget.materialTapTargetSize) {
199+
case MaterialTapTargetSize.padded:
200+
constraints = const BoxConstraints(minWidth: 48.0, minHeight: 48.0);
201+
break;
202+
case MaterialTapTargetSize.shrinkWrap:
203+
constraints = const BoxConstraints();
204+
break;
200205
}
206+
result = new _ButtonRedirectingHitDetectionWidget(
207+
constraints: constraints,
208+
child: new Center(
209+
child: result,
210+
widthFactor: 1.0,
211+
heightFactor: 1.0,
212+
),
213+
);
201214

202215
return new Semantics(
203216
container: true,
@@ -248,6 +261,7 @@ class MaterialButton extends StatelessWidget {
248261
this.minWidth,
249262
this.height,
250263
this.padding,
264+
this.materialTapTargetSize,
251265
@required this.onPressed,
252266
this.child
253267
}) : super(key: key);
@@ -353,6 +367,15 @@ class MaterialButton extends StatelessWidget {
353367
/// {@macro flutter.widgets.child}
354368
final Widget child;
355369

370+
/// Configures the minimum size of the tap target.
371+
///
372+
/// Defaults to [ThemeData.materialTapTargetSize].
373+
///
374+
/// See also:
375+
///
376+
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
377+
final MaterialTapTargetSize materialTapTargetSize;
378+
356379
/// Whether the button is enabled or disabled. Buttons are disabled by default. To
357380
/// enable a button, set its [onPressed] property to a non-null value.
358381
bool get enabled => onPressed != null;
@@ -412,6 +435,7 @@ class MaterialButton extends StatelessWidget {
412435
),
413436
shape: buttonTheme.shape,
414437
child: child,
438+
materialTapTargetSize: materialTapTargetSize ?? theme.materialTapTargetSize,
415439
);
416440
}
417441

@@ -421,3 +445,38 @@ class MaterialButton extends StatelessWidget {
421445
properties.add(new FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
422446
}
423447
}
448+
449+
/// Redirects the position passed to [RenderBox.hitTest] to the center of the widget.
450+
///
451+
/// The primary purpose of this widget is to allow padding around [Material] widgets
452+
/// to trigger the child ink feature without increasing the size of the material.
453+
class _ButtonRedirectingHitDetectionWidget extends SingleChildRenderObjectWidget {
454+
const _ButtonRedirectingHitDetectionWidget({
455+
Key key,
456+
Widget child,
457+
this.constraints
458+
}) : super(key: key, child: child);
459+
460+
final BoxConstraints constraints;
461+
462+
@override
463+
RenderObject createRenderObject(BuildContext context) {
464+
return new _RenderButtonRedirectingHitDetection(constraints);
465+
}
466+
467+
@override
468+
void updateRenderObject(BuildContext context, covariant _RenderButtonRedirectingHitDetection renderObject) {
469+
renderObject.additionalConstraints = constraints;
470+
}
471+
}
472+
473+
class _RenderButtonRedirectingHitDetection extends RenderConstrainedBox {
474+
_RenderButtonRedirectingHitDetection (BoxConstraints additionalConstraints) : super(additionalConstraints: additionalConstraints);
475+
476+
@override
477+
bool hitTest(HitTestResult result, {Offset position}) {
478+
if (!size.contains(position))
479+
return false;
480+
return child.hitTest(result, position: size.center(Offset.zero));
481+
}
482+
}

packages/flutter/lib/src/material/checkbox.dart

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart';
1010
import 'constants.dart';
1111
import 'debug.dart';
1212
import 'theme.dart';
13+
import 'theme_data.dart';
1314
import 'toggleable.dart';
1415

1516
/// A material design checkbox.
@@ -58,6 +59,7 @@ class Checkbox extends StatefulWidget {
5859
this.tristate = false,
5960
@required this.onChanged,
6061
this.activeColor,
62+
this.materialTapTargetSize,
6163
}) : assert(tristate != null),
6264
assert(tristate || value != null),
6365
super(key: key);
@@ -113,6 +115,15 @@ class Checkbox extends StatefulWidget {
113115
/// If tristate is false (the default), [value] must not be null.
114116
final bool tristate;
115117

118+
/// Configures the minimum size of the tap target.
119+
///
120+
/// Defaults to [ThemeData.materialTapTargetSize].
121+
///
122+
/// See also:
123+
///
124+
/// * [MaterialTapTargetSize], for a description of how this affects tap targets.
125+
final MaterialTapTargetSize materialTapTargetSize;
126+
116127
/// The width of a checkbox widget.
117128
static const double width = 18.0;
118129

@@ -125,12 +136,23 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
125136
Widget build(BuildContext context) {
126137
assert(debugCheckHasMaterial(context));
127138
final ThemeData themeData = Theme.of(context);
139+
Size size;
140+
switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) {
141+
case MaterialTapTargetSize.padded:
142+
size = const Size(2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0);
143+
break;
144+
case MaterialTapTargetSize.shrinkWrap:
145+
size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius);
146+
break;
147+
}
148+
final BoxConstraints additionalConstraints = new BoxConstraints.tight(size);
128149
return new _CheckboxRenderObjectWidget(
129150
value: widget.value,
130151
tristate: widget.tristate,
131152
activeColor: widget.activeColor ?? themeData.toggleableActiveColor,
132153
inactiveColor: widget.onChanged != null ? themeData.unselectedWidgetColor : themeData.disabledColor,
133154
onChanged: widget.onChanged,
155+
additionalConstraints: additionalConstraints,
134156
vsync: this,
135157
);
136158
}
@@ -145,6 +167,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
145167
@required this.inactiveColor,
146168
@required this.onChanged,
147169
@required this.vsync,
170+
@required this.additionalConstraints,
148171
}) : assert(tristate != null),
149172
assert(tristate || value != null),
150173
assert(activeColor != null),
@@ -158,6 +181,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
158181
final Color inactiveColor;
159182
final ValueChanged<bool> onChanged;
160183
final TickerProvider vsync;
184+
final BoxConstraints additionalConstraints;
161185

162186
@override
163187
_RenderCheckbox createRenderObject(BuildContext context) => new _RenderCheckbox(
@@ -167,6 +191,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
167191
inactiveColor: inactiveColor,
168192
onChanged: onChanged,
169193
vsync: vsync,
194+
additionalConstraints: additionalConstraints,
170195
);
171196

172197
@override
@@ -177,6 +202,7 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
177202
..activeColor = activeColor
178203
..inactiveColor = inactiveColor
179204
..onChanged = onChanged
205+
..additionalConstraints = additionalConstraints
180206
..vsync = vsync;
181207
}
182208
}
@@ -191,6 +217,7 @@ class _RenderCheckbox extends RenderToggleable {
191217
bool tristate,
192218
Color activeColor,
193219
Color inactiveColor,
220+
BoxConstraints additionalConstraints,
194221
ValueChanged<bool> onChanged,
195222
@required TickerProvider vsync,
196223
}): _oldValue = value,
@@ -200,7 +227,7 @@ class _RenderCheckbox extends RenderToggleable {
200227
activeColor: activeColor,
201228
inactiveColor: inactiveColor,
202229
onChanged: onChanged,
203-
size: const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius),
230+
additionalConstraints: additionalConstraints,
204231
vsync: vsync,
205232
);
206233

packages/flutter/lib/src/material/checkbox_list_tile.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
77
import 'checkbox.dart';
88
import 'list_tile.dart';
99
import 'theme.dart';
10+
import 'theme_data.dart';
1011

1112
/// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label.
1213
///
@@ -173,6 +174,7 @@ class CheckboxListTile extends StatelessWidget {
173174
value: value,
174175
onChanged: onChanged,
175176
activeColor: activeColor,
177+
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
176178
);
177179
Widget leading, trailing;
178180
switch (controlAffinity) {

0 commit comments

Comments
 (0)