Skip to content

Commit 6ec2bd0

Browse files
authored
M3 Segmented Button widget (#113723)
1 parent 8772768 commit 6ec2bd0

File tree

10 files changed

+2172
-7
lines changed

10 files changed

+2172
-7
lines changed

dev/tools/gen_defaults/bin/gen_defaults.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import 'package:gen_defaults/navigation_rail_template.dart';
4040
import 'package:gen_defaults/popup_menu_template.dart';
4141
import 'package:gen_defaults/progress_indicator_template.dart';
4242
import 'package:gen_defaults/radio_template.dart';
43+
import 'package:gen_defaults/segmented_button_template.dart';
4344
import 'package:gen_defaults/slider_template.dart';
4445
import 'package:gen_defaults/surface_tint.dart';
4546
import 'package:gen_defaults/switch_template.dart';
@@ -155,6 +156,7 @@ Future<void> main(List<String> args) async {
155156
PopupMenuTemplate('PopupMenu', '$materialLib/popup_menu.dart', tokens).updateFile();
156157
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
157158
RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile();
159+
SegmentedButtonTemplate('SegmentedButton', '$materialLib/segmented_button.dart', tokens).updateFile();
158160
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();
159161
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
160162
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'template.dart';
6+
7+
class SegmentedButtonTemplate extends TokenTemplate {
8+
const SegmentedButtonTemplate(super.blockName, super.fileName, super.tokens, {
9+
super.colorSchemePrefix = '_colors.',
10+
});
11+
12+
String _layerOpacity(String layerToken) {
13+
if (tokens.containsKey(layerToken)) {
14+
final String? layerValue = tokens[layerToken] as String?;
15+
if (tokens.containsKey(layerValue)) {
16+
final String? opacityValue = opacity(layerValue!);
17+
if (opacityValue != null) {
18+
return '.withOpacity($opacityValue)';
19+
}
20+
}
21+
}
22+
return '';
23+
}
24+
25+
String _stateColor(String componentToken, String type, String state) {
26+
final String baseColor = color('$componentToken.$type.$state.state-layer.color', '');
27+
if (baseColor.isEmpty) {
28+
return 'null';
29+
}
30+
final String opacity = _layerOpacity('$componentToken.$state.state-layer.opacity');
31+
return '$baseColor$opacity';
32+
}
33+
34+
@override
35+
String generate() => '''
36+
class _SegmentedButtonDefaultsM3 extends SegmentedButtonThemeData {
37+
_SegmentedButtonDefaultsM3(this.context);
38+
39+
final BuildContext context;
40+
late final ThemeData _theme = Theme.of(context);
41+
late final ColorScheme _colors = _theme.colorScheme;
42+
43+
@override ButtonStyle? get style {
44+
return ButtonStyle(
45+
textStyle: MaterialStatePropertyAll<TextStyle?>(${textStyle('md.comp.outlined-segmented-button.label-text')}),
46+
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
47+
if (states.contains(MaterialState.disabled)) {
48+
return ${componentColor('md.comp.outlined-segmented-button.disabled')};
49+
}
50+
if (states.contains(MaterialState.selected)) {
51+
return ${componentColor('md.comp.outlined-segmented-button.selected.container')};
52+
}
53+
return ${componentColor('md.comp.outlined-segmented-button.unselected.container')};
54+
}),
55+
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
56+
if (states.contains(MaterialState.disabled)) {
57+
return ${componentColor('md.comp.outlined-segmented-button.disabled.label-text')};
58+
}
59+
if (states.contains(MaterialState.selected)) {
60+
if (states.contains(MaterialState.pressed)) {
61+
return ${componentColor('md.comp.outlined-segmented-button.selected.pressed.label-text')};
62+
}
63+
if (states.contains(MaterialState.hovered)) {
64+
return ${componentColor('md.comp.outlined-segmented-button.selected.hover.label-text')};
65+
}
66+
if (states.contains(MaterialState.focused)) {
67+
return ${componentColor('md.comp.outlined-segmented-button.selected.focus.label-text')};
68+
}
69+
return ${componentColor('md.comp.outlined-segmented-button.selected.label-text')};
70+
} else {
71+
if (states.contains(MaterialState.pressed)) {
72+
return ${componentColor('md.comp.outlined-segmented-button.unselected.pressed.label-text')};
73+
}
74+
if (states.contains(MaterialState.hovered)) {
75+
return ${componentColor('md.comp.outlined-segmented-button.unselected.hover.label-text')};
76+
}
77+
if (states.contains(MaterialState.focused)) {
78+
return ${componentColor('md.comp.outlined-segmented-button.unselected.focus.label-text')};
79+
}
80+
return ${componentColor('md.comp.outlined-segmented-button.unselected.container')};
81+
}
82+
}),
83+
overlayColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
84+
if (states.contains(MaterialState.selected)) {
85+
if (states.contains(MaterialState.hovered)) {
86+
return ${_stateColor('md.comp.outlined-segmented-button', 'selected', 'hover')};
87+
}
88+
if (states.contains(MaterialState.focused)) {
89+
return ${_stateColor('md.comp.outlined-segmented-button', 'selected', 'focus')};
90+
}
91+
if (states.contains(MaterialState.pressed)) {
92+
return ${_stateColor('md.comp.outlined-segmented-button', 'selected', 'pressed')};
93+
}
94+
} else {
95+
if (states.contains(MaterialState.hovered)) {
96+
return ${_stateColor('md.comp.outlined-segmented-button', 'unselected', 'hover')};
97+
}
98+
if (states.contains(MaterialState.focused)) {
99+
return ${_stateColor('md.comp.outlined-segmented-button', 'unselected', 'focus')};
100+
}
101+
if (states.contains(MaterialState.pressed)) {
102+
return ${_stateColor('md.comp.outlined-segmented-button', 'unselected', 'pressed')};
103+
}
104+
}
105+
return null;
106+
}),
107+
surfaceTintColor: const MaterialStatePropertyAll<Color>(Colors.transparent),
108+
elevation: const MaterialStatePropertyAll<double>(0),
109+
iconSize: const MaterialStatePropertyAll<double?>(${tokens['md.comp.outlined-segmented-button.with-icon.icon.size']}),
110+
side: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
111+
if (states.contains(MaterialState.disabled)) {
112+
return ${border("md.comp.outlined-segmented-button.disabled.outline")};
113+
}
114+
return ${border("md.comp.outlined-segmented-button.outline")};
115+
}),
116+
shape: const MaterialStatePropertyAll<OutlinedBorder>(${shape("md.comp.outlined-segmented-button", '')}),
117+
);
118+
}
119+
120+
@override
121+
Widget? get selectedIcon => const Icon(Icons.check);
122+
}
123+
''';
124+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/// Flutter code sample for [SegmentedButton].
6+
7+
import 'package:flutter/material.dart';
8+
9+
void main() {
10+
runApp(const SegmentedButtonApp());
11+
}
12+
13+
class SegmentedButtonApp extends StatelessWidget {
14+
const SegmentedButtonApp({super.key});
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
return MaterialApp(
19+
theme: ThemeData(useMaterial3: true),
20+
home: Scaffold(
21+
body: Center(
22+
child: Column(
23+
mainAxisAlignment: MainAxisAlignment.center,
24+
children: const <Widget>[
25+
Spacer(),
26+
Text('Single choice'),
27+
SingleChoice(),
28+
SizedBox(height: 20),
29+
Text('Multiple choice'),
30+
MultipleChoice(),
31+
Spacer(),
32+
],
33+
),
34+
),
35+
),
36+
);
37+
}
38+
}
39+
40+
enum Calendar { day, week, month, year }
41+
42+
class SingleChoice extends StatefulWidget {
43+
const SingleChoice({super.key});
44+
45+
@override
46+
State<SingleChoice> createState() => _SingleChoiceState();
47+
}
48+
49+
class _SingleChoiceState extends State<SingleChoice> {
50+
51+
Calendar calendarView = Calendar.day;
52+
53+
@override
54+
Widget build(BuildContext context) {
55+
return SegmentedButton<Calendar>(
56+
segments: const <ButtonSegment<Calendar>>[
57+
ButtonSegment<Calendar>(value: Calendar.day, label: Text('Day'), icon: Icon(Icons.calendar_view_day)),
58+
ButtonSegment<Calendar>(value: Calendar.week, label: Text('Week'), icon: Icon(Icons.calendar_view_week)),
59+
ButtonSegment<Calendar>(value: Calendar.month, label: Text('Month'), icon: Icon(Icons.calendar_view_month)),
60+
ButtonSegment<Calendar>(value: Calendar.year, label: Text('Year'), icon: Icon(Icons.calendar_today)),
61+
],
62+
selected: <Calendar>{calendarView},
63+
onSelectionChanged: (Set<Calendar> newSelection) {
64+
setState(() {
65+
// By default there is only a single segment that can be
66+
// selected at one time, so its value is always the first
67+
// item in the selected set.
68+
calendarView = newSelection.first;
69+
});
70+
},
71+
);
72+
}
73+
}
74+
75+
enum Sizes { extraSmall, small, medium, large, extraLarge }
76+
77+
class MultipleChoice extends StatefulWidget {
78+
const MultipleChoice({super.key});
79+
80+
@override
81+
State<MultipleChoice> createState() => _MultipleChoiceState();
82+
}
83+
84+
class _MultipleChoiceState extends State<MultipleChoice> {
85+
Set<Sizes> selection = <Sizes>{Sizes.large, Sizes.extraLarge};
86+
87+
@override
88+
Widget build(BuildContext context) {
89+
return SegmentedButton<Sizes>(
90+
segments: const <ButtonSegment<Sizes>>[
91+
ButtonSegment<Sizes>(value: Sizes.extraSmall, label: Text('XS')),
92+
ButtonSegment<Sizes>(value: Sizes.small, label: Text('S')),
93+
ButtonSegment<Sizes>(value: Sizes.medium, label: Text('M')),
94+
ButtonSegment<Sizes>(value: Sizes.large, label: Text('L'),),
95+
ButtonSegment<Sizes>(value: Sizes.extraLarge, label: Text('XL')),
96+
],
97+
selected: selection,
98+
onSelectionChanged: (Set<Sizes> newSelection) {
99+
setState(() {
100+
selection = newSelection;
101+
});
102+
},
103+
multiSelectionEnabled: true,
104+
);
105+
}
106+
}

packages/flutter/lib/material.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ export 'src/material/scaffold.dart';
146146
export 'src/material/scrollbar.dart';
147147
export 'src/material/scrollbar_theme.dart';
148148
export 'src/material/search.dart';
149+
export 'src/material/segmented_button.dart';
150+
export 'src/material/segmented_button_theme.dart';
149151
export 'src/material/selectable_text.dart';
150152
export 'src/material/selection_area.dart';
151153
export 'src/material/shadows.dart';

0 commit comments

Comments
 (0)