Unverified Commit 6ec2bd0a authored by Darren Austin's avatar Darren Austin Committed by GitHub

M3 Segmented Button widget (#113723)

parent 87727681
......@@ -40,6 +40,7 @@ import 'package:gen_defaults/navigation_rail_template.dart';
import 'package:gen_defaults/popup_menu_template.dart';
import 'package:gen_defaults/progress_indicator_template.dart';
import 'package:gen_defaults/radio_template.dart';
import 'package:gen_defaults/segmented_button_template.dart';
import 'package:gen_defaults/slider_template.dart';
import 'package:gen_defaults/surface_tint.dart';
import 'package:gen_defaults/switch_template.dart';
......@@ -155,6 +156,7 @@ Future<void> main(List<String> args) async {
PopupMenuTemplate('PopupMenu', '$materialLib/popup_menu.dart', tokens).updateFile();
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile();
SegmentedButtonTemplate('SegmentedButton', '$materialLib/segmented_button.dart', tokens).updateFile();
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
......
// 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 'template.dart';
class SegmentedButtonTemplate extends TokenTemplate {
const SegmentedButtonTemplate(super.blockName, super.fileName, super.tokens, {
super.colorSchemePrefix = '_colors.',
});
String _layerOpacity(String layerToken) {
if (tokens.containsKey(layerToken)) {
final String? layerValue = tokens[layerToken] as String?;
if (tokens.containsKey(layerValue)) {
final String? opacityValue = opacity(layerValue!);
if (opacityValue != null) {
return '.withOpacity($opacityValue)';
}
}
}
return '';
}
String _stateColor(String componentToken, String type, String state) {
final String baseColor = color('$componentToken.$type.$state.state-layer.color', '');
if (baseColor.isEmpty) {
return 'null';
}
final String opacity = _layerOpacity('$componentToken.$state.state-layer.opacity');
return '$baseColor$opacity';
}
@override
String generate() => '''
class _SegmentedButtonDefaultsM3 extends SegmentedButtonThemeData {
_SegmentedButtonDefaultsM3(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
@override ButtonStyle? get style {
return ButtonStyle(
textStyle: MaterialStatePropertyAll<TextStyle?>(${textStyle('md.comp.outlined-segmented-button.label-text')}),
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return ${componentColor('md.comp.outlined-segmented-button.disabled')};
}
if (states.contains(MaterialState.selected)) {
return ${componentColor('md.comp.outlined-segmented-button.selected.container')};
}
return ${componentColor('md.comp.outlined-segmented-button.unselected.container')};
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return ${componentColor('md.comp.outlined-segmented-button.disabled.label-text')};
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.outlined-segmented-button.selected.pressed.label-text')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.outlined-segmented-button.selected.hover.label-text')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.outlined-segmented-button.selected.focus.label-text')};
}
return ${componentColor('md.comp.outlined-segmented-button.selected.label-text')};
} else {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.outlined-segmented-button.unselected.pressed.label-text')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.outlined-segmented-button.unselected.hover.label-text')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.outlined-segmented-button.unselected.focus.label-text')};
}
return ${componentColor('md.comp.outlined-segmented-button.unselected.container')};
}
}),
overlayColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.hovered)) {
return ${_stateColor('md.comp.outlined-segmented-button', 'selected', 'hover')};
}
if (states.contains(MaterialState.focused)) {
return ${_stateColor('md.comp.outlined-segmented-button', 'selected', 'focus')};
}
if (states.contains(MaterialState.pressed)) {
return ${_stateColor('md.comp.outlined-segmented-button', 'selected', 'pressed')};
}
} else {
if (states.contains(MaterialState.hovered)) {
return ${_stateColor('md.comp.outlined-segmented-button', 'unselected', 'hover')};
}
if (states.contains(MaterialState.focused)) {
return ${_stateColor('md.comp.outlined-segmented-button', 'unselected', 'focus')};
}
if (states.contains(MaterialState.pressed)) {
return ${_stateColor('md.comp.outlined-segmented-button', 'unselected', 'pressed')};
}
}
return null;
}),
surfaceTintColor: const MaterialStatePropertyAll<Color>(Colors.transparent),
elevation: const MaterialStatePropertyAll<double>(0),
iconSize: const MaterialStatePropertyAll<double?>(${tokens['md.comp.outlined-segmented-button.with-icon.icon.size']}),
side: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return ${border("md.comp.outlined-segmented-button.disabled.outline")};
}
return ${border("md.comp.outlined-segmented-button.outline")};
}),
shape: const MaterialStatePropertyAll<OutlinedBorder>(${shape("md.comp.outlined-segmented-button", '')}),
);
}
@override
Widget? get selectedIcon => const Icon(Icons.check);
}
''';
}
// 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.
/// Flutter code sample for [SegmentedButton].
import 'package:flutter/material.dart';
void main() {
runApp(const SegmentedButtonApp());
}
class SegmentedButtonApp extends StatelessWidget {
const SegmentedButtonApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Spacer(),
Text('Single choice'),
SingleChoice(),
SizedBox(height: 20),
Text('Multiple choice'),
MultipleChoice(),
Spacer(),
],
),
),
),
);
}
}
enum Calendar { day, week, month, year }
class SingleChoice extends StatefulWidget {
const SingleChoice({super.key});
@override
State<SingleChoice> createState() => _SingleChoiceState();
}
class _SingleChoiceState extends State<SingleChoice> {
Calendar calendarView = Calendar.day;
@override
Widget build(BuildContext context) {
return SegmentedButton<Calendar>(
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;
});
},
);
}
}
enum Sizes { extraSmall, small, medium, large, extraLarge }
class MultipleChoice extends StatefulWidget {
const MultipleChoice({super.key});
@override
State<MultipleChoice> createState() => _MultipleChoiceState();
}
class _MultipleChoiceState extends State<MultipleChoice> {
Set<Sizes> selection = <Sizes>{Sizes.large, Sizes.extraLarge};
@override
Widget build(BuildContext context) {
return SegmentedButton<Sizes>(
segments: const <ButtonSegment<Sizes>>[
ButtonSegment<Sizes>(value: Sizes.extraSmall, label: Text('XS')),
ButtonSegment<Sizes>(value: Sizes.small, label: Text('S')),
ButtonSegment<Sizes>(value: Sizes.medium, label: Text('M')),
ButtonSegment<Sizes>(value: Sizes.large, label: Text('L'),),
ButtonSegment<Sizes>(value: Sizes.extraLarge, label: Text('XL')),
],
selected: selection,
onSelectionChanged: (Set<Sizes> newSelection) {
setState(() {
selection = newSelection;
});
},
multiSelectionEnabled: true,
);
}
}
......@@ -146,6 +146,8 @@ export 'src/material/scaffold.dart';
export 'src/material/scrollbar.dart';
export 'src/material/scrollbar_theme.dart';
export 'src/material/search.dart';
export 'src/material/segmented_button.dart';
export 'src/material/segmented_button_theme.dart';
export 'src/material/selectable_text.dart';
export 'src/material/selection_area.dart';
export 'src/material/shadows.dart';
......
This diff is collapsed.
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'theme.dart';
// Examples can assume:
// late BuildContext context;
/// Overrides the default values of visual properties for descendant
/// [SegmentedButton] widgets.
///
/// Descendant widgets obtain the current [SegmentedButtonThemeData] object with
/// [SegmentedButtonTheme.of]. Instances of [SegmentedButtonTheme] can
/// be customized with [SegmentedButtonThemeData.copyWith].
///
/// Typically a [SegmentedButtonTheme] is specified as part of the overall
/// [Theme] with [ThemeData.segmentedButtonTheme].
///
/// All [SegmentedButtonThemeData] properties are null by default. When null,
/// the [SegmentedButton] computes its own default values, typically based on
/// the overall theme's [ThemeData.colorScheme], [ThemeData.textTheme], and
/// [ThemeData.iconTheme].
@immutable
class SegmentedButtonThemeData with Diagnosticable {
/// Creates a [SegmentedButtonThemeData] that can be used to override default properties
/// in a [SegmentedButtonTheme] widget.
const SegmentedButtonThemeData({
this.style,
this.selectedIcon,
});
/// Overrides the [SegmentedButton]'s default style.
///
/// Non-null properties or non-null resolved [MaterialStateProperty]
/// values override the default values used by [SegmentedButton].
///
/// If [style] is null, then this theme doesn't override anything.
final ButtonStyle? style;
/// Override for [SegmentedButton.selectedIcon] property.
///
/// If non-null, then [selectedIcon] will be used instead of default
/// value for [SegmentedButton.selectedIcon].
final Widget? selectedIcon;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
SegmentedButtonThemeData copyWith({
ButtonStyle? style,
Widget? selectedIcon,
}) {
return SegmentedButtonThemeData(
style: style ?? this.style,
selectedIcon: selectedIcon ?? this.selectedIcon,
);
}
/// Linearly interpolates between two segmented button themes.
static SegmentedButtonThemeData lerp(SegmentedButtonThemeData? a, SegmentedButtonThemeData? b, double t) {
return SegmentedButtonThemeData(
style: ButtonStyle.lerp(a?.style, b?.style, t),
selectedIcon: t < 0.5 ? a?.selectedIcon : b?.selectedIcon,
);
}
@override
int get hashCode => Object.hash(
style,
selectedIcon,
);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is SegmentedButtonThemeData
&& other.style == style
&& other.selectedIcon == selectedIcon;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null));
properties.add(DiagnosticsProperty<Widget>('selectedIcon', selectedIcon, defaultValue: null));
}
}
/// An inherited widget that defines the visual properties for
/// [SegmentedButton]s in this widget's subtree.
///
/// Values specified here are used for [SegmentedButton] properties that are not
/// given an explicit non-null value.
class SegmentedButtonTheme extends InheritedTheme {
/// Creates a [SegmentedButtonTheme] that controls visual parameters for
/// descendent [SegmentedButton]s.
const SegmentedButtonTheme({
super.key,
required this.data,
required super.child,
}) : assert(data != null);
/// Specifies the visual properties used by descendant [SegmentedButton]
/// widgets.
final SegmentedButtonThemeData data;
/// The [data] from the closest instance of this class that encloses the given
/// context.
///
/// If there is no [SegmentedButtonTheme] in scope, this will return
/// [ThemeData.segmentedButtonTheme] from the ambient [Theme].
///
/// Typical usage is as follows:
///
/// ```dart
/// SegmentedButtonThemeData theme = SegmentedButtonTheme.of(context);
/// ```
///
/// See also:
///
/// * [maybeOf], which returns null if it doesn't find a
/// [SegmentedButtonTheme] ancestor.
static SegmentedButtonThemeData of(BuildContext context) {
return maybeOf(context) ?? Theme.of(context).segmentedButtonTheme;
}
/// The data from the closest instance of this class that encloses the given
/// context, if any.
///
/// Use this function if you want to allow situations where no
/// [SegmentedButtonTheme] is in scope. Prefer using [SegmentedButtonTheme.of]
/// in situations where a [SegmentedButtonThemeData] is expected to be
/// non-null.
///
/// If there is no [SegmentedButtonTheme] in scope, then this function will
/// return null.
///
/// Typical usage is as follows:
///
/// ```dart
/// SegmentedButtonThemeData? theme = SegmentedButtonTheme.maybeOf(context);
/// if (theme == null) {
/// // Do something else instead.
/// }
/// ```
///
/// See also:
///
/// * [of], which will return [ThemeData.segmentedButtonTheme] if it doesn't
/// find a [SegmentedButtonTheme] ancestor, instead of returning null.
static SegmentedButtonThemeData? maybeOf(BuildContext context) {
assert(context != null);
return context.dependOnInheritedWidgetOfExactType<SegmentedButtonTheme>()?.data;
}
@override
Widget wrap(BuildContext context, Widget child) {
return SegmentedButtonTheme(data: data, child: child);
}
@override
bool updateShouldNotify(SegmentedButtonTheme oldWidget) => data != oldWidget.data;
}
......@@ -48,6 +48,7 @@ import 'popup_menu_theme.dart';
import 'progress_indicator_theme.dart';
import 'radio_theme.dart';
import 'scrollbar_theme.dart';
import 'segmented_button_theme.dart';
import 'slider_theme.dart';
import 'snack_bar_theme.dart';
import 'switch_theme.dart';
......@@ -361,6 +362,7 @@ class ThemeData with Diagnosticable {
PopupMenuThemeData? popupMenuTheme,
ProgressIndicatorThemeData? progressIndicatorTheme,
RadioThemeData? radioTheme,
SegmentedButtonThemeData? segmentedButtonTheme,
SliderThemeData? sliderTheme,
SnackBarThemeData? snackBarTheme,
SwitchThemeData? switchTheme,
......@@ -613,6 +615,7 @@ class ThemeData with Diagnosticable {
popupMenuTheme ??= const PopupMenuThemeData();
progressIndicatorTheme ??= const ProgressIndicatorThemeData();
radioTheme ??= const RadioThemeData();
segmentedButtonTheme ??= const SegmentedButtonThemeData();
sliderTheme ??= const SliderThemeData();
snackBarTheme ??= const SnackBarThemeData();
switchTheme ??= const SwitchThemeData();
......@@ -708,6 +711,7 @@ class ThemeData with Diagnosticable {
popupMenuTheme: popupMenuTheme,
progressIndicatorTheme: progressIndicatorTheme,
radioTheme: radioTheme,
segmentedButtonTheme: segmentedButtonTheme,
sliderTheme: sliderTheme,
snackBarTheme: snackBarTheme,
switchTheme: switchTheme,
......@@ -819,6 +823,7 @@ class ThemeData with Diagnosticable {
required this.popupMenuTheme,
required this.progressIndicatorTheme,
required this.radioTheme,
required this.segmentedButtonTheme,
required this.sliderTheme,
required this.snackBarTheme,
required this.switchTheme,
......@@ -988,6 +993,7 @@ class ThemeData with Diagnosticable {
assert(popupMenuTheme != null),
assert(progressIndicatorTheme != null),
assert(radioTheme != null),
assert(segmentedButtonTheme != null),
assert(sliderTheme != null),
assert(snackBarTheme != null),
assert(switchTheme != null),
......@@ -1252,10 +1258,8 @@ class ThemeData with Diagnosticable {
/// A temporary flag used to opt-in to Material 3 features.
///
/// If true, then widgets that have been migrated to Material 3 will
/// use new colors, typography and other features of Material 3. A new
/// purple-based [ColorScheme] will be created and applied to the updated
/// widgets, as long as this is set to true. If false, they will use the
/// Material 2 look and feel.
/// use new colors, typography and other features of Material 3. If false,
/// they will use the Material 2 look and feel.
///
/// During the migration to Material 3, turning this on may yield
/// inconsistent look and feel in your app as some widgets are migrated
......@@ -1293,10 +1297,11 @@ class ThemeData with Diagnosticable {
/// * Typography: `typography` (see table above)
///
/// ### Components
/// * Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton], [IconButton]
/// * Bottom app bar: [BottomAppBar]
/// * FAB: [FloatingActionButton]
/// * Extended FAB: [FloatingActionButton.extended]
/// * Buttons
/// - Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton], [IconButton]
/// - FAB: [FloatingActionButton], [FloatingActionButton.extended]
/// - Segmented buttons: [SegmentedButton]
/// * Cards: [Card]
/// * TextFields: [TextField] together with its [InputDecoration]
/// * Chips:
......@@ -1599,6 +1604,9 @@ class ThemeData with Diagnosticable {
/// A theme for customizing the appearance and layout of [Radio] widgets.
final RadioThemeData radioTheme;
/// A theme for customizing the appearance and layout of [SegmentedButton] widgets.
final SegmentedButtonThemeData segmentedButtonTheme;
/// The colors and shapes used to render [Slider].
///
/// This is the value returned from [SliderTheme.of].
......@@ -1880,6 +1888,7 @@ class ThemeData with Diagnosticable {
PopupMenuThemeData? popupMenuTheme,
ProgressIndicatorThemeData? progressIndicatorTheme,
RadioThemeData? radioTheme,
SegmentedButtonThemeData? segmentedButtonTheme,
SliderThemeData? sliderTheme,
SnackBarThemeData? snackBarTheme,
SwitchThemeData? switchTheme,
......@@ -2042,6 +2051,7 @@ class ThemeData with Diagnosticable {
popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme,
progressIndicatorTheme: progressIndicatorTheme ?? this.progressIndicatorTheme,
radioTheme: radioTheme ?? this.radioTheme,
segmentedButtonTheme: segmentedButtonTheme ?? this.segmentedButtonTheme,
sliderTheme: sliderTheme ?? this.sliderTheme,
snackBarTheme: snackBarTheme ?? this.snackBarTheme,
switchTheme: switchTheme ?? this.switchTheme,
......@@ -2246,6 +2256,7 @@ class ThemeData with Diagnosticable {
popupMenuTheme: PopupMenuThemeData.lerp(a.popupMenuTheme, b.popupMenuTheme, t)!,
progressIndicatorTheme: ProgressIndicatorThemeData.lerp(a.progressIndicatorTheme, b.progressIndicatorTheme, t)!,
radioTheme: RadioThemeData.lerp(a.radioTheme, b.radioTheme, t),
segmentedButtonTheme: SegmentedButtonThemeData.lerp(a.segmentedButtonTheme, b.segmentedButtonTheme, t),
sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
snackBarTheme: SnackBarThemeData.lerp(a.snackBarTheme, b.snackBarTheme, t),
switchTheme: SwitchThemeData.lerp(a.switchTheme, b.switchTheme, t),
......@@ -2352,6 +2363,7 @@ class ThemeData with Diagnosticable {
other.popupMenuTheme == popupMenuTheme &&
other.progressIndicatorTheme == progressIndicatorTheme &&
other.radioTheme == radioTheme &&
other.segmentedButtonTheme == segmentedButtonTheme &&
other.sliderTheme == sliderTheme &&
other.snackBarTheme == snackBarTheme &&
other.switchTheme == switchTheme &&
......@@ -2455,6 +2467,7 @@ class ThemeData with Diagnosticable {
popupMenuTheme,
progressIndicatorTheme,
radioTheme,
segmentedButtonTheme,
sliderTheme,
snackBarTheme,
switchTheme,
......@@ -2560,6 +2573,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<PopupMenuThemeData>('popupMenuTheme', popupMenuTheme, defaultValue: defaultData.popupMenuTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ProgressIndicatorThemeData>('progressIndicatorTheme', progressIndicatorTheme, defaultValue: defaultData.progressIndicatorTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<RadioThemeData>('radioTheme', radioTheme, defaultValue: defaultData.radioTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SegmentedButtonThemeData>('segmentedButtonTheme', segmentedButtonTheme, defaultValue: defaultData.segmentedButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SliderThemeData>('sliderTheme', sliderTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SnackBarThemeData>('snackBarTheme', snackBarTheme, defaultValue: defaultData.snackBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<SwitchThemeData>('switchTheme', switchTheme, defaultValue: defaultData.switchTheme, level: DiagnosticLevel.debug));
......
This diff is collapsed.
......@@ -799,6 +799,7 @@ void main() {
popupMenuTheme: const PopupMenuThemeData(color: Colors.black),
progressIndicatorTheme: const ProgressIndicatorThemeData(),
radioTheme: const RadioThemeData(),
segmentedButtonTheme: const SegmentedButtonThemeData(),
sliderTheme: sliderTheme,
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.black),
switchTheme: const SwitchThemeData(),
......@@ -917,6 +918,7 @@ void main() {
popupMenuTheme: const PopupMenuThemeData(color: Colors.white),
progressIndicatorTheme: const ProgressIndicatorThemeData(),
radioTheme: const RadioThemeData(),
segmentedButtonTheme: const SegmentedButtonThemeData(),
sliderTheme: otherSliderTheme,
snackBarTheme: const SnackBarThemeData(backgroundColor: Colors.white),
switchTheme: const SwitchThemeData(),
......@@ -1263,6 +1265,7 @@ void main() {
'popupMenuTheme',
'progressIndicatorTheme',
'radioTheme',
'segmentedButtonTheme',
'sliderTheme',
'snackBarTheme',
'switchTheme',
......
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