Unverified Commit 83ac7605 authored by Furkan Acar's avatar Furkan Acar Committed by GitHub

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)
<img width="626" alt="image" src="https://github.com/flutter/flutter/assets/65075121/9446b13b-c355-4d20-bda2-c47a23d42d4f">

new (just an option for developer)
<img width="483" alt="image" src="https://github.com/flutter/flutter/assets/65075121/0a645257-4c83-4029-9484-bd746c02265f">

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```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<SegmentedButtonApp> createState() => _SegmentedButtonAppState();
}

class _SegmentedButtonAppState extends State<SegmentedButtonApp> {
  Calendar calendarView = Calendar.day;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: Scaffold(
        body: Center(
          child: SegmentedButton<Calendar>(
            style: SegmentedButton.styleFrom(
              foregroundColor: Colors.amber,
              visualDensity: VisualDensity.comfortable,
            ),
            // style: const ButtonStyle(
            //   foregroundColor: MaterialStatePropertyAll<Color>(Colors.deepPurple),
            //   visualDensity: VisualDensity.comfortable,
            // ),
            segments: const <ButtonSegment<Calendar>>[
              ButtonSegment<Calendar>(
                  value: Calendar.day,
                  label: Text('Day'),
                  icon: Icon(Icons.calendar_view_day)),
              ButtonSegment<Calendar>(
                  value: Calendar.week,
                  label: Text('Week'),
                  icon: Icon(Icons.calendar_view_week)),
              ButtonSegment<Calendar>(
                  value: Calendar.month,
                  label: Text('Month'),
                  icon: Icon(Icons.calendar_view_month)),
              ButtonSegment<Calendar>(
                  value: Calendar.year,
                  label: Text('Year'),
                  icon: Icon(Icons.calendar_today)),
            ],
            selected: <Calendar>{calendarView},
            onSelectionChanged: (Set<Calendar> newSelection) {
              setState(() {
                calendarView = newSelection.first;
              });
            },
          ),
        ),
      ),
    );
  }
}

```

</details>
parent e64e7d9c
......@@ -119,6 +119,33 @@ class _${blockName}DefaultsM3 extends SegmentedButtonThemeData {
}
@override
Widget? get selectedIcon => const Icon(Icons.check);
static MaterialStateProperty<Color?> resolveStateColor(Color? unselectedColor, Color? selectedColor){
return MaterialStateProperty.resolveWith((Set<MaterialState> 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;
});
}
}
''';
}
// 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<SegmentedButtonExample> createState() => _SegmentedButtonExampleState();
}
enum Calendar { day, week, month, year }
class _SegmentedButtonExampleState extends State<SegmentedButtonExample> {
Calendar calendarView = Calendar.week;
@override
Widget build(BuildContext context) {
return SegmentedButton<Calendar>(
style: SegmentedButton.styleFrom(
backgroundColor: Colors.grey[200],
foregroundColor: Colors.red,
selectedForegroundColor: Colors.white,
selectedBackgroundColor: Colors.green,
),
segments: const <ButtonSegment<Calendar>>[
ButtonSegment<Calendar>(value: Calendar.day, label: Text('Day'), icon: Icon(Icons.calendar_view_day)),
ButtonSegment<Calendar>(value: Calendar.week, label: Text('Week'), icon: Icon(Icons.calendar_view_week)),
ButtonSegment<Calendar>(value: Calendar.month, label: Text('Month'), icon: Icon(Icons.calendar_view_month)),
ButtonSegment<Calendar>(value: Calendar.year, label: Text('Year'), icon: Icon(Icons.calendar_today)),
],
selected: <Calendar>{calendarView},
onSelectionChanged: (Set<Calendar> 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;
});
},
);
}
}
// 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<Material>(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);
});
}
......@@ -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<T> {
/// ** 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: <https://m3.material.io/components/segmented-buttons/overview>
......@@ -178,6 +187,123 @@ class SegmentedButton<T> 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<int>(
/// style: SegmentedButton.styleFrom(
/// foregroundColor: Colors.black,
/// selectedForegroundColor: Colors.white,
/// backgroundColor: Colors.amber,
/// selectedBackgroundColor: Colors.red,
/// ),
/// segments: const <ButtonSegment<int>>[
/// ButtonSegment<int>(
/// value: 0,
/// label: Text('0'),
/// icon: Icon(Icons.calendar_view_day),
/// ),
/// ButtonSegment<int>(
/// value: 1,
/// label: Text('1'),
/// icon: Icon(Icons.calendar_view_week),
/// ),
/// ],
/// selected: const <int>{0},
/// onSelectionChanged: (Set<int> 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<Color?>? foregroundColorProp =
(foregroundColor == null && disabledForegroundColor == null && selectedForegroundColor == null)
? null
: _SegmentButtonDefaultColor(foregroundColor, disabledForegroundColor, selectedForegroundColor);
final MaterialStateProperty<Color?>? backgroundColorProp =
(backgroundColor == null && disabledBackgroundColor == null && selectedBackgroundColor == null)
? null
: _SegmentButtonDefaultColor(backgroundColor, disabledBackgroundColor, selectedBackgroundColor);
final MaterialStateProperty<Color?>? 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<T> extends State<SegmentedButton<T>> {
super.dispose();
}
}
@immutable
class _SegmentButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
_SegmentButtonDefaultColor(this.color, this.disabled, this.selected);
final Color? color;
final Color? disabled;
final Color? selected;
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabled;
}
if (states.contains(MaterialState.selected)) {
return selected;
}
return color;
}
}
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
const _SegmentedButtonRenderWidget({
super.key,
......@@ -842,6 +989,33 @@ class _SegmentedButtonDefaultsM3 extends SegmentedButtonThemeData {
}
@override
Widget? get selectedIcon => const Icon(Icons.check);
static MaterialStateProperty<Color?> resolveStateColor(Color? unselectedColor, Color? selectedColor){
return MaterialStateProperty.resolveWith((Set<MaterialState> 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
......@@ -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<int>(
style: styleFromStyle,
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(value: 1, label: Text('1')),
ButtonSegment<int>(value: 2, label: Text('2')),
ButtonSegment<int>(value: 3, label: Text('3'), enabled: false),
],
selected: const <int>{2},
onSelectionChanged: (Set<int> selected) { },
selectedIcon: const Icon(Icons.alarm),
),
),
),
));
// Test provided button style is applied to the enabled button segment.
ButtonStyle? buttonStyle = tester.widget<TextButton>(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<TextButton>(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<TextButton>(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<Material>(find.descendant(
of: find.byType(SegmentedButton<int>),
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<int>),
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<MaterialState> enabled = const <MaterialState>{};
Set<MaterialState> disabled = const <MaterialState>{ MaterialState.disabled };
Set<MaterialState> selected = const <MaterialState>{ MaterialState.selected };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment