From 3e4d59eae1d4ea483135c0f537061c34c1fddfb1 Mon Sep 17 00:00:00 2001 From: abdalmonem Date: Wed, 11 Sep 2024 20:59:36 +0300 Subject: [PATCH] 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( 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 --- .../lib/src/material/segmented_button.dart | 97 ++++++++++++++++--- .../test/material/segmented_button_test.dart | 82 ++++++++++++++++ 2 files changed, 166 insertions(+), 13 deletions(-) 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 {};