Add 'direction' allow to 'SegmentedButton' oriented vertically (#150903)

This PR add the ability to change buttons of 'SegmentedButton' directionality (In the vertical and horizontal axis) to be 'vertical' or 'horizontal' instead of just horizontally position by adding "direction" argument.

`direction: Axis.horizontal` :
![Simulator Screenshot - iPhone 15 - 2024-06-26 at 13 37 26](https://github.com/flutter/flutter/assets/9139030/4936b7f8-246b-41ae-ac1c-7c75bc2d4f2d)

`direction: Axis.vertical` :
![Simulator Screenshot - iPhone 15 - 2024-06-26 at 13 43 07](https://github.com/flutter/flutter/assets/9139030/5aecf229-34d8-4608-a0f7-aee5c130257f)

Notice: in this example i used:
`style: ButtonStyle( shape: MaterialStateProperty.all<RoundedRectangleBorder>( const RoundedRectangleBorder( borderRadius: BorderRadius.zero, ), ), ) `
To change the Radius of `SegmentedButton`, and the default shape will be like:
![Simulator Screenshot - iPhone 15 - 2024-06-26 at 13 51 46](https://github.com/flutter/flutter/assets/9139030/24833153-02c8-4f5c-8c50-5a0effa19e9e)
I keep it as it is right now, cause its not the main purpose of this BR.

*List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.*

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
Fixes: #150416
This commit is contained in:
abdalmonem 2024-09-11 20:59:36 +03:00 committed by GitHub
parent 6e46ee8d6d
commit 3e4d59eae1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 166 additions and 13 deletions

View file

@ -141,6 +141,7 @@ class SegmentedButton<T> extends StatefulWidget {
this.style,
this.showSelectedIcon = true,
this.selectedIcon,
this.direction = Axis.horizontal,
}) : assert(segments.length > 0),
assert(selected.length > 0 || emptySelectionAllowed),
assert(selected.length < 2 || multiSelectionEnabled);
@ -154,6 +155,14 @@ class SegmentedButton<T> extends StatefulWidget {
/// [ChoiceChip] widgets.
final List<ButtonSegment<T>> segments;
/// The orientation of the button's [segments].
///
/// If this is [Axis.vertical], the segments will be aligned vertically
/// and the first item in [segments] will be on the top.
///
/// Defaults to [Axis.horizontal].
final Axis direction;
/// The set of [ButtonSegment.value]s that indicate which [segments] are
/// selected.
///
@ -449,7 +458,7 @@ class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
Widget build(BuildContext context) {
final SegmentedButtonThemeData theme = SegmentedButtonTheme.of(context);
final SegmentedButtonThemeData defaults = _SegmentedButtonDefaultsM3(context);
final TextDirection direction = Directionality.of(context);
final TextDirection textDirection = Directionality.of(context);
const Set<MaterialState> enabledState = <MaterialState>{};
const Set<MaterialState> disabledState = <MaterialState>{ MaterialState.disabled };
@ -576,7 +585,8 @@ class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
segments: widget.segments,
enabledBorder: _enabled ? enabledBorder : disabledBorder,
disabledBorder: disabledBorder,
direction: direction,
direction: widget.direction,
textDirection: textDirection,
isExpanded: widget.expandedInsets != null,
children: buttons,
),
@ -601,6 +611,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
required this.enabledBorder,
required this.disabledBorder,
required this.direction,
required this.textDirection,
required this.tapTargetVerticalPadding,
required this.isExpanded,
required super.children,
@ -609,7 +620,8 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
final List<ButtonSegment<T>> segments;
final OutlinedBorder enabledBorder;
final OutlinedBorder disabledBorder;
final TextDirection direction;
final Axis direction;
final TextDirection textDirection;
final double tapTargetVerticalPadding;
final bool isExpanded;
@ -619,7 +631,8 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
segments: segments,
enabledBorder: enabledBorder,
disabledBorder: disabledBorder,
textDirection: direction,
textDirection: textDirection,
direction: direction,
tapTargetVerticalPadding: tapTargetVerticalPadding,
isExpanded: isExpanded,
);
@ -631,7 +644,8 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
..segments = segments
..enabledBorder = enabledBorder
..disabledBorder = disabledBorder
..textDirection = direction;
..direction = direction
..textDirection = textDirection;
}
}
@ -651,10 +665,12 @@ class _RenderSegmentedButton<T> extends RenderBox with
required TextDirection textDirection,
required double tapTargetVerticalPadding,
required bool isExpanded,
required Axis direction,
}) : _segments = segments,
_enabledBorder = enabledBorder,
_disabledBorder = disabledBorder,
_textDirection = textDirection,
_direction = direction,
_tapTargetVerticalPadding = tapTargetVerticalPadding,
_isExpanded = isExpanded;
@ -698,6 +714,16 @@ class _RenderSegmentedButton<T> extends RenderBox with
markNeedsLayout();
}
Axis get direction => _direction;
Axis _direction;
set direction(Axis value) {
if (value == _direction) {
return;
}
_direction = value;
markNeedsLayout();
}
double get tapTargetVerticalPadding => _tapTargetVerticalPadding;
double _tapTargetVerticalPadding;
set tapTargetVerticalPadding(double value) {
@ -787,17 +813,28 @@ class _RenderSegmentedButton<T> extends RenderBox with
double start = 0.0;
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
final Offset childOffset = Offset(start, 0.0);
childParentData.offset = childOffset;
final Rect childRect = Rect.fromLTWH(start, 0.0, child.size.width, child.size.height);
final RRect rChildRect = RRect.fromRectAndCorners(childRect);
late final RRect rChildRect;
if (direction == Axis.vertical) {
childParentData.offset = Offset(0.0, start);
final Rect childRect = Rect.fromLTWH(0.0, childParentData.offset.dy, child.size.width, child.size.height);
rChildRect = RRect.fromRectAndCorners(childRect);
start += child.size.height;
} else {
childParentData.offset = Offset(start, 0.0);
final Rect childRect = Rect.fromLTWH(start, 0.0, child.size.width, child.size.height);
rChildRect = RRect.fromRectAndCorners(childRect);
start += child.size.width;
}
childParentData.surroundingRect = rChildRect;
start += child.size.width;
child = nextChild(child);
}
}
Size _calculateChildSize(BoxConstraints constraints) {
return direction == Axis.horizontal ? _calculateHorizontalChildSize(constraints) : _calculateVerticalChildSize(constraints);
}
Size _calculateHorizontalChildSize(BoxConstraints constraints) {
double maxHeight = 0;
RenderBox? child = firstChild;
double childWidth;
@ -820,7 +857,33 @@ class _RenderSegmentedButton<T> extends RenderBox with
return Size(childWidth, maxHeight);
}
Size _calculateVerticalChildSize(BoxConstraints constraints) {
double maxWidth = 0;
RenderBox? child = firstChild;
double childHeight;
if (_isExpanded) {
childHeight = constraints.maxHeight / childCount;
} else {
childHeight = constraints.minHeight / childCount;
while (child != null) {
childHeight = math.max(childHeight, child.getMaxIntrinsicHeight(double.infinity));
child = childAfter(child);
}
childHeight = math.min(childHeight, constraints.maxHeight / childCount);
}
child = firstChild;
while (child != null) {
final double boxWidth = child.getMaxIntrinsicWidth(maxWidth);
maxWidth = math.max(maxWidth, boxWidth);
child = childAfter(child);
}
return Size(maxWidth, childHeight);
}
Size _computeOverallSizeFromChildSize(Size childSize) {
if (direction == Axis.vertical) {
return constraints.constrain(Size(childSize.width, childSize.height * childCount));
}
return constraints.constrain(Size(childSize.width * childCount, childSize.height));
}
@ -926,9 +989,17 @@ class _RenderSegmentedButton<T> extends RenderBox with
final BorderSide divider = segments[index - 1].enabled || segments[index].enabled
? enabledBorder.side.copyWith(strokeAlign: 0.0)
: disabledBorder.side.copyWith(strokeAlign: 0.0);
final Offset top = Offset(dividerPos, borderRect.top);
final Offset bottom = Offset(dividerPos, borderRect.bottom);
context.canvas.drawLine(top, bottom, divider.toPaint());
if (direction == Axis.horizontal) {
final Offset top = Offset(dividerPos, borderRect.top);
final Offset bottom = Offset(dividerPos, borderRect.bottom);
context.canvas.drawLine(top, bottom, divider.toPaint());
} else if (direction == Axis.vertical) {
final Offset start = Offset(borderRect.left, childRect.top);
final Offset end = Offset(borderRect.right, childRect.top);
context.canvas..save()..clipPath(borderClipPath);
context.canvas.drawLine(start, end, divider.toPaint());
context.canvas.restore();
}
}
previousChild = child;

