Skip to content

Commit e868e2b

Browse files
authored
Add SegmentedButton expand feature (flutter#142804)
fix flutter#139486 The `expandedInsets` property enhancement for the `SegmentedButton` widget introduces flexibility in button layout design within Flutter applications. When `expandedInsets` is not null, this property allows the `SegmentedButton` to expand, filling the available horizontal space of its parent container and also have the specified insets, thus ensuring a uniform distribution of segment widths across the button. Conversely, with `expandedInsets` is null, the widget sizes itself based on the intrinsic sizes of its segments, preserving the natural width of each segment. This addition enhances the adaptability of the `SegmentedButton` to various UI designs, enabling developers to achieve both expansive and compact button layouts seamlessly. ### Setting expandedInsets = null <img width="724" alt="image" src="https://github.com/flutter/flutter/assets/65075121/f173b327-a34b-49f0-a7b1-a212a376c5e3"> ### Setting expandedInsets = EdgeInsets.zero <img width="724" alt="image" src="https://github.com/flutter/flutter/assets/36861262/f141a3d3-80e3-4aeb-b7c8-d56ca77ca049">
1 parent b63196d commit e868e2b

File tree

2 files changed

+99
-13
lines changed

2 files changed

+99
-13
lines changed

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

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class SegmentedButton<T> extends StatefulWidget {
130130
this.onSelectionChanged,
131131
this.multiSelectionEnabled = false,
132132
this.emptySelectionAllowed = false,
133+
this.expandedInsets,
133134
this.style,
134135
this.showSelectedIcon = true,
135136
this.selectedIcon,
@@ -190,6 +191,13 @@ class SegmentedButton<T> extends StatefulWidget {
190191
/// [onSelectionChanged] will not be called.
191192
final bool emptySelectionAllowed;
192193

194+
/// Determines the segmented button's size and padding based on [expandedInsets].
195+
///
196+
/// If null (default), the button adopts its intrinsic content size. When specified,
197+
/// the button expands to fill its parent's space, with the [EdgeInsets]
198+
/// defining the padding.
199+
final EdgeInsets? expandedInsets;
200+
193201
/// A static convenience method that constructs a segmented button
194202
/// [ButtonStyle] given simple values.
195203
///
@@ -539,13 +547,17 @@ class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
539547
surfaceTintColor: resolve<Color?>((ButtonStyle? style) => style?.surfaceTintColor),
540548
child: TextButtonTheme(
541549
data: TextButtonThemeData(style: segmentThemeStyle),
542-
child: _SegmentedButtonRenderWidget<T>(
543-
tapTargetVerticalPadding: tapTargetVerticalPadding,
544-
segments: widget.segments,
545-
enabledBorder: _enabled ? enabledBorder : disabledBorder,
546-
disabledBorder: disabledBorder,
547-
direction: direction,
548-
children: buttons,
550+
child: Padding(
551+
padding: widget.expandedInsets ?? EdgeInsets.zero,
552+
child: _SegmentedButtonRenderWidget<T>(
553+
tapTargetVerticalPadding: tapTargetVerticalPadding,
554+
segments: widget.segments,
555+
enabledBorder: _enabled ? enabledBorder : disabledBorder,
556+
disabledBorder: disabledBorder,
557+
direction: direction,
558+
isExpanded: widget.expandedInsets != null,
559+
children: buttons,
560+
),
549561
),
550562
),
551563
);
@@ -588,6 +600,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
588600
required this.disabledBorder,
589601
required this.direction,
590602
required this.tapTargetVerticalPadding,
603+
required this.isExpanded,
591604
required super.children,
592605
}) : assert(children.length == segments.length);
593606

@@ -596,6 +609,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
596609
final OutlinedBorder disabledBorder;
597610
final TextDirection direction;
598611
final double tapTargetVerticalPadding;
612+
final bool isExpanded;
599613

600614
@override
601615
RenderObject createRenderObject(BuildContext context) {
@@ -605,6 +619,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
605619
disabledBorder: disabledBorder,
606620
textDirection: direction,
607621
tapTargetVerticalPadding: tapTargetVerticalPadding,
622+
isExpanded: isExpanded,
608623
);
609624
}
610625

@@ -633,11 +648,13 @@ class _RenderSegmentedButton<T> extends RenderBox with
633648
required OutlinedBorder disabledBorder,
634649
required TextDirection textDirection,
635650
required double tapTargetVerticalPadding,
651+
required bool isExpanded,
636652
}) : _segments = segments,
637653
_enabledBorder = enabledBorder,
638654
_disabledBorder = disabledBorder,
639655
_textDirection = textDirection,
640-
_tapTargetVerticalPadding = tapTargetVerticalPadding;
656+
_tapTargetVerticalPadding = tapTargetVerticalPadding,
657+
_isExpanded = isExpanded;
641658

