diff --git a/packages/flutter/lib/src/material/segmented_button.dart b/packages/flutter/lib/src/material/segmented_button.dart index e3fe7c04141..fefb1a3f317 100644 --- a/packages/flutter/lib/src/material/segmented_button.dart +++ b/packages/flutter/lib/src/material/segmented_button.dart @@ -141,6 +141,7 @@ class SegmentedButton 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 extends StatefulWidget { /// [ChoiceChip] widgets. final List> 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 extends State> { 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 enabledState = {}; const Set disabledState = { MaterialState.disabled }; @@ -576,7 +585,8 @@ class SegmentedButtonState extends State> { 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 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 extends MultiChildRenderObjectWidget { final List> 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 extends MultiChildRenderObjectWidget { segments: segments, enabledBorder: enabledBorder, disabledBorder: disabledBorder, - textDirection: direction, + textDirection: textDirection, + direction: direction, tapTargetVerticalPadding: tapTargetVerticalPadding, isExpanded: isExpanded, ); @@ -631,7 +644,8 @@ class _SegmentedButtonRenderWidget extends MultiChildRenderObjectWidget { ..segments = segments ..enabledBorder = enabledBorder ..disabledBorder = disabledBorder - ..textDirection = direction; + ..direction = direction + ..textDirection = textDirection; } } @@ -651,10 +665,12 @@ class _RenderSegmentedButton 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 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 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 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 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; diff --git a/packages/flutter/test/material/segmented_button_test.dart b/packages/flutter/test/material/segmented_button_test.dart index 06424c7cf29..a7ba69f1134 100644 --- a/packages/flutter/test/material/segmented_button_test.dart +++ b/packages/flutter/test/material/segmented_button_test.dart @@ -4,6 +4,8 @@ // This file is run as part of a reduced test set in CI on Mac and Windows // machines. +@Tags(['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( + segments: const >[ + ButtonSegment( + value: 0, + label: Text('Option 0'), + ), + ButtonSegment( + value: 1, + label: Text('Option 1'), + ), + ButtonSegment( + value: 2, + label: Text('Option 2'), + ), + ButtonSegment( + value: 3, + label: Text('Option 3'), + ), + ], + onSelectionChanged: (Set selected) {}, + selected: const {-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( + segments: const >[ + ButtonSegment( + value: 0, + label: Text('Option 0'), + ), + ButtonSegment( + value: 1, + label: Text('Option 1'), + ), + ], + selected: const {0}, // Prevent any of ButtonSegment to be selected + direction: Axis.vertical, + ), + ), + ), + ), + ), + ); + + await expectLater( + find.byKey(key), + matchesGoldenFile('segmented_button_test_vertical.png'), + ); + }); } Set enabled = const {};