Add SegmentedButton expand feature (#142804)

fix #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">
This commit is contained in:
Furkan Acar 2024-04-03 19:16:52 +03:00 committed by GitHub
parent b63196def2
commit e868e2b383
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 99 additions and 13 deletions

View File

@ -130,6 +130,7 @@ class SegmentedButton<T> extends StatefulWidget {
this.onSelectionChanged, this.onSelectionChanged,
this.multiSelectionEnabled = false, this.multiSelectionEnabled = false,
this.emptySelectionAllowed = false, this.emptySelectionAllowed = false,
this.expandedInsets,
this.style, this.style,
this.showSelectedIcon = true, this.showSelectedIcon = true,
this.selectedIcon, this.selectedIcon,
@ -190,6 +191,13 @@ class SegmentedButton<T> extends StatefulWidget {
/// [onSelectionChanged] will not be called. /// [onSelectionChanged] will not be called.
final bool emptySelectionAllowed; final bool emptySelectionAllowed;
/// Determines the segmented button's size and padding based on [expandedInsets].
///
/// If null (default), the button adopts its intrinsic content size. When specified,
/// the button expands to fill its parent's space, with the [EdgeInsets]
/// defining the padding.
final EdgeInsets? expandedInsets;
/// A static convenience method that constructs a segmented button /// A static convenience method that constructs a segmented button
/// [ButtonStyle] given simple values. /// [ButtonStyle] given simple values.
/// ///
@ -539,13 +547,17 @@ class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
surfaceTintColor: resolve<Color?>((ButtonStyle? style) => style?.surfaceTintColor), surfaceTintColor: resolve<Color?>((ButtonStyle? style) => style?.surfaceTintColor),
child: TextButtonTheme( child: TextButtonTheme(
data: TextButtonThemeData(style: segmentThemeStyle), data: TextButtonThemeData(style: segmentThemeStyle),
child: _SegmentedButtonRenderWidget<T>( child: Padding(
tapTargetVerticalPadding: tapTargetVerticalPadding, padding: widget.expandedInsets ?? EdgeInsets.zero,
segments: widget.segments, child: _SegmentedButtonRenderWidget<T>(
enabledBorder: _enabled ? enabledBorder : disabledBorder, tapTargetVerticalPadding: tapTargetVerticalPadding,
disabledBorder: disabledBorder, segments: widget.segments,
direction: direction, enabledBorder: _enabled ? enabledBorder : disabledBorder,
children: buttons, disabledBorder: disabledBorder,
direction: direction,
isExpanded: widget.expandedInsets != null,
children: buttons,
),
), ),
), ),
); );
@ -588,6 +600,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
required this.disabledBorder, required this.disabledBorder,
required this.direction, required this.direction,
required this.tapTargetVerticalPadding, required this.tapTargetVerticalPadding,
required this.isExpanded,
required super.children, required super.children,
}) : assert(children.length == segments.length); }) : assert(children.length == segments.length);
@ -596,6 +609,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
final OutlinedBorder disabledBorder; final OutlinedBorder disabledBorder;
final TextDirection direction; final TextDirection direction;
final double tapTargetVerticalPadding; final double tapTargetVerticalPadding;
final bool isExpanded;
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
@ -605,6 +619,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
disabledBorder: disabledBorder, disabledBorder: disabledBorder,
textDirection: direction, textDirection: direction,
tapTargetVerticalPadding: tapTargetVerticalPadding, tapTargetVerticalPadding: tapTargetVerticalPadding,
isExpanded: isExpanded,
); );
} }
@ -633,11 +648,13 @@ class _RenderSegmentedButton<T> extends RenderBox with
required OutlinedBorder disabledBorder, required OutlinedBorder disabledBorder,
required TextDirection textDirection, required TextDirection textDirection,
required double tapTargetVerticalPadding, required double tapTargetVerticalPadding,
required bool isExpanded,
}) : _segments = segments, }) : _segments = segments,
_enabledBorder = enabledBorder, _enabledBorder = enabledBorder,
_disabledBorder = disabledBorder, _disabledBorder = disabledBorder,
_textDirection = textDirection, _textDirection = textDirection,
_tapTargetVerticalPadding = tapTargetVerticalPadding; _tapTargetVerticalPadding = tapTargetVerticalPadding,
_isExpanded = isExpanded;
List<ButtonSegment<T>> get segments => _segments; List<ButtonSegment<T>> get segments => _segments;
List<ButtonSegment<T>> _segments; List<ButtonSegment<T>> _segments;
@ -689,6 +706,16 @@ class _RenderSegmentedButton<T> extends RenderBox with
markNeedsLayout(); markNeedsLayout();
} }
bool get isExpanded => _isExpanded;
bool _isExpanded;
set isExpanded(bool value) {
if (value == _isExpanded) {
return;
}
_isExpanded = value;
markNeedsLayout();
}
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
RenderBox? child = firstChild; RenderBox? child = firstChild;
@ -770,13 +797,18 @@ class _RenderSegmentedButton<T> extends RenderBox with
Size _calculateChildSize(BoxConstraints constraints) { Size _calculateChildSize(BoxConstraints constraints) {
double maxHeight = 0; double maxHeight = 0;
double childWidth = constraints.minWidth / childCount;
RenderBox? child = firstChild; RenderBox? child = firstChild;
while (child != null) { double childWidth;
childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity)); if (_isExpanded) {
child = childAfter(child); childWidth = constraints.maxWidth / childCount;
} else {
childWidth = constraints.minWidth / childCount;
while (child != null) {
childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
child = childAfter(child);
}
childWidth = math.min(childWidth, constraints.maxWidth / childCount);
} }
childWidth = math.min(childWidth, constraints.maxWidth / childCount);
child = firstChild; child = firstChild;
while (child != null) { while (child != null) {
final double boxHeight = child.getMaxIntrinsicHeight(childWidth); final double boxHeight = child.getMaxIntrinsicHeight(childWidth);

View File

@ -6,6 +6,7 @@
// machines. // machines.
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -855,6 +856,59 @@ void main() {
) )
); );
}); });
testWidgets('SegmentedButton expands to fill the available width when expandedInsets is not null', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Center(
child: SegmentedButton<int>(
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(value: 1, label: Text('Segment 1')),
ButtonSegment<int>(value: 2, label: Text('Segment 2')),
],
selected: const <int>{1},
expandedInsets: EdgeInsets.zero,
),
),
),
));
// Get the width of the SegmentedButton.
final RenderBox box = tester.renderObject(find.byType(SegmentedButton<int>));
final double segmentedButtonWidth = box.size.width;
// Get the width of the parent widget.
final double screenWidth = tester.getSize(find.byType(Scaffold)).width;
// The width of the SegmentedButton must be equal to the width of the parent widget.
expect(segmentedButtonWidth, equals(screenWidth));
});
testWidgets('SegmentedButton does not expand when expandedInsets is null', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Center(
child: SegmentedButton<int>(
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(value: 1, label: Text('Segment 1')),
ButtonSegment<int>(value: 2, label: Text('Segment 2')),
],
selected: const <int>{1},
),
),
),
));
// Get the width of the SegmentedButton.
final RenderBox box = tester.renderObject(find.byType(SegmentedButton<int>));
final double segmentedButtonWidth = box.size.width;
// Get the width of the parent widget.
final double screenWidth = tester.getSize(find.byType(Scaffold)).width;
// The width of the SegmentedButton must be less than the width of the parent widget.
expect(segmentedButtonWidth, lessThan(screenWidth));
}, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/145527
} }
Set<MaterialState> enabled = const <MaterialState>{}; Set<MaterialState> enabled = const <MaterialState>{};