Skip to content

Commit 3e4d59e

Browse files
authored
Add 'direction' allow to 'SegmentedButton' oriented vertically (flutter#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: flutter#150416
1 parent 6e46ee8 commit 3e4d59e

File tree

2 files changed

+166
-13
lines changed

2 files changed

+166
-13
lines changed

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

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class SegmentedButton<T> extends StatefulWidget {
141141
this.style,
142142
this.showSelectedIcon = true,
143143
this.selectedIcon,
144+
this.direction = Axis.horizontal,
144145
}) : assert(segments.length > 0),
145146
assert(selected.length > 0 || emptySelectionAllowed),
146147
assert(selected.length < 2 || multiSelectionEnabled);
@@ -154,6 +155,14 @@ class SegmentedButton<T> extends StatefulWidget {
154155
/// [ChoiceChip] widgets.
155156
final List<ButtonSegment<T>> segments;
156157

158+
/// The orientation of the button's [segments].
159+
///
160+
/// If this is [Axis.vertical], the segments will be aligned vertically
161+
/// and the first item in [segments] will be on the top.
162+
///
163+
/// Defaults to [Axis.horizontal].
164+
final Axis direction;
165+
157166
/// The set of [ButtonSegment.value]s that indicate which [segments] are
158167
/// selected.
159168
///
@@ -449,7 +458,7 @@ class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
449458
Widget build(BuildContext context) {
450459
final SegmentedButtonThemeData theme = SegmentedButtonTheme.of(context);
451460
final SegmentedButtonThemeData defaults = _SegmentedButtonDefaultsM3(context);
452-
final TextDirection direction = Directionality.of(context);
461+
final TextDirection textDirection = Directionality.of(context);
453462

454463
const Set<MaterialState> enabledState = <MaterialState>{};
455464
const Set<MaterialState> disabledState = <MaterialState>{ MaterialState.disabled };
@@ -576,7 +585,8 @@ class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
576585
segments: widget.segments,
577586
enabledBorder: _enabled ? enabledBorder : disabledBorder,
578587
disabledBorder: disabledBorder,
579-
direction: direction,
588+
direction: widget.direction,
589+
textDirection: textDirection,
580590
isExpanded: widget.expandedInsets != null,
581591
children: buttons,
582592
),
@@ -601,6 +611,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
601611
required this.enabledBorder,
602612
required this.disabledBorder,
603613
required this.direction,
614+
required this.textDirection,
604615
required this.tapTargetVerticalPadding,
605616
required this.isExpanded,
606617
required super.children,
@@ -609,7 +620,8 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
609620
final List<ButtonSegment<T>> segments;
610621
final OutlinedBorder enabledBorder;
611622
final OutlinedBorder disabledBorder;
612-
final TextDirection direction;
623+
final Axis direction;
624+
final TextDirection textDirection;
613625
final double tapTargetVerticalPadding;
614626
final bool isExpanded;
615627

@@ -619,7 +631,8 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
619631
segments: segments,
620632
enabledBorder: enabledBorder,
621633
disabledBorder: disabledBorder,
622-
textDirection: direction,
634+
textDirection: textDirection,
635+
direction: direction,
623636
tapTargetVerticalPadding: tapTargetVerticalPadding,
624637
isExpanded: isExpanded,
625638
);
@@ -631,7 +644,8 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
631644
..segments = segments
632645
..enabledBorder = enabledBorder
633646
..disabledBorder = disabledBorder
634-
..textDirection = direction;
647+
..direction = direction
648+
..textDirection = textDirection;
635649
}
636650
}
637651

@@ -651,10 +665,12 @@ class _RenderSegmentedButton<T> extends RenderBox with
651665
required TextDirection textDirection,
652666
required double tapTargetVerticalPadding,
653667
required bool isExpanded,
668+
required Axis direction,
654669
}) : _segments = segments,
655670
_enabledBorder = enabledBorder,
656671
_disabledBorder = disabledBorder,
657672
_textDirection = textDirection,
673+
_direction = direction,
658674
_tapTargetVerticalPadding = tapTargetVerticalPadding,
659675
_isExpanded = isExpanded;
660676

@@ -698,6 +714,16 @@ class _RenderSegmentedButton<T> extends RenderBox with
698714
markNeedsLayout();
699715
}
700716

717+
Axis get direction => _direction;
718+
Axis _direction;
719+
set direction(Axis value) {
720+
if (value == _direction) {
721+
return;
722+
}
723+
_direction = value;
724+
markNeedsLayout();
725+
}
726+
701727
double get tapTargetVerticalPadding => _tapTargetVerticalPadding;
702728
double _tapTargetVerticalPadding;
703729
set tapTargetVerticalPadding(double value) {
@@ -787,17 +813,28 @@ class _RenderSegmentedButton<T> extends RenderBox with
787813
double start = 0.0;
788814
while (child != null) {
789815
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
790-
final Offset childOffset = Offset(start, 0.0);
791-
childParentData.offset = childOffset;
792-
final Rect childRect = Rect.fromLTWH(start, 0.0, child.size.width, child.size.height);
793-
final RRect rChildRect = RRect.fromRectAndCorners(childRect);
816+
late final RRect rChildRect;
817+
if (direction == Axis.vertical) {
818+
childParentData.offset = Offset(0.0, start);
819+
final Rect childRect = Rect.fromLTWH(0.0, childParentData.offset.dy, child.size.width, child.size.height);
820+
rChildRect = RRect.fromRectAndCorners(childRect);
821+
start += child.size.height;
822+
} else {
823+
childParentData.offset = Offset(start, 0.0);
824+
final Rect childRect = Rect.fromLTWH(start, 0.0, child.size.width, child.size.height);
825+
rChildRect = RRect.fromRectAndCorners(childRect);
826+
start += child.size.width;
827+
}
794828
childParentData.surroundingRect = rChildRect;
795-
start += child.size.width;
796829
child = nextChild(child);
797830
}
798831
}
799832

800833
Size _calculateChildSize(BoxConstraints constraints) {
834+
return direction == Axis.horizontal ? _calculateHorizontalChildSize(constraints) : _calculateVerticalChildSize(constraints);
835+
}
836+
837+
Size _calculateHorizontalChildSize(BoxConstraints constraints) {
801838
double maxHeight = 0;
802839
RenderBox? child = firstChild;
803840
double childWidth;
@@ -820,7 +857,33 @@ class _RenderSegmentedButton<T> extends RenderBox with
820857
return Size(childWidth, maxHeight);
821858
}
822859

860+
Size _calculateVerticalChildSize(BoxConstraints constraints) {
861+
double maxWidth = 0;
862+
RenderBox? child = firstChild;
863+
double childHeight;
864+
if (_isExpanded) {
865+
childHeight = constraints.maxHeight / childCount;
866+
} else {
867+
childHeight = constraints.minHeight / childCount;
868+
while (child != null) {
869+
childHeight = math.max(childHeight, child.getMaxIntrinsicHeight(double.infinity));
870+
child = childAfter(child);
871+
}
872+
childHeight = math.min(childHeight, constraints.maxHeight / childCount);
873+
}
874+
child = firstChild;
875+
while (child != null) {
876+
final double boxWidth = child.getMaxIntrinsicWidth(maxWidth);
877+
maxWidth = math.max(maxWidth, boxWidth);
878+
child = childAfter(child);
879+
}
880+
return Size(maxWidth, childHeight);
881+
}
882+
823883
Size _computeOverallSizeFromChildSize(Size childSize) {
884+
if (direction == Axis.vertical) {
885+
return constraints.constrain(Size(childSize.width, childSize.height * childCount));
886+
}
824887
return constraints.constrain(Size(childSize.width * childCount, childSize.height));
825888
}
826889

