Skip to content

Commit

Permalink
M3 Segmented Button widget (#113723)
Browse files Browse the repository at this point in the history
  • Loading branch information
darrenaustin authored Nov 12, 2022
1 parent 8772768 commit 6ec2bd0
Show file tree
Hide file tree
Showing 10 changed files with 2,172 additions and 7 deletions.
2 changes: 2 additions & 0 deletions dev/tools/gen_defaults/bin/gen_defaults.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
124 changes: 124 additions & 0 deletions dev/tools/gen_defaults/lib/segmented_button_template.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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);
}
''';
}
106 changes: 106 additions & 0 deletions examples/api/lib/material/segmented_button/segmented_button.0.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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,
);
}
}
2 changes: 2 additions & 0 deletions packages/flutter/lib/material.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit 6ec2bd0

Please sign in to comment.