66library ;
77
88import 'dart:math' as math;
9+ import 'dart:math' ;
910
11+ import 'package:collection/collection.dart' ;
1012import 'package:flutter/foundation.dart' ;
1113import 'package:flutter/gestures.dart' ;
1214import '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