@@ -926,9 +989,17 @@ class _RenderSegmentedButton<T> extends RenderBox with
926989
final BorderSide divider = segments[index - 1].enabled || segments[index].enabled
927990
? enabledBorder.side.copyWith(strokeAlign: 0.0)
928991
: disabledBorder.side.copyWith(strokeAlign: 0.0);
929-
final Offset top = Offset(dividerPos, borderRect.top);
930-
final Offset bottom = Offset(dividerPos, borderRect.bottom);
931-
context.canvas.drawLine(top, bottom, divider.toPaint());
992+
if (direction == Axis.horizontal) {
993+
final Offset top = Offset(dividerPos, borderRect.top);
994+
final Offset bottom = Offset(dividerPos, borderRect.bottom);
995+
context.canvas.drawLine(top, bottom, divider.toPaint());
996+
} else if (direction == Axis.vertical) {
997+
final Offset start = Offset(borderRect.left, childRect.top);
998+
final Offset end = Offset(borderRect.right, childRect.top);
999+
context.canvas..save()..clipPath(borderClipPath);
1000+
context.canvas.drawLine(start, end, divider.toPaint());
1001+
context.canvas.restore();
1002+
}
9321003
}
9331004

9341005
previousChild = child;

packages/flutter/test/material/segmented_button_test.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
// This file is run as part of a reduced test set in CI on Mac and Windows
66
// machines.
7+
@Tags(<String>['reduced-test-set'])
8+
library;
79
import 'dart:ui';
810

911
import 'package:flutter/foundation.dart';
@@ -1122,6 +1124,86 @@ void main() {
11221124
),
11231125
);
11241126
}, skip: kIsWeb && !isSkiaWeb); // https://github.com/flutter/flutter/issues/99933
1127+
1128+
testWidgets('SegmentedButton vertical aligned children', (WidgetTester tester) async {
1129+
await tester.pumpWidget(
1130+
MaterialApp(
1131+
home: Scaffold(
1132+
body: Center(
1133+
child: SegmentedButton<int>(
1134+
segments: const <ButtonSegment<int>>[
1135+
ButtonSegment<int>(
1136+
value: 0,
1137+
label: Text('Option 0'),
1138+
),
1139+
ButtonSegment<int>(
1140+
value: 1,
1141+
label: Text('Option 1'),
1142+
),
1143+
ButtonSegment<int>(
1144+
value: 2,
1145+
label: Text('Option 2'),
1146+
),
1147+
ButtonSegment<int>(
1148+
value: 3,
1149+
label: Text('Option 3'),
1150+
),
1151+
],
1152+
onSelectionChanged: (Set<int> selected) {},
1153+
selected: const <int>{-1}, // Prevent any of ButtonSegment to be selected
1154+
direction: Axis.vertical,
1155+
),
1156+
),
1157+
),
1158+
),
1159+
);
1160+
1161+
Rect? previewsChildRect;
1162+
for (int i = 0; i <= 3; i++) {
1163+
final Rect currentChildRect = tester.getRect(find.widgetWithText(TextButton, 'Option $i'));
1164+
if (previewsChildRect != null) {
1165+
expect(currentChildRect.left, previewsChildRect.left);
1166+
expect(currentChildRect.right, previewsChildRect.right);
1167+
expect(currentChildRect.top, previewsChildRect.top + previewsChildRect.height);
1168+
}
1169+
previewsChildRect = currentChildRect;
1170+
}
1171+
});
1172+
1173+
1174+
testWidgets('SegmentedButton vertical aligned golden image', (WidgetTester tester) async {
1175+
final GlobalKey key = GlobalKey();
1176+
await tester.pumpWidget(
1177+
MaterialApp(
1178+
home: Scaffold(
1179+
body: Center(
1180+
child: RepaintBoundary(
1181+
key: key,
1182+
child: SegmentedButton<int>(
1183+
segments: const <ButtonSegment<int>>[
1184+
ButtonSegment<int>(
1185+
value: 0,
1186+
label: Text('Option 0'),
1187+
),
1188+
ButtonSegment<int>(
1189+
value: 1,
1190+
label: Text('Option 1'),
1191+
),
1192+
],
1193+
selected: const <int>{0}, // Prevent any of ButtonSegment to be selected
1194+
direction: Axis.vertical,
1195+
),
1196+
),
1197+
),
1198+
),
1199+
),
1200+
);
1201+
1202+
await expectLater(
1203+
find.byKey(key),
1204+
matchesGoldenFile('segmented_button_test_vertical.png'),
1205+
);
1206+
});
11251207
}
11261208

11271209
Set<MaterialState> enabled = const <MaterialState>{};

0 commit comments

Comments
 (0)