Unverified Commit a395c952 authored by Rami's avatar Rami Committed by GitHub

Support for MaterialTapTargetSize within ToggleButtons (#93259)

parent ca14d85c
......@@ -172,6 +172,7 @@ class ToggleButtons extends StatelessWidget {
required this.isSelected,
this.onPressed,
this.mouseCursor,
this.tapTargetSize,
this.textStyle,
this.constraints,
this.color,
......@@ -231,6 +232,15 @@ class ToggleButtons extends StatelessWidget {
/// {@macro flutter.material.RawMaterialButton.mouseCursor}
final MouseCursor? mouseCursor;
/// Configures the minimum size of the area within which the buttons may
/// be pressed.
///
/// If the [tapTargetSize] is larger than [constraints], the buttons will
/// include a transparent margin that responds to taps.
///
/// Defaults to [ThemeData.materialTapTargetSize].
final MaterialTapTargetSize? tapTargetSize;
/// The [TextStyle] to apply to any text in these toggle buttons.
///
/// [TextStyle.color] will be ignored and substituted by [color],
......@@ -686,7 +696,7 @@ class ToggleButtons extends StatelessWidget {
);
});
return direction == Axis.horizontal
final Widget result = direction == Axis.horizontal
? IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.min,
......@@ -702,6 +712,18 @@ class ToggleButtons extends StatelessWidget {
children: buttons,
),
);
final MaterialTapTargetSize resolvedTapTargetSize = tapTargetSize ?? theme.materialTapTargetSize;
switch (resolvedTapTargetSize) {
case MaterialTapTargetSize.padded:
return _InputPadding(
minSize: const Size(kMinInteractiveDimension, kMinInteractiveDimension),
direction: direction,
child: result,
);
case MaterialTapTargetSize.shrinkWrap:
return result;
}
}
@override
......@@ -1550,3 +1572,141 @@ class _SelectToggleButtonRenderObject extends RenderShiftedBox {
}
}
}
/// A widget to pad the area around a [ToggleButtons]'s children.
///
/// This widget is based on a similar one used in [ButtonStyleButton] but it
/// only redirects taps along one axis to ensure the correct button is tapped
/// within the [ToggleButtons].
///
/// This ensures that a widget takes up at least as much space as the minSize
/// parameter to ensure adequate tap target size, while keeping the widget
/// visually smaller to the user.
class _InputPadding extends SingleChildRenderObjectWidget {
const _InputPadding({
Key? key,
Widget? child,
required this.minSize,
required this.direction,
}) : super(key: key, child: child);
final Size minSize;
final Axis direction;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderInputPadding(minSize, direction);
}
@override
void updateRenderObject(BuildContext context, covariant _RenderInputPadding renderObject) {
renderObject.minSize = minSize;
renderObject.direction = direction;
}
}
class _RenderInputPadding extends RenderShiftedBox {
_RenderInputPadding(this._minSize, this._direction, [RenderBox? child]) : super(child);
Size get minSize => _minSize;
Size _minSize;
set minSize(Size value) {
if (_minSize == value)
return;
_minSize = value;
markNeedsLayout();
}
Axis get direction => _direction;
Axis _direction;
set direction(Axis value) {
if (_direction == value)
return;
_direction = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
if (child != null)
return math.max(child!.getMinIntrinsicWidth(height), minSize.width);
return 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null)
return math.max(child!.getMinIntrinsicHeight(width), minSize.height);
return 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null)
return math.max(child!.getMaxIntrinsicWidth(height), minSize.width);
return 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null)
return math.max(child!.getMaxIntrinsicHeight(width), minSize.height);
return 0.0;
}
Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
if (child != null) {
final Size childSize = layoutChild(child!, constraints);
final double height = math.max(childSize.width, minSize.width);
final double width = math.max(childSize.height, minSize.height);
return constraints.constrain(Size(height, width));
}
return Size.zero;
}
@override
Size computeDryLayout(BoxConstraints constraints) {
return _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.dryLayoutChild,
);
}
@override
void performLayout() {
size = _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.layoutChild,
);
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = Alignment.center.alongOffset(size - child!.size as Offset);
}
}
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
// The super.hitTest() method also checks hitTestChildren(). We don't
// want that in this case because we've padded around the children per
// tapTargetSize.
if (!size.contains(position)) {
return false;
}
// Only adjust one axis to ensure the correct button is tapped.
Offset center;
if (direction == Axis.horizontal) {
center = Offset(position.dx, child!.size.height / 2);
} else {
center = Offset(child!.size.width / 2, position.dy);
}
return result.addWithRawTransform(
transform: MatrixUtils.forceToPoint(center),
position: center,
hitTest: (BoxHitTestResult result, Offset position) {
assert(position == center);
return child!.hitTest(result, position: center);
},
);
}
}
......@@ -1591,6 +1591,96 @@ void main() {
},
);
testWidgets('Tap target size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) {
return Theme(
data: ThemeData(materialTapTargetSize: tapTargetSize),
child: Material(
child: boilerplate(
child: ToggleButtons(
key: key,
constraints: const BoxConstraints(minWidth: 32.0, minHeight: 32.0),
isSelected: const <bool>[false, true, false],
onPressed: (int index) {},
children: const <Widget>[
Text('First'),
Text('Second'),
Text('Third'),
],
),
),
),
);
}
final Key key1 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1));
expect(tester.getSize(find.byKey(key1)), const Size(228.0, 48.0));
final Key key2 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2));
expect(tester.getSize(find.byKey(key2)), const Size(228.0, 34.0));
});
testWidgets('Tap target size is configurable', (WidgetTester tester) async {
Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) {
return Material(
child: boilerplate(
child: ToggleButtons(
key: key,
tapTargetSize: tapTargetSize,
constraints: const BoxConstraints(minWidth: 32.0, minHeight: 32.0),
isSelected: const <bool>[false, true, false],
onPressed: (int index) {},
children: const <Widget>[
Text('First'),
Text('Second'),
Text('Third'),
],
),
),
);
}
final Key key1 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1));
expect(tester.getSize(find.byKey(key1)), const Size(228.0, 48.0));
final Key key2 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2));
expect(tester.getSize(find.byKey(key2)), const Size(228.0, 34.0));
});
testWidgets('Tap target size is configurable for vertical axis', (WidgetTester tester) async {
Widget buildFrame(MaterialTapTargetSize tapTargetSize, Key key) {
return Material(
child: boilerplate(
child: ToggleButtons(
key: key,
tapTargetSize: tapTargetSize,
constraints: const BoxConstraints(minWidth: 32.0, minHeight: 32.0),
direction: Axis.vertical,
isSelected: const <bool>[false, true, false],
onPressed: (int index) {},
children: const <Widget>[
Text('1'),
Text('2'),
Text('3'),
],
),
),
);
}
final Key key1 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.padded, key1));
expect(tester.getSize(find.byKey(key1)), const Size(48.0, 100.0));
final Key key2 = UniqueKey();
await tester.pumpWidget(buildFrame(MaterialTapTargetSize.shrinkWrap, key2));
expect(tester.getSize(find.byKey(key2)), const Size(34.0, 100.0));
});
// Regression test for https://github.com/flutter/flutter/issues/73725
testWidgets('Border radius paint test when there is only one button', (WidgetTester tester) async {
final ThemeData theme = ThemeData();
......
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