6
6
library ;
7
7
8
8
import 'dart:math' as math;
9
+ import 'dart:math' ;
9
10
11
+ import 'package:collection/collection.dart' ;
10
12
import 'package:flutter/foundation.dart' ;
11
13
import 'package:flutter/gestures.dart' ;
12
14
import 'package:flutter/physics.dart' ;
@@ -323,6 +325,7 @@ class CupertinoSlidingSegmentedControl<T extends Object> extends StatefulWidget
323
325
this .thumbColor = _kThumbColor,
324
326
this .padding = _kHorizontalItemPadding,
325
327
this .backgroundColor = CupertinoColors .tertiarySystemFill,
328
+ this .proportionalWidth = false ,
326
329
}) : assert (children.length >= 2 ),
327
330
assert (
328
331
groupValue == null || children.keys.contains (groupValue),
@@ -395,6 +398,21 @@ class CupertinoSlidingSegmentedControl<T extends Object> extends StatefulWidget
395
398
/// will not be painted if null is specified.
396
399
final Color backgroundColor;
397
400
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
+
398
416
/// The color used to paint the interior of the thumb that appears behind the
399
417
/// currently selected item.
400
418
///
@@ -422,10 +440,12 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSlidingSeg
422
440
final TapGestureRecognizer tap = TapGestureRecognizer ();
423
441
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer ();
424
442
final LongPressGestureRecognizer longPress = LongPressGestureRecognizer ();
443
+ final GlobalKey segmentedControlRenderWidgetKey = GlobalKey ();
425
444
426
445
@override
427
446
void initState () {
428
447
super .initState ();
448
+
429
449
// If the long press or horizontal drag recognizer gets accepted, we know for
430
450
// sure the gesture is meant for the segmented control. Hand everything to
431
451
// the drag gesture recognizer.
@@ -485,23 +505,24 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSlidingSeg
485
505
// them from interfering with the active drag gesture.
486
506
bool get isThumbDragging => _startedOnSelectedSegment ?? false ;
487
507
488
- // Converts local coordinate to segments. This method assumes each segment has
489
- // the same width.
508
+ // Converts local coordinate to segments.
490
509
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
+
492
513
final int numOfChildren = widget.children.length;
493
514
assert (renderBox.hasSize);
494
515
assert (numOfChildren >= 2 );
495
- int index = (dx ~ / (renderBox.size.width / numOfChildren)).clamp (0 , numOfChildren - 1 );
516
+
517
+ int segmentIndex = renderBox.getClosestSegmentIndex (dx);
496
518
497
519
switch (Directionality .of (context)) {
498
520
case TextDirection .ltr:
499
521
break ;
500
522
case TextDirection .rtl:
501
- index = numOfChildren - 1 - index ;
523
+ segmentIndex = numOfChildren - 1 - segmentIndex ;
502
524
}
503
-
504
- return widget.children.keys.elementAt (index);
525
+ return widget.children.keys.elementAt (segmentIndex);
505
526
}
506
527
507
528
bool _hasDraggedTooFar (DragUpdateDetails details) {
@@ -696,9 +717,11 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSlidingSeg
696
717
animation: thumbScaleAnimation,
697
718
builder: (BuildContext context, Widget ? child) {
698
719
return _SegmentedControlRenderWidget <T >(
720
+ key: segmentedControlRenderWidgetKey,
699
721
highlightedIndex: highlightedIndex,
700
722
thumbColor: CupertinoDynamicColor .resolve (widget.thumbColor, context),
701
723
thumbScale: thumbScaleAnimation.value,
724
+ proportionalWidth: widget.proportionalWidth,
702
725
state: this ,
703
726
children: children,
704
727
);
@@ -716,12 +739,14 @@ class _SegmentedControlRenderWidget<T extends Object> extends MultiChildRenderOb
716
739
required this .highlightedIndex,
717
740
required this .thumbColor,
718
741
required this .thumbScale,
742
+ required this .proportionalWidth,
719
743
required this .state,
720
744
});
721
745
722
746
final int ? highlightedIndex;
723
747
final Color thumbColor;
724
748
final double thumbScale;
749
+ final bool proportionalWidth;
725
750
final _SegmentedControlState <T > state;
726
751
727
752
@override
@@ -730,6 +755,7 @@ class _SegmentedControlRenderWidget<T extends Object> extends MultiChildRenderOb
730
755
highlightedIndex: highlightedIndex,
731
756
thumbColor: thumbColor,
732
757
thumbScale: thumbScale,
758
+ proportionalWidth: proportionalWidth,
733
759
state: state,
734
760
);
735
761
}
@@ -740,7 +766,8 @@ class _SegmentedControlRenderWidget<T extends Object> extends MultiChildRenderOb
740
766
renderObject
741
767
..thumbColor = thumbColor
742
768
..thumbScale = thumbScale
743
- ..highlightedIndex = highlightedIndex;
769
+ ..highlightedIndex = highlightedIndex
770
+ ..proportionalWidth = proportionalWidth;
744
771
}
745
772
}
746
773
@@ -785,10 +812,12 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
785
812
required int ? highlightedIndex,
786
813
required Color thumbColor,
787
814
required double thumbScale,
815
+ required bool proportionalWidth,
788
816
required this .state,
789
817
}) : _highlightedIndex = highlightedIndex,
790
818
_thumbColor = thumbColor,
791
- _thumbScale = thumbScale;
819
+ _thumbScale = thumbScale,
820
+ _proportionalWidth = proportionalWidth;
792
821
793
822
final _SegmentedControlState <T > state;
794
823
@@ -841,6 +870,16 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
841
870
markNeedsPaint ();
842
871
}
843
872
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
+
844
883
@override
845
884
void handleEvent (PointerEvent event, BoxHitTestEntry entry) {
846
885
assert (debugHandleEvent (event, entry));
@@ -853,8 +892,29 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
853
892
}
854
893
855
894
// Intrinsic Dimensions
895
+ double get separatorWidth => _kSeparatorInset.horizontal + _kSeparatorWidth;
896
+ double get totalSeparatorWidth => separatorWidth * (childCount ~ / 2 );
856
897
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
+ }
858
918
859
919
RenderBox ? nonSeparatorChildAfter (RenderBox child) {
860
920
final RenderBox ? nextChild = childAfter (child);
@@ -923,62 +983,106 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
923
983
}
924
984
}
925
985
926
- Size _calculateChildSize (BoxConstraints constraints) {
986
+ double _getMaxChildWidth (BoxConstraints constraints) {
927
987
final int childCount = this .childCount ~ / 2 + 1 ;
928
988
double childWidth = (constraints.minWidth - totalSeparatorWidth) / childCount;
929
- double maxHeight = _kMinSegmentedControlHeight;
930
989
RenderBox ? child = firstChild;
931
990
while (child != null ) {
932
991
childWidth = math.max (childWidth, child.getMaxIntrinsicWidth (double .infinity) + 2 * _kSegmentMinPadding);
933
992
child = nonSeparatorChildAfter (child);
934
993
}
935
- childWidth = math.min (
994
+ return math.min (
936
995
childWidth,
937
996
(constraints.maxWidth - totalSeparatorWidth) / childCount,
938
997
);
939
- child = firstChild;
998
+ }
999
+
1000
+ double _getMaxChildHeight (BoxConstraints constraints, double childWidth) {
1001
+ double maxHeight = _kMinSegmentedControlHeight;
1002
+ RenderBox ? child = firstChild;
940
1003
while (child != null ) {
941
1004
final double boxHeight = child.getMaxIntrinsicHeight (childWidth);
942
1005
maxHeight = math.max (maxHeight, boxHeight);
943
1006
child = nonSeparatorChildAfter (child);
944
1007
}
945
- return Size (childWidth, maxHeight) ;
1008
+ return maxHeight;
946
1009
}
947
1010
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));
951
1048
}
952
1049
953
1050
@override
954
1051
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 );
957
1054
1055
+ int index = 0 ;
958
1056
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));
960
1060
baselineOffset = baselineOffset.minOf (BaselineOffset (child.getDryBaseline (childConstraints, baseline)));
1061
+
1062
+ child = nonSeparatorChildAfter (child);
1063
+ index++ ;
961
1064
}
1065
+
962
1066
return baselineOffset.offset;
963
1067
}
964
1068
965
1069
@override
966
1070
Size computeDryLayout (BoxConstraints constraints) {
967
- final Size childSize = _calculateChildSize (constraints);
968
- return _computeOverallSizeFromChildSize (childSize, constraints);
1071
+ return _computeOverallSize (constraints);
969
1072
}
970
1073
971
1074
@override
972
1075
void performLayout () {
973
1076
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);
977
1078
1079
+ final double childHeight = _getMaxChildHeight (constraints, double .infinity);
1080
+ final BoxConstraints separatorConstraints = BoxConstraints (minHeight: childHeight, maxHeight: childHeight);
978
1081
RenderBox ? child = firstChild;
979
1082
int index = 0 ;
980
1083
double start = 0 ;
981
1084
while (child != null ) {
1085
+ final BoxConstraints childConstraints = BoxConstraints .tight (Size (segmentWidths[index ~ / 2 ], childHeight));
982
1086
child.layout (index.isEven ? childConstraints : separatorConstraints, parentUsesSize: true );
983
1087
final _SegmentedControlContainerBoxParentData childParentData = child.parentData! as _SegmentedControlContainerBoxParentData ;
984
1088
final Offset childOffset = Offset (start, 0 );
@@ -991,8 +1095,7 @@ class _RenderSegmentedControl<T extends Object> extends RenderBox
991
1095
child = childAfter (child);
992
1096
index += 1 ;
993
1097
}
994
-
995
- size = _computeOverallSizeFromChildSize (childSize, constraints);
1098
+ size = _computeOverallSize (constraints);
996
1099
}
997
1100
998
1101
// This method is used to convert the original unscaled thumb rect painted in
0 commit comments