mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Support for MaterialTapTargetSize within ToggleButtons (#93259)
This commit is contained in:
parent
ca14d85cf7
commit
a395c952e9
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user