642659
List<ButtonSegment<T>> get segments => _segments;
643660
List<ButtonSegment<T>> _segments;
@@ -689,6 +706,16 @@ class _RenderSegmentedButton<T> extends RenderBox with
689706
markNeedsLayout();
690707
}
691708

709+
bool get isExpanded => _isExpanded;
710+
bool _isExpanded;
711+
set isExpanded(bool value) {
712+
if (value == _isExpanded) {
713+
return;
714+
}
715+
_isExpanded = value;
716+
markNeedsLayout();
717+
}
718+
692719
@override
693720
double computeMinIntrinsicWidth(double height) {
694721
RenderBox? child = firstChild;
@@ -770,13 +797,18 @@ class _RenderSegmentedButton<T> extends RenderBox with
770797

771798
Size _calculateChildSize(BoxConstraints constraints) {
772799
double maxHeight = 0;
773-
double childWidth = constraints.minWidth / childCount;
774800
RenderBox? child = firstChild;
775-
while (child != null) {
776-
childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
777-
child = childAfter(child);
801+
double childWidth;
802+
if (_isExpanded) {
803+
childWidth = constraints.maxWidth / childCount;
804+
} else {
805+
childWidth = constraints.minWidth / childCount;
806+
while (child != null) {
807+
childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
808+
child = childAfter(child);
809+
}
810+
childWidth = math.min(childWidth, constraints.maxWidth / childCount);
778811
}
779-
childWidth = math.min(childWidth, constraints.maxWidth / childCount);
780812
child = firstChild;
781813
while (child != null) {
782814
final double boxHeight = child.getMaxIntrinsicHeight(childWidth);

packages/flutter/test/material/segmented_button_test.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// machines.
77
import 'dart:ui';
88

9+
import 'package:flutter/foundation.dart';
910
import 'package:flutter/material.dart';
1011
import 'package:flutter/rendering.dart';
1112
import 'package:flutter_test/flutter_test.dart';
@@ -855,6 +856,59 @@ void main() {
855856
)
856857
);
857858
});
859+
860+
testWidgets('SegmentedButton expands to fill the available width when expandedInsets is not null', (WidgetTester tester) async {
861+
await tester.pumpWidget(MaterialApp(
862+
home: Scaffold(
863+
body: Center(
864+
child: SegmentedButton<int>(
865+
segments: const <ButtonSegment<int>>[
866+
ButtonSegment<int>(value: 1, label: Text('Segment 1')),
867+
ButtonSegment<int>(value: 2, label: Text('Segment 2')),
868+
],
869+
selected: const <int>{1},
870+
expandedInsets: EdgeInsets.zero,
871+
),
872+
),
873+
),
874+
));
875+
876+
// Get the width of the SegmentedButton.
877+
final RenderBox box = tester.renderObject(find.byType(SegmentedButton<int>));
878+
final double segmentedButtonWidth = box.size.width;
879+
880+
// Get the width of the parent widget.
881+
final double screenWidth = tester.getSize(find.byType(Scaffold)).width;
882+
883+
// The width of the SegmentedButton must be equal to the width of the parent widget.
884+
expect(segmentedButtonWidth, equals(screenWidth));
885+
});
886+
887+
testWidgets('SegmentedButton does not expand when expandedInsets is null', (WidgetTester tester) async {
888+
await tester.pumpWidget(MaterialApp(
889+
home: Scaffold(
890+
body: Center(
891+
child: SegmentedButton<int>(
892+
segments: const <ButtonSegment<int>>[
893+
ButtonSegment<int>(value: 1, label: Text('Segment 1')),
894+
ButtonSegment<int>(value: 2, label: Text('Segment 2')),
895+
],
896+
selected: const <int>{1},
897+
),
898+
),
899+
),
900+
));
901+
902+
// Get the width of the SegmentedButton.
903+
final RenderBox box = tester.renderObject(find.byType(SegmentedButton<int>));
904+
final double segmentedButtonWidth = box.size.width;
905+
906+
// Get the width of the parent widget.
907+
final double screenWidth = tester.getSize(find.byType(Scaffold)).width;
908+
909+
// The width of the SegmentedButton must be less than the width of the parent widget.
910+
expect(segmentedButtonWidth, lessThan(screenWidth));
911+
}, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/145527
858912
}
859913

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

0 commit comments

Comments
 (0)