View file

@ -4,6 +4,8 @@
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;
import 'dart:ui';
import 'package:flutter/foundation.dart';
@ -1122,6 +1124,86 @@ void main() {
),
);
}, skip: kIsWeb && !isSkiaWeb); // https://github.com/flutter/flutter/issues/99933
testWidgets('SegmentedButton vertical aligned children', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SegmentedButton<int>(
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(
value: 0,
label: Text('Option 0'),
),
ButtonSegment<int>(
value: 1,
label: Text('Option 1'),
),
ButtonSegment<int>(
value: 2,
label: Text('Option 2'),
),
ButtonSegment<int>(
value: 3,
label: Text('Option 3'),
),
],
onSelectionChanged: (Set<int> selected) {},
selected: const <int>{-1}, // Prevent any of ButtonSegment to be selected
direction: Axis.vertical,
),
),
),
),
);
Rect? previewsChildRect;
for (int i = 0; i <= 3; i++) {
final Rect currentChildRect = tester.getRect(find.widgetWithText(TextButton, 'Option $i'));
if (previewsChildRect != null) {
expect(currentChildRect.left, previewsChildRect.left);
expect(currentChildRect.right, previewsChildRect.right);
expect(currentChildRect.top, previewsChildRect.top + previewsChildRect.height);
}
previewsChildRect = currentChildRect;
}
});
testWidgets('SegmentedButton vertical aligned golden image', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RepaintBoundary(
key: key,
child: SegmentedButton<int>(
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(
value: 0,
label: Text('Option 0'),
),
ButtonSegment<int>(
value: 1,
label: Text('Option 1'),
),
],
selected: const <int>{0}, // Prevent any of ButtonSegment to be selected
direction: Axis.vertical,
),
),
),
),
),
);
await expectLater(
find.byKey(key),
matchesGoldenFile('segmented_button_test_vertical.png'),
);
});
}
Set<MaterialState> enabled = const <MaterialState>{};