Skip to content

Commit 8fdb22e

Browse files
QuncCcccccBuchimi
authored andcommitted
CupertinoSlidingSegmentedControl is able to have proportional layout based on segment content (flutter#153125)
1 parent df4ed3b commit 8fdb22e

File tree

2 files changed

+523
-29
lines changed

2 files changed

+523
-29
lines changed

packages/flutter/lib/src/cupertino/sliding_segmented_control.dart

Lines changed: 131 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
library;
77

88
import 'dart:math' as math;
9+
import 'dart:math';
910

11+
import 'package:collection/collection.dart';
1012
import 'package:flutter/foundation.dart';
1113
import 'package:flutter/gestures.dart';
1214
import 'package:flutter/physics.dart';
@@ -323,6 +325,7 @@ class CupertinoSlidingSegmentedControl<T extends Object> extends StatefulWidget
323325
this.thumbColor = _kThumbColor,
324326
this.padding = _kHorizontalItemPadding,
325327
this.backgroundColor = CupertinoColors.tertiarySystemFill,
328+
this.proportionalWidth = false,
326329
}) : assert(children.length >= 2),
327330
assert(
328331
groupValue == null || children.keys.contains(groupValue),
@@ -395,6 +398,21 @@ class CupertinoSlidingSegmentedControl<T extends Object> extends StatefulWidget
395398
/// will not be painted if null is specified.
396399
final Color backgroundColor;
397400

401+
/// Determine whether segments have proportional widths based on their content.
402+
///
403+
/// If false, all segments will have the same width, determined by the longest
404+
/// segment. If true, each segment's width will be determined by its individual
405+
/// content.
406+
///
407+
/// If the max width of parent constraints is smaller than the width that the
408+
/// segmented control needs, The segment widths will scale down proportionally
409+
/// to ensure the segment control fits within the boundaries; similarly, if
410+
/// the min width of parent constraints is larger, the segment width will scales
411+
/// up to meet the min width requirement.
412+
///
413+
/// Defaults to false.
414+
final bool proportionalWidth;
415+
398416
/// The color used to paint the interior of the thumb that appears behind the
399417
/// currently selected item.
400418
///
@@ -422,10 +440,12 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSlidingSeg
422440
final TapGestureRecognizer tap = TapGestureRecognizer();
423441
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
424442
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
443+
final GlobalKey segmentedControlRenderWidgetKey = GlobalKey();
425444

426445
@override
427446
void initState() {
428447
super.initState();
448+
429449
// If the long press or horizontal drag recognizer gets accepted, we know for
430450
// sure the gesture is meant for the segmented control. Hand everything to
431451
// the drag gesture recognizer.
@@ -485,23 +505,24 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSlidingSeg
485505
// them from interfering with the active drag gesture.
486506
bool get isThumbDragging => _startedOnSelectedSegment ?? false;
487507

488-
// Converts local coordinate to segments. This method assumes each segment has
489-
// the same width.
508+
// Converts local coordinate to segments.
490509
T segmentForXPosition(double dx) {
491-
final RenderBox renderBox = context.findRenderObject()! as RenderBox;
510+
final BuildContext currentContext = segmentedControlRenderWidgetKey.currentContext!;
511+
final _RenderSegmentedControl<T> renderBox = currentContext.findRenderObject()! as _RenderSegmentedControl<T>;
512+
492513
final int numOfChildren = widget.children.length;
493514
assert(renderBox.hasSize);
494515
assert(numOfChildren >= 2);
495-
int index = (dx ~/ (renderBox.size.width / numOfChildren)).clamp(0, numOfChildren - 1);
516+
517+
int segmentIndex = renderBox.getClosestSegmentIndex(dx);
496518

497519
switch (Directionality.of(context)) {
498520
case TextDirection.ltr:
499521
break;
500522
case TextDirection.rtl:
501-
index = numOfChildren - 1 - index;
523+
segmentIndex = numOfChildren - 1 - segmentIndex;
502524
}
503-
504-
return widget.children.keys.elementAt(index);
525+
return widget.children.keys.elementAt(segmentIndex);
505526
}
506527

507528
bool _hasDraggedTooFar(DragUpdateDetails details) {
@@ -696,9 +717,11 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSlidingSeg
696717
animation: thumbScaleAnimation,
697718
builder: (BuildContext context, Widget? child) {
698719
return _SegmentedControlRenderWidget<T>(
720+
key: segmentedControlRenderWidgetKey,
699721
highlightedIndex: highlightedIndex,
700722
thumbColor: CupertinoDynamicColor.resolve(widget.thumbColor, context),
701723
thumbScale: thumbScaleAnimation.value,
724+
proportionalWidth: widget.proportionalWidth,
702725
state: this,
703726
children: children,
704727
);
@@ -716,12 +739,14 @@ class _SegmentedControlRenderWidget<T extends Object> extends MultiChildRenderOb
716739
required this.highlightedIndex,
717740
required this.thumbColor,
718741
required this.thumbScale,
742+
required this.proportionalWidth,
719743
required this.state,
720744
});
721745

722746
final int? highlightedIndex;
723747
final Color thumbColor;
724748
final double thumbScale;
749+
final bool proportionalWidth;
725750
final _SegmentedControlState<T> state;
726751

727752
@override
@@ -730,6 +755,7 @@ class _SegmentedControlRenderWidget<T extends Object> extends MultiChildRenderOb
730755
highlightedIndex: highlightedIndex,
731756
thumbColor: thumbColor,
732757
thumbScale: thumbScale,
758+
proportionalWidth: proportionalWidth,
733759
state: state,
734760
);
735761
}
@@ -740,7 +766,8 @@ class _SegmentedControlRenderWidget<T extends Object> extends MultiChildRenderOb
740766
renderObject
741767
..thumbColor = thumbColor
742768
..thumbScale = thumbScale
743-
..highlightedIndex = highlightedIndex;
769+
..highlightedIndex = highlightedIndex
770+
..proportionalWidth = proportionalWidth;
744771
}
745772
}
746773

