Skip to content

Commit fa52543

Browse files
authored
Add ability to disable CupertinoSegmentedControl (#152813)
### Summary Add the ability to configure enabled or disabled segments in CupertinoSegmentedControl. The idea is to pass a `segmentStates` map, where the user can set the state according to segment key. User can also set background and text colors when the segment is disabled. ### Demo https://github.com/user-attachments/assets/4a02da02-a0fb-4ded-a271-033a8dc79ac3 ### Related issue Fixes flutter/flutter#52105
1 parent 0167f02 commit fa52543

4 files changed

Lines changed: 315 additions & 9 deletions

File tree

examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class SegmentedControlExample extends StatefulWidget {
3737

3838
class _SegmentedControlExampleState extends State<SegmentedControlExample> {
3939
Sky _selectedSegment = Sky.midnight;
40+
bool _toggleOne = false;
41+
bool _toggleAll = true;
42+
Set<Sky> _disabledChildren = <Sky>{};
4043

4144
@override
4245
Widget build(BuildContext context) {
@@ -45,6 +48,7 @@ class _SegmentedControlExampleState extends State<SegmentedControlExample> {
4548
navigationBar: CupertinoNavigationBar(
4649
// This Cupertino segmented control has the enum "Sky" as the type.
4750
middle: CupertinoSegmentedControl<Sky>(
51+
disabledChildren: _disabledChildren,
4852
selectedColor: skyColors[_selectedSegment],
4953
// Provide horizontal padding around the children.
5054
padding: const EdgeInsets.symmetric(horizontal: 12),
@@ -73,9 +77,60 @@ class _SegmentedControlExampleState extends State<SegmentedControlExample> {
7377
),
7478
),
7579
child: Center(
76-
child: Text(
77-
'Selected Segment: ${_selectedSegment.name}',
78-
style: const TextStyle(color: CupertinoColors.white),
80+
child: Column(
81+
mainAxisAlignment: MainAxisAlignment.center,
82+
children: <Widget>[
83+
Text(
84+
'Selected Segment: ${_selectedSegment.name}',
85+
style: const TextStyle(color: CupertinoColors.white),
86+
),
87+
const SizedBox(height: 20),
88+
Row(
89+
mainAxisSize: MainAxisSize.min,
90+
children: <Widget>[
91+
const Text('Disable one segment', style: TextStyle(color: CupertinoColors.white)),
92+
CupertinoSwitch(
93+
value: _toggleOne,
94+
onChanged: (bool value) {
95+
setState(() {
96+
_toggleOne = value;
97+
if (value) {
98+
_toggleAll = false;
99+
_disabledChildren = <Sky>{Sky.midnight};
100+
} else {
101+
_toggleAll = true;
102+
_disabledChildren = <Sky>{};
103+
}
104+
});
105+
},
106+
),
107+
],
108+
),
109+
Row(
110+
mainAxisSize: MainAxisSize.min,
111+
children: <Widget>[
112+
const Text('Toggle all segments', style: TextStyle(color: CupertinoColors.white)),
113+
CupertinoSwitch(
114+
value: _toggleAll,
115+
onChanged: (bool value) {
116+
setState(() {
117+
_toggleAll = value;
118+
if (value) {
119+
_toggleOne = false;
120+
_disabledChildren = <Sky>{};
121+
} else {
122+
_disabledChildren = <Sky>{
123+
Sky.midnight,
124+
Sky.viridian,
125+
Sky.cerulean,
126+
};
127+
}
128+
});
129+
},
130+
),
131+
],
132+
),
133+
],
79134
),
80135
),
81136
);

examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,43 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/cupertino.dart';
56
import 'package:flutter_api_samples/cupertino/segmented_control/cupertino_segmented_control.0.dart' as example;
67
import 'package:flutter_test/flutter_test.dart';
78

89
void main() {
10+
testWidgets('Verify initial state', (WidgetTester tester) async {
11+
await tester.pumpWidget(
12+
const example.SegmentedControlApp(),
13+
);
14+
15+
// Midnight is the default selected segment.
16+
expect(find.text('Selected Segment: midnight'), findsOneWidget);
17+
18+
// All segments are enabled and can be selected.
19+
await tester.tap(find.text('Viridian'));
20+
await tester.pumpAndSettle();
21+
expect(find.text('Selected Segment: viridian'), findsOneWidget);
22+
23+
await tester.tap(find.text('Cerulean'));
24+
await tester.pumpAndSettle();
25+
expect(find.text('Selected Segment: cerulean'), findsOneWidget);
26+
27+
await tester.tap(find.text('Midnight'));
28+
await tester.pumpAndSettle();
29+
expect(find.text('Selected Segment: midnight'), findsOneWidget);
30+
31+
// Verify that the first CupertinoSwitch is off.
32+
final Finder firstSwitchFinder = find.byType(CupertinoSwitch).first;
33+
final CupertinoSwitch firstSwitch = tester.widget<CupertinoSwitch>(firstSwitchFinder);
34+
expect(firstSwitch.value, false);
35+
36+
// Verify that the second CupertinoSwitch is on.
37+
final Finder secondSwitchFinder = find.byType(CupertinoSwitch).last;
38+
final CupertinoSwitch secondSwitch = tester.widget<CupertinoSwitch>(secondSwitchFinder);
39+
expect(secondSwitch.value, true);
40+
});
41+
942
testWidgets('Can change a selected segmented control', (WidgetTester tester) async {
1043
await tester.pumpWidget(
1144
const example.SegmentedControlApp(),
@@ -18,4 +51,50 @@ void main() {
1851

1952
expect(find.text('Selected Segment: cerulean'), findsOneWidget);
2053
});
54+
55+
testWidgets('Can not select on a disabled segment', (WidgetTester tester) async {
56+
await tester.pumpWidget(
57+
const example.SegmentedControlApp(),
58+
);
59+
60+
// Toggle on the first CupertinoSwitch to disable the first segment.
61+
final Finder firstSwitchFinder = find.byType(CupertinoSwitch).first;
62+
await tester.tap(firstSwitchFinder);
63+
await tester.pumpAndSettle();
64+
final CupertinoSwitch firstSwitch = tester.widget<CupertinoSwitch>(firstSwitchFinder);
65+
expect(firstSwitch.value, true);
66+
67+
// Tap on the second segment then tap back on the first segment.
68+
// Verify that the selected segment is still the second segment.
69+
await tester.tap(find.text('Viridian'));
70+
await tester.pumpAndSettle();
71+
expect(find.text('Selected Segment: viridian'), findsOneWidget);
72+
73+
await tester.tap(find.text('Midnight'));
74+
await tester.pumpAndSettle();
75+
expect(find.text('Selected Segment: viridian'), findsOneWidget);
76+
});
77+
78+
testWidgets('Can not select on all disabled segments', (WidgetTester tester) async {
79+
await tester.pumpWidget(
80+
const example.SegmentedControlApp(),
81+
);
82+
83+
// Toggle off the second CupertinoSwitch to disable all segments.
84+
final Finder secondSwitchFinder = find.byType(CupertinoSwitch).last;
85+
await tester.tap(secondSwitchFinder);
86+
await tester.pumpAndSettle();
87+
final CupertinoSwitch secondSwitch = tester.widget<CupertinoSwitch>(secondSwitchFinder);
88+
expect(secondSwitch.value, false);
89+
90+
// Tap on the second segment and verify that the selected segment is still the first segment.
91+
await tester.tap(find.text('Viridian'));
92+
await tester.pumpAndSettle();
93+
expect(find.text('Selected Segment: midnight'), findsOneWidget);
94+
95+
// Tap on the third segment and verify that the selected segment is still the first segment.
96+
await tester.tap(find.text('Cerulean'));
97+
await tester.pumpAndSettle();
98+
expect(find.text('Selected Segment: midnight'), findsOneWidget);
99+
});
21100
}

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

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const EdgeInsetsGeometry _kHorizontalItemPadding = EdgeInsets.symmetric(horizont
1818
// Minimum height of the segmented control.
1919
const double _kMinSegmentedControlHeight = 28.0;
2020

21+
// The default color used for the text of the disabled segment.
22+
const Color _kDisableTextColor = Color.fromARGB(115, 122, 122, 122);
23+
2124
// The duration of the fade animation used to transition when a new widget
2225
// is selected.
2326
const Duration _kFadeDuration = Duration(milliseconds: 165);
@@ -57,17 +60,26 @@ const Duration _kFadeDuration = Duration(milliseconds: 165);
5760
/// A segmented control may optionally be created with custom colors. The
5861
/// [unselectedColor], [selectedColor], [borderColor], and [pressedColor]
5962
/// arguments can be used to override the segmented control's colors from
60-
/// [CupertinoTheme] defaults.
63+
/// [CupertinoTheme] defaults. The [disabledColor] and [disabledTextColor]
64+
/// set the background and text colors of the segment when it is disabled.
65+
///
66+
/// The segmented control can be disabled by adding children to the [Set] of
67+
/// [disabledChildren]. If the child is not present in the [Set], it is enabled
68+
/// by default.
6169
///
6270
/// {@tool dartpad}
6371
/// This example shows a [CupertinoSegmentedControl] with an enum type.
6472
///
6573
/// The callback provided to [onValueChanged] should update the state of
6674
/// the parent [StatefulWidget] using the [State.setState] method, so that
67-
/// the parent gets rebuilt; for example:
75+
/// the parent gets rebuilt.
76+
///
77+
/// This example also demonstrates how to use the [disabledChildren] property by
78+
/// toggling each [Switch] to enable or disable the segments.
6879
///
6980
/// ** See code in examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart **
7081
/// {@end-tool}
82+
///
7183
/// See also:
7284
///
7385
/// * [CupertinoSegmentedControl], a segmented control widget in the style used
@@ -98,7 +110,10 @@ class CupertinoSegmentedControl<T extends Object> extends StatefulWidget {
98110
this.selectedColor,
99111
this.borderColor,
100112
this.pressedColor,
113+
this.disabledColor,
114+
this.disabledTextColor,
101115
this.padding,
116+
this.disabledChildren = const <Never>{},
102117
}) : assert(children.length >= 2),
103118
assert(
104119
groupValue == null || children.keys.any((T child) => child == groupValue),
@@ -148,11 +163,26 @@ class CupertinoSegmentedControl<T extends Object> extends StatefulWidget {
148163
/// Defaults to the selectedColor at 20% opacity if null.
149164
final Color? pressedColor;
150165

166+
/// The color used to fill the background of the segment when it is disabled.
167+
///
168+
/// If null, this color will be 50% opacity of the [selectedColor] when
169+
/// the segment is selected. If the segment is unselected, this color will be
170+
/// set to [unselectedColor].
171+
final Color? disabledColor;
172+
173+
/// The color used for the text of the segment when it is disabled.
174+
final Color? disabledTextColor;
175+
151176
/// The CupertinoSegmentedControl will be placed inside this padding.
152177
///
153178
/// Defaults to EdgeInsets.symmetric(horizontal: 16.0)
154179
final EdgeInsetsGeometry? padding;
155180

181+
/// The set of identifying keys that correspond to the segments that should be disabled.
182+
///
183+
/// All segments are enabled by default.
184+
final Set<T> disabledChildren;
185+
156186
@override
157187
State<CupertinoSegmentedControl<T>> createState() => _SegmentedControlState<T>();
158188
}
@@ -172,6 +202,9 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSegmentedC
172202
Color? _unselectedColor;
173203
Color? _borderColor;
174204
Color? _pressedColor;
205+
Color? _selectedDisabledColor;
206+
Color? _unselectedDisabledColor;
207+
Color? _disabledTextColor;
175208

176209
AnimationController createAnimationController() {
177210
return AnimationController(
@@ -187,6 +220,11 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSegmentedC
187220
bool _updateColors() {
188221
assert(mounted, 'This should only be called after didUpdateDependencies');
189222
bool changed = false;
223+
final Color disabledTextColor = widget.disabledTextColor ?? _kDisableTextColor;
224+
if (_disabledTextColor != disabledTextColor) {
225+
changed = true;
226+
_disabledTextColor = disabledTextColor;
227+
}
190228
final Color selectedColor = widget.selectedColor ?? CupertinoTheme.of(context).primaryColor;
191229
if (_selectedColor != selectedColor) {
192230
changed = true;
@@ -197,6 +235,13 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSegmentedC
197235
changed = true;
198236
_unselectedColor = unselectedColor;
199237
}
238+
final Color selectedDisabledColor = widget.disabledColor ?? selectedColor.withOpacity(0.5);
239+
final Color unselectedDisabledColor = widget.disabledColor ?? unselectedColor;
240+
if (_selectedDisabledColor != selectedDisabledColor || _unselectedDisabledColor != unselectedDisabledColor) {
241+
changed = true;
242+
_selectedDisabledColor = selectedDisabledColor;
243+
_unselectedDisabledColor = unselectedDisabledColor;
244+
}
200245
final Color borderColor = widget.borderColor ?? CupertinoTheme.of(context).primaryColor;
201246
if (_borderColor != borderColor) {
202247
changed = true;
@@ -302,13 +347,18 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSegmentedC
302347
if (currentKey != _pressedKey) {
303348
return;
304349
}
305-
if (currentKey != widget.groupValue) {
306-
widget.onValueChanged(currentKey);
350+
if (!widget.disabledChildren.contains(currentKey)) {
351+
if (currentKey != widget.groupValue) {
352+
widget.onValueChanged(currentKey);
353+
}
307354
}
308355
_pressedKey = null;
309356
}
310357

311358
Color? getTextColor(int index, T currentKey) {
359+
if (widget.disabledChildren.contains(currentKey)) {
360+
return _disabledTextColor;
361+
}
312362
if (_selectionControllers[index].isAnimating) {
313363
return _textColorTween.evaluate(_selectionControllers[index]);
314364
}
@@ -319,6 +369,9 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSegmentedC
319369
}
320370

321371
Color? getBackgroundColor(int index, T currentKey) {
372+
if (widget.disabledChildren.contains(currentKey)) {
373+
return widget.groupValue == currentKey ? _selectedDisabledColor : _unselectedDisabledColor;
374+
}
322375
if (_selectionControllers[index].isAnimating) {
323376
return _childTweens[index].evaluate(_selectionControllers[index]);
324377
}
@@ -357,10 +410,10 @@ class _SegmentedControlState<T extends Object> extends State<CupertinoSegmentedC
357410
cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
358411
child: GestureDetector(
359412
behavior: HitTestBehavior.opaque,
360-
onTapDown: (TapDownDetails event) {
413+
onTapDown: widget.disabledChildren.contains(currentKey) ? null : (TapDownDetails event) {
361414
_onTapDown(currentKey);
362415
},
363-
onTapCancel: _onTapCancel,
416+
onTapCancel: widget.disabledChildren.contains(currentKey) ? null : _onTapCancel,
364417
onTap: () {
365418
_onTap(currentKey);
366419
},

0 commit comments

Comments
 (0)