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.multiSelectionEnabled = false,
this.emptySelectionAllowed = false,
this.expandedInsets,
this.style,
this.showSelectedIcon = true,
this.selectedIcon,
@ -190,6 +191,13 @@ class SegmentedButton<T> extends StatefulWidget {
/// [onSelectionChanged] will not be called.
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
/// [ButtonStyle] given simple values.
///
@ -539,13 +547,17 @@ class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
surfaceTintColor: resolve<Color?>((ButtonStyle? style) => style?.surfaceTintColor),
child: TextButtonTheme(
data: TextButtonThemeData(style: segmentThemeStyle),
child: _SegmentedButtonRenderWidget<T>(
tapTargetVerticalPadding: tapTargetVerticalPadding,
segments: widget.segments,
enabledBorder: _enabled ? enabledBorder : disabledBorder,
disabledBorder: disabledBorder,
direction: direction,
children: buttons,
child: Padding(
padding: widget.expandedInsets ?? EdgeInsets.zero,
child: _SegmentedButtonRenderWidget<T>(
tapTargetVerticalPadding: tapTargetVerticalPadding,
segments: widget.segments,
enabledBorder: _enabled ? enabledBorder : disabledBorder,
disabledBorder: disabledBorder,
direction: direction,
isExpanded: widget.expandedInsets != null,
children: buttons,
),
),
),
);
@ -588,6 +600,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
required this.disabledBorder,
required this.direction,
required this.tapTargetVerticalPadding,
required this.isExpanded,
required super.children,
}) : assert(children.length == segments.length);
@ -596,6 +609,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
final OutlinedBorder disabledBorder;
final TextDirection direction;
final double tapTargetVerticalPadding;
final bool isExpanded;
@override
RenderObject createRenderObject(BuildContext context) {
@ -605,6 +619,7 @@ class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
disabledBorder: disabledBorder,
textDirection: direction,
tapTargetVerticalPadding: tapTargetVerticalPadding,
isExpanded: isExpanded,
);
}
@ -633,11 +648,13 @@ class _RenderSegmentedButton<T> extends RenderBox with
required OutlinedBorder disabledBorder,
required TextDirection textDirection,
required double tapTargetVerticalPadding,
required bool isExpanded,
}) : _segments = segments,
_enabledBorder = enabledBorder,
_disabledBorder = disabledBorder,
_textDirection = textDirection,
_tapTargetVerticalPadding = tapTargetVerticalPadding;
_tapTargetVerticalPadding = tapTargetVerticalPadding,
_isExpanded = isExpanded;
List<ButtonSegment<T>> get segments => _segments;
List<ButtonSegment<T>> _segments;
@ -689,6 +706,16 @@ class _RenderSegmentedButton<T> extends RenderBox with
markNeedsLayout();
}
bool get isExpanded => _isExpanded;
bool _isExpanded;
set isExpanded(bool value) {
if (value == _isExpanded) {
return;
}
_isExpanded = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
RenderBox? child = firstChild;
@ -770,13 +797,18 @@ class _RenderSegmentedButton<T> extends RenderBox with
Size _calculateChildSize(BoxConstraints constraints) {
double maxHeight = 0;
double childWidth = constraints.minWidth / childCount;
RenderBox? child = firstChild;
while (child != null) {
childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
child = childAfter(child);
double childWidth;
if (_isExpanded) {
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;
while (child != null) {
final double boxHeight = child.getMaxIntrinsicHeight(childWidth);

View File

@ -6,6 +6,7 @@
// machines.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.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>{};