@@ -785,10 +812,12 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
785812
required int? highlightedIndex,
786813
required Color thumbColor,
787814
required double thumbScale,
815+
required bool proportionalWidth,
788816
required this.state,
789817
}) : _highlightedIndex = highlightedIndex,
790818
_thumbColor = thumbColor,
791-
_thumbScale = thumbScale;
819+
_thumbScale = thumbScale,
820+
_proportionalWidth = proportionalWidth;
792821

793822
final _SegmentedControlState<T> state;
794823

@@ -841,6 +870,16 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
841870
markNeedsPaint();
842871
}
843872

873+
bool get proportionalWidth => _proportionalWidth;
874+
bool _proportionalWidth;
875+
set proportionalWidth(bool value) {
876+
if (_proportionalWidth == value) {
877+
return;
878+
}
879+
_proportionalWidth = value;
880+
markNeedsLayout();
881+
}
882+
844883
@override
845884
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
846885
assert(debugHandleEvent(event, entry));
@@ -853,8 +892,29 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
853892
}
854893

855894
// Intrinsic Dimensions
895+
double get separatorWidth => _kSeparatorInset.horizontal + _kSeparatorWidth;
896+
double get totalSeparatorWidth => separatorWidth * (childCount ~/ 2);
856897

857-
double get totalSeparatorWidth => (_kSeparatorInset.horizontal + _kSeparatorWidth) * (childCount ~/ 2);
898+
int getClosestSegmentIndex(double dx) {
899+
int index = 0;
900+
RenderBox? child = firstChild;
901+
while (child != null) {
902+
final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData;
903+
final double clampX = clampDouble(dx, childParentData.offset.dx, child.size.width + childParentData.offset.dx);
904+
905+
if (dx <= clampX) {
906+
break;
907+
}
908+
909+
index++;
910+
child = nonSeparatorChildAfter(child);
911+
}
912+
913+
final int segmentCount = childCount ~/ 2 + 1;
914+
// When the thumb is dragging out of bounds, the return result must be
915+
// smaller than segment count.
916+
return min(index, segmentCount - 1);
917+
}
858918

859919
RenderBox? nonSeparatorChildAfter(RenderBox child) {
860920
final RenderBox? nextChild = childAfter(child);
@@ -923,62 +983,106 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
923983
}
924984
}
925985

