From 83ac76050d094ca93518788ffdb02762c05ac468 Mon Sep 17 00:00:00 2001
From: Furkan Acar <65075121+AcarFurkan@users.noreply.github.com>
Date: Thu, 4 Jan 2024 00:26:02 +0300
Subject: [PATCH] Add `SegmentedButton.styleFrom` (#137542)
fixes https://github.com/flutter/flutter/issues/138289
---
SegmentedButtom.styleFrom has been added to the segment button, so there is no longer any need to the button style from the beginning. It works like ElevatedButton.styleFrom only I added selectedForegroundColor, selectedBackgroundColor. In this way, the user will be able to change the color first without checking the MaterialState states. I added tests of the same controls.
#129215 I opened this problem myself, but I was rejected because I handled too many items in a PR. For now, I wrote a structure that only handles MaterialStates instead of users.
old (still avaliable)
new (just an option for developer)
### Code sample
expand to view the code sample
```dart
import 'package:flutter/material.dart';
/// Flutter code sample for [SegmentedButton].
void main() {
runApp(const SegmentedButtonApp());
}
enum Calendar { day, week, month, year }
class SegmentedButtonApp extends StatefulWidget {
const SegmentedButtonApp({super.key});
@override
State createState() => _SegmentedButtonAppState();
}
class _SegmentedButtonAppState extends State {
Calendar calendarView = Calendar.day;
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: Scaffold(
body: Center(
child: SegmentedButton(
style: SegmentedButton.styleFrom(
foregroundColor: Colors.amber,
visualDensity: VisualDensity.comfortable,
),
// style: const ButtonStyle(
// foregroundColor: MaterialStatePropertyAll(Colors.deepPurple),
// visualDensity: VisualDensity.comfortable,
// ),
segments: const >[
ButtonSegment(
value: Calendar.day,
label: Text('Day'),
icon: Icon(Icons.calendar_view_day)),
ButtonSegment(
value: Calendar.week,
label: Text('Week'),
icon: Icon(Icons.calendar_view_week)),
ButtonSegment(
value: Calendar.month,
label: Text('Month'),
icon: Icon(Icons.calendar_view_month)),
ButtonSegment(
value: Calendar.year,
label: Text('Year'),
icon: Icon(Icons.calendar_today)),
],
selected: {calendarView},
onSelectionChanged: (Set newSelection) {
setState(() {
calendarView = newSelection.first;
});
},
),
),
),
);
}
}
```
---
.../lib/segmented_button_template.dart | 27 +++
.../segmented_button/segmented_button.1.dart | 66 +++++++
.../segmented_button.1_test.dart | 36 ++++
.../lib/src/material/segmented_button.dart | 174 ++++++++++++++++++
.../test/material/segmented_button_test.dart | 114 ++++++++++++
5 files changed, 417 insertions(+)
create mode 100644 examples/api/lib/material/segmented_button/segmented_button.1.dart
create mode 100644 examples/api/test/material/segmented_button/segmented_button.1_test.dart
diff --git a/dev/tools/gen_defaults/lib/segmented_button_template.dart b/dev/tools/gen_defaults/lib/segmented_button_template.dart
index 51f859a67d..8f9e8f6a3d 100644
--- a/dev/tools/gen_defaults/lib/segmented_button_template.dart
+++ b/dev/tools/gen_defaults/lib/segmented_button_template.dart
@@ -119,6 +119,33 @@ class _${blockName}DefaultsM3 extends SegmentedButtonThemeData {
}
@override
Widget? get selectedIcon => const Icon(Icons.check);
+
+ static MaterialStateProperty resolveStateColor(Color? unselectedColor, Color? selectedColor){
+ return MaterialStateProperty.resolveWith((Set states) {
+ if (states.contains(MaterialState.selected)) {
+ if (states.contains(MaterialState.pressed)) {
+ return selectedColor?.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return selectedColor?.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return selectedColor?.withOpacity(0.12);
+ }
+ } else {
+ if (states.contains(MaterialState.pressed)) {
+ return unselectedColor?.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return unselectedColor?.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return unselectedColor?.withOpacity(0.12);
+ }
+ }
+ return Colors.transparent;
+ });
+ }
}
''';
}
diff --git a/examples/api/lib/material/segmented_button/segmented_button.1.dart b/examples/api/lib/material/segmented_button/segmented_button.1.dart
new file mode 100644
index 0000000000..c277e55f56
--- /dev/null
+++ b/examples/api/lib/material/segmented_button/segmented_button.1.dart
@@ -0,0 +1,66 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+/// Flutter code sample for [SegmentedButton.styleFrom].
+
+void main() {
+ runApp(const SegmentedButtonApp());
+}
+
+class SegmentedButtonApp extends StatelessWidget {
+ const SegmentedButtonApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: SegmentedButtonExample(),
+ ),
+ ),
+ );
+ }
+}
+
+class SegmentedButtonExample extends StatefulWidget {
+ const SegmentedButtonExample({super.key});
+
+ @override
+ State createState() => _SegmentedButtonExampleState();
+}
+
+enum Calendar { day, week, month, year }
+
+class _SegmentedButtonExampleState extends State {
+ Calendar calendarView = Calendar.week;
+
+ @override
+ Widget build(BuildContext context) {
+ return SegmentedButton(
+ style: SegmentedButton.styleFrom(
+ backgroundColor: Colors.grey[200],
+ foregroundColor: Colors.red,
+ selectedForegroundColor: Colors.white,
+ selectedBackgroundColor: Colors.green,
+ ),
+ segments: const >[
+ ButtonSegment(value: Calendar.day, label: Text('Day'), icon: Icon(Icons.calendar_view_day)),
+ ButtonSegment(value: Calendar.week, label: Text('Week'), icon: Icon(Icons.calendar_view_week)),
+ ButtonSegment(value: Calendar.month, label: Text('Month'), icon: Icon(Icons.calendar_view_month)),
+ ButtonSegment(value: Calendar.year, label: Text('Year'), icon: Icon(Icons.calendar_today)),
+ ],
+ selected: {calendarView},
+ onSelectionChanged: (Set newSelection) {
+ setState(() {
+ // By default there is only a single segment that can be
+ // selected at one time, so its value is always the first
+ // item in the selected set.
+ calendarView = newSelection.first;
+ });
+ },
+ );
+ }
+}
diff --git a/examples/api/test/material/segmented_button/segmented_button.1_test.dart b/examples/api/test/material/segmented_button/segmented_button.1_test.dart
new file mode 100644
index 0000000000..885bf0e722
--- /dev/null
+++ b/examples/api/test/material/segmented_button/segmented_button.1_test.dart
@@ -0,0 +1,36 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_api_samples/material/segmented_button/segmented_button.1.dart'
+ as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets('Can use SegmentedButton.styleFrom to customize SegmentedButton', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const example.SegmentedButtonApp(),
+ );
+
+ final Color unselectedBackgroundColor = Colors.grey[200]!;
+ const Color unselectedForegroundColor = Colors.red;
+ const Color selectedBackgroundColor = Colors.green;
+ const Color selectedForegroundColor = Colors.white;
+
+ Material getMaterial(String text) {
+ return tester.widget(find.ancestor(
+ of: find.text(text),
+ matching: find.byType(Material),
+ ).first);
+ }
+
+ // Verify the unselected button style.
+ expect(getMaterial('Day').textStyle?.color, unselectedForegroundColor);
+ expect(getMaterial('Day').color, unselectedBackgroundColor);
+
+ // Verify the selected button style.
+ expect(getMaterial('Week').textStyle?.color, selectedForegroundColor);
+ expect(getMaterial('Week').color, selectedBackgroundColor);
+ });
+}
diff --git a/packages/flutter/lib/src/material/segmented_button.dart b/packages/flutter/lib/src/material/segmented_button.dart
index a68c372135..917526d35b 100644
--- a/packages/flutter/lib/src/material/segmented_button.dart
+++ b/packages/flutter/lib/src/material/segmented_button.dart
@@ -8,15 +8,18 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
+import 'button_style_button.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'icons.dart';
+import 'ink_well.dart';
import 'material.dart';
import 'material_state.dart';
import 'segmented_button_theme.dart';
import 'text_button.dart';
import 'text_button_theme.dart';
import 'theme.dart';
+import 'theme_data.dart';
import 'tooltip.dart';
/// Data describing a segment of a [SegmentedButton].
@@ -86,6 +89,12 @@ class ButtonSegment {
/// ** See code in examples/api/lib/material/segmented_button/segmented_button.0.dart **
/// {@end-tool}
///
+/// {@tool dartpad}
+/// This sample showcases how to customize [SegmentedButton] using [SegmentedButton.styleFrom].
+///
+/// ** See code in examples/api/lib/material/segmented_button/segmented_button.1.dart **
+/// {@end-tool}
+///
/// See also:
///
/// * Material Design spec:
@@ -178,6 +187,123 @@ class SegmentedButton extends StatefulWidget {
/// [onSelectionChanged] will not be called.
final bool emptySelectionAllowed;
+ /// A static convenience method that constructs a segmented button
+ /// [ButtonStyle] given simple values.
+ ///
+ /// The [foregroundColor], [selectedForegroundColor], and [disabledForegroundColor]
+ /// colors are used to create a [MaterialStateProperty] [ButtonStyle.foregroundColor],
+ /// and a derived [ButtonStyle.overlayColor].
+ ///
+ /// The [backgroundColor], [selectedBackgroundColor] and [disabledBackgroundColor]
+ /// colors are used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
+ ///
+ /// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
+ /// parameters are used to construct [ButtonStyle.mouseCursor].
+ ///
+ /// All of the other parameters are either used directly or used to
+ /// create a [MaterialStateProperty] with a single value for all
+ /// states.
+ ///
+ /// All parameters default to null. By default this method returns
+ /// a [ButtonStyle] that doesn't override anything.
+ ///
+ /// {@tool snippet}
+ ///
+ /// For example, to override the default text and icon colors for a
+ /// [SegmentedButton], as well as its overlay color, with all of the
+ /// standard opacity adjustments for the pressed, focused, and
+ /// hovered states, one could write:
+ ///
+ /// ** See code in examples/api/lib/material/segmented_button/segmented_button.1.dart **
+ ///
+ /// ```dart
+ /// SegmentedButton(
+ /// style: SegmentedButton.styleFrom(
+ /// foregroundColor: Colors.black,
+ /// selectedForegroundColor: Colors.white,
+ /// backgroundColor: Colors.amber,
+ /// selectedBackgroundColor: Colors.red,
+ /// ),
+ /// segments: const >[
+ /// ButtonSegment(
+ /// value: 0,
+ /// label: Text('0'),
+ /// icon: Icon(Icons.calendar_view_day),
+ /// ),
+ /// ButtonSegment(
+ /// value: 1,
+ /// label: Text('1'),
+ /// icon: Icon(Icons.calendar_view_week),
+ /// ),
+ /// ],
+ /// selected: const {0},
+ /// onSelectionChanged: (Set selection) {},
+ /// ),
+ /// ```
+ /// {@end-tool}
+ static ButtonStyle styleFrom({
+ Color? foregroundColor,
+ Color? backgroundColor,
+ Color? selectedForegroundColor,
+ Color? selectedBackgroundColor,
+ Color? disabledForegroundColor,
+ Color? disabledBackgroundColor,
+ Color? shadowColor,
+ Color? surfaceTintColor,
+ double? elevation,
+ TextStyle? textStyle,
+ EdgeInsetsGeometry? padding,
+ Size? minimumSize,
+ Size? fixedSize,
+ Size? maximumSize,
+ BorderSide? side,
+ OutlinedBorder? shape,
+ MouseCursor? enabledMouseCursor,
+ MouseCursor? disabledMouseCursor,
+ VisualDensity? visualDensity,
+ MaterialTapTargetSize? tapTargetSize,
+ Duration? animationDuration,
+ bool? enableFeedback,
+ AlignmentGeometry? alignment,
+ InteractiveInkFeatureFactory? splashFactory,
+ }) {
+ final MaterialStateProperty? foregroundColorProp =
+ (foregroundColor == null && disabledForegroundColor == null && selectedForegroundColor == null)
+ ? null
+ : _SegmentButtonDefaultColor(foregroundColor, disabledForegroundColor, selectedForegroundColor);
+ final MaterialStateProperty? backgroundColorProp =
+ (backgroundColor == null && disabledBackgroundColor == null && selectedBackgroundColor == null)
+ ? null
+ : _SegmentButtonDefaultColor(backgroundColor, disabledBackgroundColor, selectedBackgroundColor);
+ final MaterialStateProperty? overlayColor = (foregroundColor == null && selectedForegroundColor == null)
+ ? null
+ : _SegmentedButtonDefaultsM3.resolveStateColor(foregroundColor, selectedForegroundColor);
+ return TextButton.styleFrom(
+ textStyle: textStyle,
+ shadowColor: shadowColor,
+ surfaceTintColor: surfaceTintColor,
+ elevation: elevation,
+ padding: padding,
+ minimumSize: minimumSize,
+ fixedSize: fixedSize,
+ maximumSize: maximumSize,
+ side: side,
+ shape: shape,
+ enabledMouseCursor: enabledMouseCursor,
+ disabledMouseCursor: disabledMouseCursor,
+ visualDensity: visualDensity,
+ tapTargetSize: tapTargetSize,
+ animationDuration: animationDuration,
+ enableFeedback: enableFeedback,
+ alignment: alignment,
+ splashFactory: splashFactory,
+ ).copyWith(
+ foregroundColor: foregroundColorProp,
+ backgroundColor: backgroundColorProp,
+ overlayColor: overlayColor,
+ );
+ }
+
/// Customizes this button's appearance.
///
/// The following style properties apply to the entire segmented button:
@@ -417,6 +543,27 @@ class SegmentedButtonState extends State> {
super.dispose();
}
}
+
+@immutable
+class _SegmentButtonDefaultColor extends MaterialStateProperty with Diagnosticable {
+ _SegmentButtonDefaultColor(this.color, this.disabled, this.selected);
+
+ final Color? color;
+ final Color? disabled;
+ final Color? selected;
+
+ @override
+ Color? resolve(Set states) {
+ if (states.contains(MaterialState.disabled)) {
+ return disabled;
+ }
+ if (states.contains(MaterialState.selected)) {
+ return selected;
+ }
+ return color;
+ }
+}
+
class _SegmentedButtonRenderWidget extends MultiChildRenderObjectWidget {
const _SegmentedButtonRenderWidget({
super.key,
@@ -842,6 +989,33 @@ class _SegmentedButtonDefaultsM3 extends SegmentedButtonThemeData {
}
@override
Widget? get selectedIcon => const Icon(Icons.check);
+
+ static MaterialStateProperty resolveStateColor(Color? unselectedColor, Color? selectedColor){
+ return MaterialStateProperty.resolveWith((Set states) {
+ if (states.contains(MaterialState.selected)) {
+ if (states.contains(MaterialState.pressed)) {
+ return selectedColor?.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return selectedColor?.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return selectedColor?.withOpacity(0.12);
+ }
+ } else {
+ if (states.contains(MaterialState.pressed)) {
+ return unselectedColor?.withOpacity(0.12);
+ }
+ if (states.contains(MaterialState.hovered)) {
+ return unselectedColor?.withOpacity(0.08);
+ }
+ if (states.contains(MaterialState.focused)) {
+ return unselectedColor?.withOpacity(0.12);
+ }
+ }
+ return Colors.transparent;
+ });
+ }
}
// END GENERATED TOKEN PROPERTIES - SegmentedButton
diff --git a/packages/flutter/test/material/segmented_button_test.dart b/packages/flutter/test/material/segmented_button_test.dart
index 61ff0498a7..6fa33b08fb 100644
--- a/packages/flutter/test/material/segmented_button_test.dart
+++ b/packages/flutter/test/material/segmented_button_test.dart
@@ -664,4 +664,118 @@ testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTes
expect(find.byTooltip('t2'), findsOneWidget);
expect(find.byTooltip('t3'), findsOneWidget);
});
+
+ testWidgets('SegmentedButton.styleFrom is applied to the SegmentedButton', (WidgetTester tester) async {
+ const Color foregroundColor = Color(0xfffffff0);
+ const Color backgroundColor = Color(0xfffffff1);
+ const Color selectedBackgroundColor = Color(0xfffffff2);
+ const Color selectedForegroundColor = Color(0xfffffff3);
+ const Color disabledBackgroundColor = Color(0xfffffff4);
+ const Color disabledForegroundColor = Color(0xfffffff5);
+ const MouseCursor enabledMouseCursor = SystemMouseCursors.text;
+ const MouseCursor disabledMouseCursor = SystemMouseCursors.grab;
+
+ final ButtonStyle styleFromStyle = SegmentedButton.styleFrom(
+ foregroundColor: foregroundColor,
+ backgroundColor: backgroundColor,
+ selectedForegroundColor: selectedForegroundColor,
+ selectedBackgroundColor: selectedBackgroundColor,
+ disabledForegroundColor: disabledForegroundColor,
+ disabledBackgroundColor: disabledBackgroundColor,
+ shadowColor: const Color(0xfffffff6),
+ surfaceTintColor: const Color(0xfffffff7),
+ elevation: 1,
+ textStyle: const TextStyle(color: Color(0xfffffff8)),
+ padding: const EdgeInsets.all(2),
+ side: const BorderSide(color: Color(0xfffffff9)),
+ shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(3))),
+ enabledMouseCursor: enabledMouseCursor,
+ disabledMouseCursor: disabledMouseCursor,
+ visualDensity: VisualDensity.compact,
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ animationDuration: const Duration(milliseconds: 100),
+ enableFeedback: true,
+ alignment: Alignment.center,
+ splashFactory: NoSplash.splashFactory,
+ );
+
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: Center(
+ child: SegmentedButton(
+ style: styleFromStyle,
+ segments: const >[
+ ButtonSegment(value: 1, label: Text('1')),
+ ButtonSegment(value: 2, label: Text('2')),
+ ButtonSegment(value: 3, label: Text('3'), enabled: false),
+ ],
+ selected: const {2},
+ onSelectionChanged: (Set selected) { },
+ selectedIcon: const Icon(Icons.alarm),
+ ),
+ ),
+ ),
+ ));
+
+ // Test provided button style is applied to the enabled button segment.
+ ButtonStyle? buttonStyle = tester.widget(find.byType(TextButton).first).style;
+ expect(buttonStyle?.foregroundColor?.resolve(enabled), foregroundColor);
+ expect(buttonStyle?.backgroundColor?.resolve(enabled), backgroundColor);
+ expect(buttonStyle?.overlayColor, styleFromStyle.overlayColor);
+ expect(buttonStyle?.surfaceTintColor, styleFromStyle.surfaceTintColor);
+ expect(buttonStyle?.elevation, styleFromStyle.elevation);
+ expect(buttonStyle?.textStyle, styleFromStyle.textStyle);
+ expect(buttonStyle?.padding, styleFromStyle.padding);
+ expect(buttonStyle?.mouseCursor?.resolve(enabled), enabledMouseCursor);
+ expect(buttonStyle?.visualDensity, styleFromStyle.visualDensity);
+ expect(buttonStyle?.tapTargetSize, styleFromStyle.tapTargetSize);
+ expect(buttonStyle?.animationDuration, styleFromStyle.animationDuration);
+ expect(buttonStyle?.enableFeedback, styleFromStyle.enableFeedback);
+ expect(buttonStyle?.alignment, styleFromStyle.alignment);
+ expect(buttonStyle?.splashFactory, styleFromStyle.splashFactory);
+
+ // Test provided button style is applied selected button segment.
+ buttonStyle = tester.widget(find.byType(TextButton).at(1)).style;
+ expect(buttonStyle?.foregroundColor?.resolve(selected), selectedForegroundColor);
+ expect(buttonStyle?.backgroundColor?.resolve(selected), selectedBackgroundColor);
+ expect(buttonStyle?.mouseCursor?.resolve(enabled), enabledMouseCursor);
+
+ // Test provided button style is applied disabled button segment.
+ buttonStyle = tester.widget(find.byType(TextButton).last).style;
+ expect(buttonStyle?.foregroundColor?.resolve(disabled), disabledForegroundColor);
+ expect(buttonStyle?.backgroundColor?.resolve(disabled), disabledBackgroundColor);
+ expect(buttonStyle?.mouseCursor?.resolve(disabled), disabledMouseCursor);
+
+ // Test provided button style is applied to the segmented button material.
+ final Material material = tester.widget(find.descendant(
+ of: find.byType(SegmentedButton),
+ matching: find.byType(Material),
+ ).first);
+ expect(material.shape, styleFromStyle.shape?.resolve(enabled)?.copyWith(side: BorderSide.none));
+ expect(material.elevation, styleFromStyle.elevation?.resolve(enabled));
+ expect(material.shadowColor, styleFromStyle.shadowColor?.resolve(enabled));
+ expect(material.surfaceTintColor, styleFromStyle.surfaceTintColor?.resolve(enabled));
+
+ // Test provided button style border is applied to the segmented button border.
+ expect(
+ find.byType(SegmentedButton),
+ paints..line(color: styleFromStyle.side?.resolve(enabled)?.color),
+ );
+
+ // Test foreground color is applied to the overlay color.
+ RenderObject overlayColor() {
+ return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
+ }
+ final TestGesture gesture = await tester.createGesture(
+ kind: PointerDeviceKind.mouse,
+ );
+ await gesture.addPointer();
+ await gesture.down(tester.getCenter(find.text('1')));
+ await tester.pumpAndSettle();
+ expect(overlayColor(), paints..rect(color: foregroundColor.withOpacity(0.08)));
+ });
}
+
+Set enabled = const {};
+Set disabled = const { MaterialState.disabled };
+Set selected = const { MaterialState.selected };