diff --git a/examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart b/examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart index caad200d02..776fe172f1 100644 --- a/examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart +++ b/examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart @@ -37,6 +37,9 @@ class SegmentedControlExample extends StatefulWidget { class _SegmentedControlExampleState extends State { Sky _selectedSegment = Sky.midnight; + bool _toggleOne = false; + bool _toggleAll = true; + Set _disabledChildren = {}; @override Widget build(BuildContext context) { @@ -45,6 +48,7 @@ class _SegmentedControlExampleState extends State { navigationBar: CupertinoNavigationBar( // This Cupertino segmented control has the enum "Sky" as the type. middle: CupertinoSegmentedControl( + disabledChildren: _disabledChildren, selectedColor: skyColors[_selectedSegment], // Provide horizontal padding around the children. padding: const EdgeInsets.symmetric(horizontal: 12), @@ -73,9 +77,60 @@ class _SegmentedControlExampleState extends State { ), ), child: Center( - child: Text( - 'Selected Segment: ${_selectedSegment.name}', - style: const TextStyle(color: CupertinoColors.white), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Selected Segment: ${_selectedSegment.name}', + style: const TextStyle(color: CupertinoColors.white), + ), + const SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Disable one segment', style: TextStyle(color: CupertinoColors.white)), + CupertinoSwitch( + value: _toggleOne, + onChanged: (bool value) { + setState(() { + _toggleOne = value; + if (value) { + _toggleAll = false; + _disabledChildren = {Sky.midnight}; + } else { + _toggleAll = true; + _disabledChildren = {}; + } + }); + }, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Toggle all segments', style: TextStyle(color: CupertinoColors.white)), + CupertinoSwitch( + value: _toggleAll, + onChanged: (bool value) { + setState(() { + _toggleAll = value; + if (value) { + _toggleOne = false; + _disabledChildren = {}; + } else { + _disabledChildren = { + Sky.midnight, + Sky.viridian, + Sky.cerulean, + }; + } + }); + }, + ), + ], + ), + ], ), ), ); diff --git a/examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart b/examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart index 3eb15abb6f..4b30f7e02f 100644 --- a/examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart +++ b/examples/api/test/cupertino/segmented_control/cupertino_segmented_control.0_test.dart @@ -2,10 +2,43 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/cupertino.dart'; import 'package:flutter_api_samples/cupertino/segmented_control/cupertino_segmented_control.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; void main() { + testWidgets('Verify initial state', (WidgetTester tester) async { + await tester.pumpWidget( + const example.SegmentedControlApp(), + ); + + // Midnight is the default selected segment. + expect(find.text('Selected Segment: midnight'), findsOneWidget); + + // All segments are enabled and can be selected. + await tester.tap(find.text('Viridian')); + await tester.pumpAndSettle(); + expect(find.text('Selected Segment: viridian'), findsOneWidget); + + await tester.tap(find.text('Cerulean')); + await tester.pumpAndSettle(); + expect(find.text('Selected Segment: cerulean'), findsOneWidget); + + await tester.tap(find.text('Midnight')); + await tester.pumpAndSettle(); + expect(find.text('Selected Segment: midnight'), findsOneWidget); + + // Verify that the first CupertinoSwitch is off. + final Finder firstSwitchFinder = find.byType(CupertinoSwitch).first; + final CupertinoSwitch firstSwitch = tester.widget(firstSwitchFinder); + expect(firstSwitch.value, false); + + // Verify that the second CupertinoSwitch is on. + final Finder secondSwitchFinder = find.byType(CupertinoSwitch).last; + final CupertinoSwitch secondSwitch = tester.widget(secondSwitchFinder); + expect(secondSwitch.value, true); + }); + testWidgets('Can change a selected segmented control', (WidgetTester tester) async { await tester.pumpWidget( const example.SegmentedControlApp(), @@ -18,4 +51,50 @@ void main() { expect(find.text('Selected Segment: cerulean'), findsOneWidget); }); + + testWidgets('Can not select on a disabled segment', (WidgetTester tester) async { + await tester.pumpWidget( + const example.SegmentedControlApp(), + ); + + // Toggle on the first CupertinoSwitch to disable the first segment. + final Finder firstSwitchFinder = find.byType(CupertinoSwitch).first; + await tester.tap(firstSwitchFinder); + await tester.pumpAndSettle(); + final CupertinoSwitch firstSwitch = tester.widget(firstSwitchFinder); + expect(firstSwitch.value, true); + + // Tap on the second segment then tap back on the first segment. + // Verify that the selected segment is still the second segment. + await tester.tap(find.text('Viridian')); + await tester.pumpAndSettle(); + expect(find.text('Selected Segment: viridian'), findsOneWidget); + + await tester.tap(find.text('Midnight')); + await tester.pumpAndSettle(); + expect(find.text('Selected Segment: viridian'), findsOneWidget); + }); + + testWidgets('Can not select on all disabled segments', (WidgetTester tester) async { + await tester.pumpWidget( + const example.SegmentedControlApp(), + ); + + // Toggle off the second CupertinoSwitch to disable all segments. + final Finder secondSwitchFinder = find.byType(CupertinoSwitch).last; + await tester.tap(secondSwitchFinder); + await tester.pumpAndSettle(); + final CupertinoSwitch secondSwitch = tester.widget(secondSwitchFinder); + expect(secondSwitch.value, false); + + // Tap on the second segment and verify that the selected segment is still the first segment. + await tester.tap(find.text('Viridian')); + await tester.pumpAndSettle(); + expect(find.text('Selected Segment: midnight'), findsOneWidget); + + // Tap on the third segment and verify that the selected segment is still the first segment. + await tester.tap(find.text('Cerulean')); + await tester.pumpAndSettle(); + expect(find.text('Selected Segment: midnight'), findsOneWidget); + }); } diff --git a/packages/flutter/lib/src/cupertino/segmented_control.dart b/packages/flutter/lib/src/cupertino/segmented_control.dart index ee9f4bfb40..b956a336b3 100644 --- a/packages/flutter/lib/src/cupertino/segmented_control.dart +++ b/packages/flutter/lib/src/cupertino/segmented_control.dart @@ -18,6 +18,9 @@ const EdgeInsetsGeometry _kHorizontalItemPadding = EdgeInsets.symmetric(horizont // Minimum height of the segmented control. const double _kMinSegmentedControlHeight = 28.0; +// The default color used for the text of the disabled segment. +const Color _kDisableTextColor = Color.fromARGB(115, 122, 122, 122); + // The duration of the fade animation used to transition when a new widget // is selected. const Duration _kFadeDuration = Duration(milliseconds: 165); @@ -57,17 +60,26 @@ const Duration _kFadeDuration = Duration(milliseconds: 165); /// A segmented control may optionally be created with custom colors. The /// [unselectedColor], [selectedColor], [borderColor], and [pressedColor] /// arguments can be used to override the segmented control's colors from -/// [CupertinoTheme] defaults. +/// [CupertinoTheme] defaults. The [disabledColor] and [disabledTextColor] +/// set the background and text colors of the segment when it is disabled. +/// +/// The segmented control can be disabled by adding children to the [Set] of +/// [disabledChildren]. If the child is not present in the [Set], it is enabled +/// by default. /// /// {@tool dartpad} /// This example shows a [CupertinoSegmentedControl] with an enum type. /// /// The callback provided to [onValueChanged] should update the state of /// the parent [StatefulWidget] using the [State.setState] method, so that -/// the parent gets rebuilt; for example: +/// the parent gets rebuilt. +/// +/// This example also demonstrates how to use the [disabledChildren] property by +/// toggling each [Switch] to enable or disable the segments. /// /// ** See code in examples/api/lib/cupertino/segmented_control/cupertino_segmented_control.0.dart ** /// {@end-tool} +/// /// See also: /// /// * [CupertinoSegmentedControl], a segmented control widget in the style used @@ -98,7 +110,10 @@ class CupertinoSegmentedControl extends StatefulWidget { this.selectedColor, this.borderColor, this.pressedColor, + this.disabledColor, + this.disabledTextColor, this.padding, + this.disabledChildren = const {}, }) : assert(children.length >= 2), assert( groupValue == null || children.keys.any((T child) => child == groupValue), @@ -148,11 +163,26 @@ class CupertinoSegmentedControl extends StatefulWidget { /// Defaults to the selectedColor at 20% opacity if null. final Color? pressedColor; + /// The color used to fill the background of the segment when it is disabled. + /// + /// If null, this color will be 50% opacity of the [selectedColor] when + /// the segment is selected. If the segment is unselected, this color will be + /// set to [unselectedColor]. + final Color? disabledColor; + + /// The color used for the text of the segment when it is disabled. + final Color? disabledTextColor; + /// The CupertinoSegmentedControl will be placed inside this padding. /// /// Defaults to EdgeInsets.symmetric(horizontal: 16.0) final EdgeInsetsGeometry? padding; + /// The set of identifying keys that correspond to the segments that should be disabled. + /// + /// All segments are enabled by default. + final Set disabledChildren; + @override State> createState() => _SegmentedControlState(); } @@ -172,6 +202,9 @@ class _SegmentedControlState extends State extends State extends State extends State extends State extends State children = { + 0: const Text('Child 1'), + 1: const Text('Child 2'), + 2: const Text('Child 3'), + }; + + final Set disabledChildren = {1}; + + int sharedValue = 0; + + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return boilerplate( + child: CupertinoSegmentedControl( + key: const ValueKey('Segmented Control'), + children: children, + disabledChildren: disabledChildren, + onValueChanged: (int newValue) { + setState(() { + sharedValue = newValue; + }); + }, + groupValue: sharedValue, + ), + ); + }, + ), + ); + + expect(sharedValue, 0); + + await tester.tap(find.text('Child 2')); + await tester.pumpAndSettle(); + + expect(sharedValue, 0); + }); + + testWidgets('Background color of disabled segment should be different than enabled segment', (WidgetTester tester) async { + final Map children = { + 0: const Text('Child 1'), + 1: const Text('Child 2'), + }; + int sharedValue = 0; + + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return boilerplate( + child: CupertinoSegmentedControl( + children: children, + disabledChildren: const {0}, + onValueChanged: (int newValue) { + setState(() { + sharedValue = newValue; + }); + }, + groupValue: sharedValue, + ), + ); + }, + ), + ); + + // Colors are different for disabled and enabled segments in initial state. + // By default, the first segment is selected (and also is disabled in this test), + // it should have a blue background (selected color) with 50% opacity + expect( + getBackgroundColor(tester, 0), + isSameColorAs(CupertinoColors.systemBlue.withOpacity(0.5)), + ); + expect(getBackgroundColor(tester, 1), isSameColorAs(CupertinoColors.white)); + + // Tap on disabled segment should not change its color + await tester.tap(find.text('Child 1')); + await tester.pumpAndSettle(); + + expect( + getBackgroundColor(tester, 0), + isSameColorAs(CupertinoColors.systemBlue.withOpacity(0.5)), + ); + + // When tapping on another enabled segment, the first disabled segment is not selected anymore, + // it should have a white background (same to unselected color). + await tester.tap(find.text('Child 2')); + await tester.pumpAndSettle(); + + expect(getBackgroundColor(tester, 0), isSameColorAs(CupertinoColors.white)); + }); + + testWidgets('Custom disabled color of disabled segment is showing as desired', (WidgetTester tester) async { + final Map children = { + 0: const Text('Child 1'), + 1: const Text('Child 2'), + 2: const Text('Child 3'), + }; + + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return boilerplate( + child: CupertinoSegmentedControl( + children: children, + disabledChildren: const {0}, + onValueChanged: (int newValue) {}, + disabledColor: CupertinoColors.systemGrey2, + ), + ); + }, + ), + ); + + expect( + getBackgroundColor(tester, 0), + isSameColorAs(CupertinoColors.systemGrey2), + ); + }); }