926-
Size _calculateChildSize(BoxConstraints constraints) {
986+
double _getMaxChildWidth(BoxConstraints constraints) {
927987
final int childCount = this.childCount ~/ 2 + 1;
928988
double childWidth = (constraints.minWidth - totalSeparatorWidth) / childCount;
929-
double maxHeight = _kMinSegmentedControlHeight;
930989
RenderBox? child = firstChild;
931990
while (child != null) {
932991
childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding);
933992
child = nonSeparatorChildAfter(child);
934993
}
935-
childWidth = math.min(
994+
return math.min(
936995
childWidth,
937996
(constraints.maxWidth - totalSeparatorWidth) / childCount,
938997
);
939-
child = firstChild;
998+
}
999+
1000+
double _getMaxChildHeight(BoxConstraints constraints, double childWidth) {
1001+
double maxHeight = _kMinSegmentedControlHeight;
1002+
RenderBox? child = firstChild;
9401003
while (child != null) {
9411004
final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
9421005
maxHeight = math.max(maxHeight, boxHeight);
9431006
child = nonSeparatorChildAfter(child);
9441007
}
945-
return Size(childWidth, maxHeight);
1008+
return maxHeight;
9461009
}
9471010

948-
Size _computeOverallSizeFromChildSize(Size childSize, BoxConstraints constraints) {
949-
final int childCount = this.childCount ~/ 2 + 1;
950-
return constraints.constrain(Size(childSize.width * childCount + totalSeparatorWidth, childSize.height));
1011+
List<double> _getChildWidths(BoxConstraints constraints) {
1012+
if (!proportionalWidth) {
1013+
final double maxChildWidth = _getMaxChildWidth(constraints);
1014+
final int segmentCount = childCount ~/ 2 + 1;
1015+
return List<double>.filled(segmentCount, maxChildWidth);
1016+
}
1017+
1018+
final List<double> segmentWidths = <double>[];
1019+
RenderBox? child = firstChild;
1020+
while (child != null) {
1021+
final double childWidth = child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding;
1022+
child = nonSeparatorChildAfter(child);
1023+
segmentWidths.add(childWidth);
1024+
}
1025+
1026+
final double totalWidth = segmentWidths.sum;
1027+
1028+
// If the sum of the children's width is larger than the allowed max width,
1029+
// each segment width should scale down until the overall size can fit in
1030+
// the parent constraints; similarly, if the sum of the children's width is
1031+
// smaller than the allowed min width, each segment width should scale up
1032+
// until the overall size can fit in the parent constraints.
1033+
final double allowedMaxWidth = constraints.maxWidth - totalSeparatorWidth;
1034+
final double allowedMinWidth = constraints.minWidth - totalSeparatorWidth;
1035+
1036+
final double scale = clampDouble(totalWidth, allowedMinWidth, allowedMaxWidth) / totalWidth;
1037+
if (scale != 1) {
1038+
for (int i = 0; i < segmentWidths.length; i++) {
1039+
segmentWidths[i] = segmentWidths[i] * scale;
1040+
}
1041+
}
1042+
return segmentWidths;
1043+
}
1044+
1045+
Size _computeOverallSize(BoxConstraints constraints) {
1046+
final double maxChildHeight = _getMaxChildHeight(constraints, constraints.maxWidth);
1047+
return constraints.constrain(Size(_getChildWidths(constraints).sum + totalSeparatorWidth, maxChildHeight));
9511048
}
9521049

9531050
@override
9541051
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
955-
final Size childSize = _calculateChildSize(constraints);
956-
final BoxConstraints childConstraints = BoxConstraints.tight(childSize);
1052+
final List<double> segmentWidths = _getChildWidths(constraints);
1053+
final double childHeight = _getMaxChildHeight(constraints, constraints.maxWidth);
9571054

1055+
int index = 0;
9581056
BaselineOffset baselineOffset = BaselineOffset.noBaseline;
959-
for (RenderBox? child = firstChild; child != null; child = childAfter(child)) {
1057+
RenderBox? child = firstChild;
1058+
while (child != null) {
1059+
final BoxConstraints childConstraints = BoxConstraints.tight(Size(segmentWidths[index], childHeight));
9601060
baselineOffset = baselineOffset.minOf(BaselineOffset(child.getDryBaseline(childConstraints, baseline)));
1061+
1062+
child = nonSeparatorChildAfter(child);
1063+
index++;
9611064
}
1065+
9621066
return baselineOffset.offset;
9631067
}
9641068

9651069
@override
9661070
Size computeDryLayout(BoxConstraints constraints) {
967-
final Size childSize = _calculateChildSize(constraints);
968-
return _computeOverallSizeFromChildSize(childSize, constraints);
1071+
return _computeOverallSize(constraints);
9691072
}
9701073

9711074
@override
9721075
void performLayout() {
9731076
final BoxConstraints constraints = this.constraints;
974-
final Size childSize = _calculateChildSize(constraints);
975-
final BoxConstraints childConstraints = BoxConstraints.tight(childSize);
976-
final BoxConstraints separatorConstraints = childConstraints.heightConstraints();
1077+
final List<double> segmentWidths = _getChildWidths(constraints);
9771078

1079+
final double childHeight = _getMaxChildHeight(constraints, double.infinity);
1080+
final BoxConstraints separatorConstraints = BoxConstraints(minHeight: childHeight, maxHeight: childHeight);
9781081
RenderBox? child = firstChild;
9791082
int index = 0;
9801083
double start = 0;
9811084
while (child != null) {
1085+
final BoxConstraints childConstraints = BoxConstraints.tight(Size(segmentWidths[index ~/ 2], childHeight));
9821086
child.layout(index.isEven ? childConstraints : separatorConstraints, parentUsesSize: true);
9831087
final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData;
9841088
final Offset childOffset = Offset(start, 0);
@@ -991,8 +1095,7 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
9911095
child = childAfter(child);
9921096
index += 1;
9931097
}
994-
995-
size = _computeOverallSizeFromChildSize(childSize, constraints);
1098+
size = _computeOverallSize(constraints);
9961099
}
9971100

9981101
// This method is used to convert the original unscaled thumb rect painted in

0 commit comments

Comments
 (0)