M3 Segmented Button widget (#113723)

This commit is contained in:
Darren Austin 2022-11-11 22:13:57 -08:00 committed by GitHub
parent 877276812b
commit 6ec2bd0a80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 2172 additions and 7 deletions

View file

@ -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();

View file

@ -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);
}
''';
}

View file

@ -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,
);
}
}

View file

@ -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';

View file

@ -0,0 +1,813 @@
// 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 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'icons.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';
/// Data describing a segment of a [SegmentedButton].
class ButtonSegment<T> {
/// Construct a SegmentData
///
/// One of [icon] or [label] must be non-null.
const ButtonSegment({
required this.value,
this.icon,
this.label,
this.enabled = true,
}) : assert(icon != null || label != null);
/// Value used to identify the segment.
///
/// This value must be unique across all segments in a [SegmentedButton].
final T value;
/// Optional icon displayed in the segment.
final Widget? icon;
/// Optional label displayed in the segment.
final Widget? label;
/// Determines if the segment is available for selection.
final bool enabled;
}
/// A Material button that allows the user to select from limited set of options.
///
/// Segmented buttons are used to help people select options, switch views, or
/// sort elements. They are typically used in cases where there are only 2-5
/// options.
///
/// The options are represented by segments described with [ButtonSegment]
/// entries in the [segments] field. Each segment has a [ButtonSegment.value]
/// that is used to indicate which segments are selected.
///
/// The [selected] field is a set of selected [ButtonSegment.value]s. This
/// should be updated by the app in response to [onSelectionChanged] updates.
///
/// By default, only a single segment can be selected (for mutually exclusive
/// choices). This can be relaxed with the [multiSelectionEnabled] field.
///
/// Like [ButtonStyleButton]s, the [SegmentedButton]'s visuals can be
/// configured with a [ButtonStyle] [style] field. However, unlike other
/// buttons, some of the style parameters are applied to the entire segmented
/// button, and others are used for each of the segments.
///
/// By default, a checkmark icon is used to show selected items. To configure
/// this behavior, you can use the [showSelectedIcon] and [selectedIcon] fields.
///
/// Individual segments can be enabled or disabled with their
/// [ButtonSegment.enabled] flag. If the [onSelectionChanged] field is null,
/// then the entire segmented button will be disabled, regardless of the
/// individual segment settings.
///
/// {@tool dartpad}
/// This sample shows how to display a [SegmentedButton] with either a single or
/// multiple selection.
///
/// ** See code in examples/api/lib/material/segmented_button/segmented_button.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * Material Design spec: <https://m3.material.io/components/segmented-buttons/overview>
/// * [ButtonStyle], which can be used in the [style] field to configure
/// the appearance of the button and its segments.
/// * [ToggleButtons], a similar widget that was built for Material 2.
/// [SegmentedButton] should be considered as a replacement for
/// [ToggleButtons].
/// * [Radio], an alternative way to present the user with a mutually exclusive set of options.
/// * [FilterChip], [ChoiceChip], which can be used when you need to show more than five options.
class SegmentedButton<T> extends StatelessWidget {
/// Creates a const [SegmentedButton].
///
/// [segments] must contain at least one segment, but it is recommended
/// to have two to five segments. If you need only single segment,
/// consider using a [Checkbox] or [Radio] widget instead. If you need
/// more than five options, consider using [FilterChip] or [ChoiceChip]
/// widgets.
///
/// If [onSelectionChanged] is null, then the entire segemented button will
/// be disabled.
///
/// By default [selected] must only contain one entry. However, if
/// [multiSelectionEnabled] is true, then [selected] can contain multiple
/// entries. If [emptySelectionAllowed] is true, then [selected] can be empty.
const SegmentedButton({
super.key,
required this.segments,
required this.selected,
this.onSelectionChanged,
this.multiSelectionEnabled = false,
this.emptySelectionAllowed = false,
this.style,
this.showSelectedIcon = true,
this.selectedIcon,
}) : assert(segments != null),
assert(segments.length > 0),
assert(selected != null),
assert(selected.length > 0 || emptySelectionAllowed),
assert(selected.length < 2 || multiSelectionEnabled);
/// Descriptions of the segments in the button.
///
/// This a required parameter and must contain at least one segment,
/// but it is recommended to contain two to five segments. If you need only
/// a single segment, consider using a [Checkbox] or [Radio] widget instead.
/// If you need more than five options, consider using [FilterChip] or
/// [ChoiceChip] widgets.
final List<ButtonSegment<T>> segments;
/// The set of [ButtonSegment.value]s that indicate which [segments] are
/// selected.
///
/// As the [SegmentedButton] does not maintain the state of the selection,
/// you will need to update this in response to [onSelectionChanged] calls.
///
/// This is a required parameter.
final Set<T> selected;
/// The function that is called when the selection changes.
///
/// The callback's parameter indicates which of the segments are selected.
///
/// When the callback is null, the entire [SegmentedButton] is disabled,
/// and will not respond to input.
///
/// The default is null.
final void Function(Set<T>)? onSelectionChanged;
/// Determines if multiple segments can be selected at one time.
///
/// If true, more than one segment can be selected. When selecting a
/// segment, the other selected segments will stay selected. Selecting an
/// already selected segment will unselect it.
///
/// If false, only one segment may be selected at a time. When a segment
/// is selected, any previously selected segment will be unselected.
///
/// The default is false, so only a single segement may be selected at one
/// time.
final bool multiSelectionEnabled;
/// Determines if having no selected segments is allowed.
///
/// If true, then it is acceptable for none of the segements to be selected.
/// This means that [selected] can be empty. If the user taps on a
/// selected segment, it will be removed from the selection set passed into
/// [onSelectionChanged].
///
/// If false (the default), there must be at least one segment selected. If
/// the user taps on the only selected segment it will not be deselected, and
/// [onSelectionChanged] will not be called.
final bool emptySelectionAllowed;
/// Customizes this button's appearance.
///
/// The following style properties apply to the entire segmented button:
///
/// * [ButtonStyle.shadowColor]
/// * [ButtonStyle.elevation]
/// * [ButtonStyle.side] - which is used for both the outer shape and
/// dividers between segments.
/// * [ButtonStyle.shape]
///
/// The following style properties are applied to each of the invidual
/// button segments. For properties that are a [MaterialStateProperty],
/// they will be resolved with the current state of the segment:
///
/// * [ButtonStyle.textStyle]
/// * [ButtonStyle.backgroundColor]
/// * [ButtonStyle.foregroundColor]
/// * [ButtonStyle.overlayColor]
/// * [ButtonStyle.surfaceTintColor]
/// * [ButtonStyle.elevation]
/// * [ButtonStyle.padding]
/// * [ButtonStyle.iconColor]
/// * [ButtonStyle.iconSize]
/// * [ButtonStyle.mouseCursor]
/// * [ButtonStyle.visualDensity]
/// * [ButtonStyle.tapTargetSize]
/// * [ButtonStyle.animationDuration]
/// * [ButtonStyle.enableFeedback]
/// * [ButtonStyle.alignment]
/// * [ButtonStyle.splashFactory]
final ButtonStyle? style;
/// Determines if the [selectedIcon] (usually an icon using [Icons.check])
/// is displayed on the selected segments.
///
/// If true, the [selectedIcon] will be displayed at the start of the segment.
/// If both the [ButtonSegment.label] and [ButtonSegment.icon] are provided,
/// then the icon will be replaced with the [selectedIcon]. If only the icon
/// or the label is present then the [selectedIcon] will be shown at the start
/// of the segment.
///
/// If false, then the [selectedIcon] is not used and will not be displayed
/// on selected segments.
///
/// The default is true, meaning the [selectedIcon] will be shown on selected
/// segments.
final bool showSelectedIcon;
/// An icon that is used to indicate a segment is selected.
///
/// If [showSelectedIcon] is true then for selected segments this icon
/// will be shown before the [ButtonSegment.label], replacing the
/// [ButtonSegment.icon] if it is specified.
///
/// Defaults to an [Icon] with [Icons.check].
final Widget? selectedIcon;
bool get _enabled => onSelectionChanged != null;
void _handleOnPressed(T segmentValue) {
if (!_enabled) {
return;
}
final bool onlySelectedSegment = selected.length == 1 && selected.contains(segmentValue);
final bool validChange = emptySelectionAllowed || !onlySelectedSegment;
if (validChange) {
final bool toggle = multiSelectionEnabled || (emptySelectionAllowed && onlySelectedSegment);
final Set<T> pressedSegment = <T>{segmentValue};
late final Set<T> updatedSelection;
if (toggle) {
updatedSelection = selected.contains(segmentValue)
? selected.difference(pressedSegment)
: selected.union(pressedSegment);
} else {
updatedSelection = pressedSegment;
}
if (!setEquals(updatedSelection, selected)) {
onSelectionChanged!(updatedSelection);
}
}
}
@override
Widget build(BuildContext context) {
final SegmentedButtonThemeData theme = SegmentedButtonTheme.of(context);
final SegmentedButtonThemeData defaults = _SegmentedButtonDefaultsM3(context);
final TextDirection direction = Directionality.of(context);
const Set<MaterialState> enabledState = <MaterialState>{};
const Set<MaterialState> disabledState = <MaterialState>{ MaterialState.disabled };
final Set<MaterialState> currentState = _enabled ? enabledState : disabledState;
P? effectiveValue<P>(P? Function(ButtonStyle? style) getProperty) {
late final P? widgetValue = getProperty(style);
late final P? themeValue = getProperty(theme.style);
late final P? defaultValue = getProperty(defaults.style);
return widgetValue ?? themeValue ?? defaultValue;
}
P? resolve<P>(MaterialStateProperty<P>? Function(ButtonStyle? style) getProperty, [Set<MaterialState>? states]) {
return effectiveValue(
(ButtonStyle? style) => getProperty(style)?.resolve(states ?? currentState),
);
}
ButtonStyle segmentStyleFor(ButtonStyle? style) {
return ButtonStyle(
textStyle: style?.textStyle,
backgroundColor: style?.backgroundColor,
foregroundColor: style?.foregroundColor,
overlayColor: style?.overlayColor,
surfaceTintColor: style?.surfaceTintColor,
elevation: style?.elevation,
padding: style?.padding,
iconColor: style?.iconColor,
iconSize: style?.iconSize,
shape: const MaterialStatePropertyAll<OutlinedBorder>(RoundedRectangleBorder()),
mouseCursor: style?.mouseCursor,
visualDensity: style?.visualDensity,
tapTargetSize: style?.tapTargetSize,
animationDuration: style?.animationDuration,
enableFeedback: style?.enableFeedback,
alignment: style?.alignment,
splashFactory: style?.splashFactory,
);
}
final ButtonStyle segmentStyle = segmentStyleFor(style);
final ButtonStyle segmentThemeStyle = segmentStyleFor(theme.style).merge(segmentStyleFor(defaults.style));
final Widget? selectedIcon = showSelectedIcon
? this.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon
: null;
Widget buttonFor(ButtonSegment<T> segment) {
final Widget label = segment.label ?? segment.icon ?? const SizedBox.shrink();
final bool segmentSelected = selected.contains(segment.value);
final Widget? icon = (segmentSelected && showSelectedIcon)
? selectedIcon
: segment.label != null
? segment.icon
: null;
final MaterialStatesController controller = MaterialStatesController(
<MaterialState>{
if (segmentSelected) MaterialState.selected,
}
);
final Widget button = icon != null
? TextButton.icon(
style: segmentStyle,
statesController: controller,
onPressed: (_enabled && segment.enabled) ? () => _handleOnPressed(segment.value) : null,
icon: icon,
label: label,
)
: TextButton(
style: segmentStyle,
statesController: controller,
onPressed: (_enabled && segment.enabled) ? () => _handleOnPressed(segment.value) : null,
child: label,
);
return MergeSemantics(
child: Semantics(
checked: segmentSelected,
inMutuallyExclusiveGroup: multiSelectionEnabled ? null : true,
child: button,
),
);
}
final OutlinedBorder resolvedEnabledBorder = resolve<OutlinedBorder?>((ButtonStyle? style) => style?.shape, disabledState) ?? const RoundedRectangleBorder();
final OutlinedBorder resolvedDisabledBorder = resolve<OutlinedBorder?>((ButtonStyle? style) => style?.shape, disabledState)?? const RoundedRectangleBorder();
final BorderSide enabledSide = resolve<BorderSide?>((ButtonStyle? style) => style?.side, enabledState) ?? BorderSide.none;
final BorderSide disabledSide = resolve<BorderSide?>((ButtonStyle? style) => style?.side, disabledState) ?? BorderSide.none;
final OutlinedBorder enabledBorder = resolvedEnabledBorder.copyWith(side: enabledSide);
final OutlinedBorder disabledBorder = resolvedDisabledBorder.copyWith(side: disabledSide);
final List<Widget> buttons = segments.map(buttonFor).toList();
return Material(
shape: enabledBorder.copyWith(side: BorderSide.none),
elevation: resolve<double?>((ButtonStyle? style) => style?.elevation)!,
shadowColor: resolve<Color?>((ButtonStyle? style) => style?.shadowColor),
surfaceTintColor: resolve<Color?>((ButtonStyle? style) => style?.surfaceTintColor),
child: TextButtonTheme(
data: TextButtonThemeData(style: segmentThemeStyle),
child: _SegmentedButtonRenderWidget<T>(
segments: segments,
enabledBorder: _enabled ? enabledBorder : disabledBorder,
disabledBorder: disabledBorder,
direction: direction,
children: buttons,
),
),
);
}
}
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
_SegmentedButtonRenderWidget({
super.key,
required this.segments,
required this.enabledBorder,
required this.disabledBorder,
required this.direction,
required super.children,
}) : assert(children.length == segments.length);
final List<ButtonSegment<T>> segments;
final OutlinedBorder enabledBorder;
final OutlinedBorder disabledBorder;
final TextDirection direction;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderSegmentedButton<T>(
segments: segments,
enabledBorder: enabledBorder,
disabledBorder: disabledBorder,
textDirection: direction,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSegmentedButton<T> renderObject) {
renderObject
..segments = segments
..enabledBorder = enabledBorder
..disabledBorder = disabledBorder
..textDirection = direction;
}
}
class _SegmentedButtonContainerBoxParentData extends ContainerBoxParentData<RenderBox> {
RRect? surroundingRect;
}
typedef _NextChild = RenderBox? Function(RenderBox child);
class _RenderSegmentedButton<T> extends RenderBox with
ContainerRenderObjectMixin<RenderBox, ContainerBoxParentData<RenderBox>>,
RenderBoxContainerDefaultsMixin<RenderBox, ContainerBoxParentData<RenderBox>> {
_RenderSegmentedButton({
required List<ButtonSegment<T>> segments,
required OutlinedBorder enabledBorder,
required OutlinedBorder disabledBorder,
required TextDirection textDirection,
}) : _segments = segments,
_enabledBorder = enabledBorder,
_disabledBorder = disabledBorder,
_textDirection = textDirection;
List<ButtonSegment<T>> get segments => _segments;
List<ButtonSegment<T>> _segments;
set segments(List<ButtonSegment<T>> value) {
if (listEquals(segments, value)) {
return;
}
_segments = value;
markNeedsLayout();
}
OutlinedBorder get enabledBorder => _enabledBorder;
OutlinedBorder _enabledBorder;
set enabledBorder(OutlinedBorder value) {
if (_enabledBorder == value) {
return;
}
_enabledBorder = value;
markNeedsLayout();
}
OutlinedBorder get disabledBorder => _disabledBorder;
OutlinedBorder _disabledBorder;
set disabledBorder(OutlinedBorder value) {
if (_disabledBorder == value) {
return;
}
_disabledBorder = value;
markNeedsLayout();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (value == _textDirection) {
return;
}
_textDirection = value;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
RenderBox? child = firstChild;
double minWidth = 0.0;
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
final double childWidth = child.getMinIntrinsicWidth(height);
minWidth = math.max(minWidth, childWidth);
child = childParentData.nextSibling;
}
return minWidth * childCount;
}
@override
double computeMaxIntrinsicWidth(double height) {
RenderBox? child = firstChild;
double maxWidth = 0.0;
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
final double childWidth = child.getMaxIntrinsicWidth(height);
maxWidth = math.max(maxWidth, childWidth);
child = childParentData.nextSibling;
}
return maxWidth * childCount;
}
@override
double computeMinIntrinsicHeight(double width) {
RenderBox? child = firstChild;
double minHeight = 0.0;
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
final double childHeight = child.getMinIntrinsicHeight(width);
minHeight = math.max(minHeight, childHeight);
child = childParentData.nextSibling;
}
return minHeight;
}
@override
double computeMaxIntrinsicHeight(double width) {
RenderBox? child = firstChild;
double maxHeight = 0.0;
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
final double childHeight = child.getMaxIntrinsicHeight(width);
maxHeight = math.max(maxHeight, childHeight);
child = childParentData.nextSibling;
}
return maxHeight;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
return defaultComputeDistanceToHighestActualBaseline(baseline);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! _SegmentedButtonContainerBoxParentData) {
child.parentData = _SegmentedButtonContainerBoxParentData();
}
}
void _layoutRects(_NextChild nextChild, RenderBox? leftChild, RenderBox? rightChild) {
RenderBox? child = leftChild;
double start = 0.0;
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
final Offset childOffset = Offset(start, 0.0);
childParentData.offset = childOffset;
final Rect childRect = Rect.fromLTWH(start, 0.0, child.size.width, child.size.height);
final RRect rChildRect = RRect.fromRectAndCorners(childRect);
childParentData.surroundingRect = rChildRect;
start += child.size.width;
child = nextChild(child);
}
}
Size _calculateChildSize(BoxConstraints constraints) {
double maxHeight = 0;
double childWidth = constraints.minWidth / childCount;
RenderBox? child = firstChild;
while (child != null) {
childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity));
child = childAfter(child);
}
childWidth = math.min(childWidth, constraints.maxWidth / childCount);
child = firstChild;
while (child != null) {
final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
maxHeight = math.max(maxHeight, boxHeight);
child = childAfter(child);
}
return Size(childWidth, maxHeight);
}
Size _computeOverallSizeFromChildSize(Size childSize) {
return constraints.constrain(Size(childSize.width * childCount, childSize.height));
}
@override
Size computeDryLayout(BoxConstraints constraints) {
final Size childSize = _calculateChildSize(constraints);
return _computeOverallSizeFromChildSize(childSize);
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final Size childSize = _calculateChildSize(constraints);
final BoxConstraints childConstraints = BoxConstraints.tightFor(
width: childSize.width,
height: childSize.height,
);
RenderBox? child = firstChild;
while (child != null) {
child.layout(childConstraints, parentUsesSize: true);
child = childAfter(child);
}
switch (textDirection) {
case TextDirection.rtl:
_layoutRects(
childBefore,
lastChild,
firstChild,
);
break;
case TextDirection.ltr:
_layoutRects(
childAfter,
firstChild,
lastChild,
);
break;
}
size = _computeOverallSizeFromChildSize(childSize);
}
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final Rect borderRect = offset & size;
final Path borderClipPath = enabledBorder.getInnerPath(borderRect, textDirection: textDirection);
RenderBox? child = firstChild;
RenderBox? previousChild;
int index = 0;
Path? enabledClipPath;
Path? disabledClipPath;
canvas..save()..clipPath(borderClipPath);
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
final Rect childRect = childParentData.surroundingRect!.outerRect.shift(offset);
canvas..save()..clipRect(childRect);
context.paintChild(child, childParentData.offset + offset);
canvas.restore();
// Compute a clip rect for the outer border of the child.
late final double segmentLeft;
late final double segmentRight;
late final double dividerPos;
final double borderOutset = math.max(enabledBorder.side.strokeOutset, disabledBorder.side.strokeOutset);
switch (textDirection) {
case TextDirection.rtl:
segmentLeft = child == lastChild ? borderRect.left - borderOutset : childRect.left;
segmentRight = child == firstChild ? borderRect.right + borderOutset : childRect.right;
dividerPos = segmentRight;
break;
case TextDirection.ltr:
segmentLeft = child == firstChild ? borderRect.left - borderOutset : childRect.left;
segmentRight = child == lastChild ? borderRect.right + borderOutset : childRect.right;
dividerPos = segmentLeft;
break;
}
final Rect segmentClipRect = Rect.fromLTRB(
segmentLeft, borderRect.top - borderOutset,
segmentRight, borderRect.bottom + borderOutset);
// Add the clip rect to the appropriate border clip path
if (segments[index].enabled) {
enabledClipPath = (enabledClipPath ?? Path())..addRect(segmentClipRect);
} else {
disabledClipPath = (disabledClipPath ?? Path())..addRect(segmentClipRect);
}
// Paint the divider between this segment and the previous one.
if (previousChild != null) {
final BorderSide divider = segments[index - 1].enabled || segments[index].enabled
? enabledBorder.side.copyWith(strokeAlign: 0.0)
: disabledBorder.side.copyWith(strokeAlign: 0.0);
final Offset top = Offset(dividerPos, childRect.top);
final Offset bottom = Offset(dividerPos, childRect.bottom);
canvas.drawLine(top, bottom, divider.toPaint());
}
previousChild = child;
child = childAfter(child);
index += 1;
}
canvas.restore();
// Paint the outer border for both disabled and enabled clip rect if needed.
if (disabledClipPath == null) {
// Just paint the enabled border with no clip.
enabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
} else if (enabledClipPath == null) {
// Just paint the disabled border with no.
disabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
} else {
// Paint both of them clipped appropriately for the children segments.
canvas..save()..clipPath(enabledClipPath);
enabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
canvas..restore()..save()..clipPath(disabledClipPath);
disabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
canvas.restore();
}
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
assert(position != null);
RenderBox? child = lastChild;
while (child != null) {
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
if (childParentData.surroundingRect!.contains(position)) {
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset localOffset) {
assert(localOffset == position - childParentData.offset);
return child!.hitTest(result, position: localOffset);
},
);
}
child = childParentData.previousSibling;
}
return false;
}
}
// BEGIN GENERATED TOKEN PROPERTIES - SegmentedButton
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_137
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?>(Theme.of(context).textTheme.labelLarge),
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return _colors.secondaryContainer;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.onSecondaryContainer;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSecondaryContainer;
}
if (states.contains(MaterialState.focused)) {
return _colors.onSecondaryContainer;
}
return _colors.onSecondaryContainer;
} else {
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface;
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface;
}
return null;
}
}),
overlayColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.hovered)) {
return _colors.onSecondaryContainer.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSecondaryContainer.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSecondaryContainer.withOpacity(0.12);
}
} else {
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface.withOpacity(0.12);
}
}
return null;
}),
surfaceTintColor: const MaterialStatePropertyAll<Color>(Colors.transparent),
elevation: const MaterialStatePropertyAll<double>(0),
iconSize: const MaterialStatePropertyAll<double?>(18.0),
side: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return BorderSide(color: _colors.onSurface.withOpacity(0.12));
}
return BorderSide(color: _colors.outline);
}),
shape: const MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder()),
);
}
@override
Widget? get selectedIcon => const Icon(Icons.check);
}
// END GENERATED TOKEN PROPERTIES - SegmentedButton

View file

@ -0,0 +1,172 @@
// 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;
}

View file

@ -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));

View file

@ -0,0 +1,456 @@
// 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.
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
Widget boilerplate({required Widget child}) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(child: child),
);
}
void main() {
testWidgets('SegmentedButton supports exclusive choice by default', (WidgetTester tester) async {
int callbackCount = 0;
int selectedSegment = 2;
Widget frameWithSelection(int selected) {
return Material(
child: boilerplate(
child: SegmentedButton<int>(
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')),
],
selected: <int>{selected},
onSelectionChanged: (Set<int> selected) {
assert(selected.length == 1);
selectedSegment = selected.first;
callbackCount += 1;
},
),
),
);
}
await tester.pumpWidget(frameWithSelection(selectedSegment));
expect(selectedSegment, 2);
expect(callbackCount, 0);
// Tap on segment 1.
await tester.tap(find.text('1'));
await tester.pumpAndSettle();
expect(callbackCount, 1);
expect(selectedSegment, 1);
// Update the selection in the widget
await tester.pumpWidget(frameWithSelection(1));
// Tap on segment 1 again should do nothing.
await tester.tap(find.text('1'));
await tester.pumpAndSettle();
expect(callbackCount, 1);
expect(selectedSegment, 1);
// Tap on segment 3.
await tester.tap(find.text('3'));
await tester.pumpAndSettle();
expect(callbackCount, 2);
expect(selectedSegment, 3);
});
testWidgets('SegmentedButton supports multiple selected segments', (WidgetTester tester) async {
int callbackCount = 0;
Set<int> selection = <int>{1};
Widget frameWithSelection(Set<int> selected) {
return Material(
child: boilerplate(
child: SegmentedButton<int>(
multiSelectionEnabled: true,
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')),
],
selected: selected,
onSelectionChanged: (Set<int> selected) {
selection = selected;
callbackCount += 1;
},
),
),
);
}
await tester.pumpWidget(frameWithSelection(selection));
expect(selection, <int>{1});
expect(callbackCount, 0);
// Tap on segment 2.
await tester.tap(find.text('2'));
await tester.pumpAndSettle();
expect(callbackCount, 1);
expect(selection, <int>{1, 2});
// Update the selection in the widget
await tester.pumpWidget(frameWithSelection(<int>{1, 2}));
await tester.pumpAndSettle();
// Tap on segment 1 again should remove it from selection.
await tester.tap(find.text('1'));
await tester.pumpAndSettle();
expect(callbackCount, 2);
expect(selection, <int>{2});
// Update the selection in the widget
await tester.pumpWidget(frameWithSelection(<int>{2}));
await tester.pumpAndSettle();
// Tap on segment 3.
await tester.tap(find.text('3'));
await tester.pumpAndSettle();
expect(callbackCount, 3);
expect(selection, <int>{2, 3});
});
testWidgets('SegmentedButton allows for empty selection', (WidgetTester tester) async {
int callbackCount = 0;
int? selectedSegment = 1;
Widget frameWithSelection(int? selected) {
return Material(
child: boilerplate(
child: SegmentedButton<int>(
emptySelectionAllowed: true,
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')),
],
selected: <int>{if (selected != null) selected},
onSelectionChanged: (Set<int> selected) {
selectedSegment = selected.isEmpty ? null : selected.first;
callbackCount += 1;
},
),
),
);
}
await tester.pumpWidget(frameWithSelection(selectedSegment));
expect(selectedSegment,1);
expect(callbackCount, 0);
// Tap on segment 1 should deselect it and make the selection empty.
await tester.tap(find.text('1'));
await tester.pumpAndSettle();
expect(callbackCount, 1);
expect(selectedSegment, null);
// Update the selection in the widget
await tester.pumpWidget(frameWithSelection(null));
// Tap on segment 2 should select it.
await tester.tap(find.text('2'));
await tester.pumpAndSettle();
expect(callbackCount, 2);
expect(selectedSegment, 2);
// Update the selection in the widget
await tester.pumpWidget(frameWithSelection(2));
// Tap on segment 3.
await tester.tap(find.text('3'));
await tester.pumpAndSettle();
expect(callbackCount, 3);
expect(selectedSegment, 3);
});
testWidgets('SegmentedButton shows checkboxes for selected segments', (WidgetTester tester) async {
Widget frameWithSelection(int selected) {
return Material(
child: boilerplate(
child: SegmentedButton<int>(
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')),
],
selected: <int>{selected},
onSelectionChanged: (Set<int> selected) {},
),
),
);
}
Finder textHasIcon(String text, IconData icon) {
return find.descendant(
of: find.widgetWithText(Row, text),
matching: find.byIcon(icon)
);
}
await tester.pumpWidget(frameWithSelection(1));
expect(textHasIcon('1', Icons.check), findsOneWidget);
expect(find.byIcon(Icons.check), findsOneWidget);
await tester.pumpWidget(frameWithSelection(2));
expect(textHasIcon('2', Icons.check), findsOneWidget);
expect(find.byIcon(Icons.check), findsOneWidget);
await tester.pumpWidget(frameWithSelection(2));
expect(textHasIcon('2', Icons.check), findsOneWidget);
expect(find.byIcon(Icons.check), findsOneWidget);
});
testWidgets('SegmentedButton shows selected checkboxes in place of icon if it has a label as well', (WidgetTester tester) async {
Widget frameWithSelection(int selected) {
return Material(
child: boilerplate(
child: SegmentedButton<int>(
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(value: 1, icon: Icon(Icons.add), label: Text('1')),
ButtonSegment<int>(value: 2, icon: Icon(Icons.add_a_photo), label: Text('2')),
ButtonSegment<int>(value: 3, icon: Icon(Icons.add_alarm), label: Text('3')),
],
selected: <int>{selected},
onSelectionChanged: (Set<int> selected) {},
),
),
);
}
Finder textHasIcon(String text, IconData icon) {
return find.descendant(
of: find.widgetWithText(Row, text),
matching: find.byIcon(icon)
);
}
await tester.pumpWidget(frameWithSelection(1));
expect(textHasIcon('1', Icons.check), findsOneWidget);
expect(find.byIcon(Icons.add), findsNothing);
expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget);
expect(textHasIcon('3', Icons.add_alarm), findsOneWidget);
await tester.pumpWidget(frameWithSelection(2));
expect(textHasIcon('1', Icons.add), findsOneWidget);
expect(textHasIcon('2', Icons.check), findsOneWidget);
expect(find.byIcon(Icons.add_a_photo), findsNothing);
expect(textHasIcon('3', Icons.add_alarm), findsOneWidget);
await tester.pumpWidget(frameWithSelection(3));
expect(textHasIcon('1', Icons.add), findsOneWidget);
expect(textHasIcon('2', Icons.add_a_photo), findsOneWidget);
expect(textHasIcon('3', Icons.check), findsOneWidget);
expect(find.byIcon(Icons.add_alarm), findsNothing);
});
testWidgets('SegmentedButton shows selected checkboxes next to icon if there is no label', (WidgetTester tester) async {
Widget frameWithSelection(int selected) {
return Material(
child: boilerplate(
child: SegmentedButton<int>(
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(value: 1, icon: Icon(Icons.add)),
ButtonSegment<int>(value: 2, icon: Icon(Icons.add_a_photo)),
ButtonSegment<int>(value: 3, icon: Icon(Icons.add_alarm)),
],
selected: <int>{selected},
onSelectionChanged: (Set<int> selected) {},
),
),
);
}
Finder rowWithIcons(IconData icon1, IconData icon2) {
return find.descendant(
of: find.widgetWithIcon(Row, icon1),
matching: find.byIcon(icon2)
);
}
await tester.pumpWidget(frameWithSelection(1));
expect(rowWithIcons(Icons.add, Icons.check), findsOneWidget);
expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing);
expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing);
await tester.pumpWidget(frameWithSelection(2));
expect(rowWithIcons(Icons.add, Icons.check), findsNothing);
expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsOneWidget);
expect(rowWithIcons(Icons.add_alarm, Icons.check), findsNothing);
await tester.pumpWidget(frameWithSelection(3));
expect(rowWithIcons(Icons.add, Icons.check), findsNothing);
expect(rowWithIcons(Icons.add_a_photo, Icons.check), findsNothing);
expect(rowWithIcons(Icons.add_alarm, Icons.check), findsOneWidget);
});
testWidgets('SegmentedButtons have correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Material(
child: boilerplate(
child: SegmentedButton<int>(
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) {},
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
// First is an unselected, enabled button.
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasCheckedState,
SemanticsFlag.isFocusable,
SemanticsFlag.isInMutuallyExclusiveGroup,
],
label: '1',
actions: <SemanticsAction>[
SemanticsAction.tap,
],
),
// Second is a selected, enabled button.
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasCheckedState,
SemanticsFlag.isChecked,
SemanticsFlag.isFocusable,
SemanticsFlag.isInMutuallyExclusiveGroup,
],
label: '2',
actions: <SemanticsAction>[
SemanticsAction.tap,
],
),
// Third is an unselected, disabled button.
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasCheckedState,
SemanticsFlag.isInMutuallyExclusiveGroup,
],
label: '3',
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets('Multi-select SegmentedButtons have correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Material(
child: boilerplate(
child: SegmentedButton<int>(
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>{1, 3},
onSelectionChanged: (Set<int> selected) {},
multiSelectionEnabled: true,
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
// First is selected, enabled button.
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasCheckedState,
SemanticsFlag.isChecked,
SemanticsFlag.isFocusable,
],
label: '1',
actions: <SemanticsAction>[
SemanticsAction.tap,
],
),
// Second is an unselected, enabled button.
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isEnabled,
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasCheckedState,
SemanticsFlag.isFocusable,
],
label: '2',
actions: <SemanticsAction>[
SemanticsAction.tap,
],
),
// Third is a selected, disabled button.
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isChecked,
SemanticsFlag.hasCheckedState,
],
label: '3',
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
}

View file

@ -0,0 +1,473 @@
// 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('SegmentedButtonThemeData copyWith, ==, hashCode basics', () {
expect(const SegmentedButtonThemeData(), const SegmentedButtonThemeData().copyWith());
expect(const SegmentedButtonThemeData().hashCode, const SegmentedButtonThemeData().copyWith().hashCode);
const SegmentedButtonThemeData custom = SegmentedButtonThemeData(
style: ButtonStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.green)),
selectedIcon: Icon(Icons.error),
);
final SegmentedButtonThemeData copy = const SegmentedButtonThemeData().copyWith(
style: custom.style,
selectedIcon: custom.selectedIcon,
);
expect(copy, custom);
});
testWidgets('Default SegmentedButtonThemeData debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SegmentedButtonThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('With no other configuration, defaults are used', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Center(
child: SegmentedButton<int>(
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) { },
),
),
),
),
);
// Test first segment, should be enabled
{
final Finder text = find.text('1');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.check));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, theme.colorScheme.primary);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
// Test second segment, should be enabled and selected
{
final Finder text = find.text('2');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.check));
final Material material = tester.widget<Material>(parent);
expect(material.color, theme.colorScheme.secondaryContainer);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, theme.colorScheme.onSecondaryContainer);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsOneWidget);
}
// Test last segment, should be disabled
{
final Finder text = find.text('3');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.check));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, theme.colorScheme.onSurface.withOpacity(0.38));
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
});
testWidgets('ThemeData.segmentedButtonTheme overrides defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(
useMaterial3: true,
segmentedButtonTheme: SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.blue;
}
if (states.contains(MaterialState.selected)) {
return Colors.purple;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.yellow;
}
if (states.contains(MaterialState.selected)) {
return Colors.brown;
} else {
return Colors.cyan;
}
}),
),
selectedIcon: const Icon(Icons.error),
),
);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Center(
child: SegmentedButton<int>(
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) { },
),
),
),
),
);
// Test first segment, should be enabled
{
final Finder text = find.text('1');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.error));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.cyan);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
// Test second segment, should be enabled and selected
{
final Finder text = find.text('2');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.error));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.purple);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.brown);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsOneWidget);
}
// Test last segment, should be disabled
{
final Finder text = find.text('3');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.error));
final Material material = tester.widget<Material>(parent);
expect(material.color, Colors.blue);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.yellow);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
});
testWidgets('SegmentedButtonTheme overrides ThemeData and defaults', (WidgetTester tester) async {
final SegmentedButtonThemeData global = SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.blue;
}
if (states.contains(MaterialState.selected)) {
return Colors.purple;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.yellow;
}
if (states.contains(MaterialState.selected)) {
return Colors.brown;
} else {
return Colors.cyan;
}
}),
),
selectedIcon: const Icon(Icons.error),
);
final SegmentedButtonThemeData segmentedTheme = SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.lightBlue;
}
if (states.contains(MaterialState.selected)) {
return Colors.lightGreen;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.lime;
}
if (states.contains(MaterialState.selected)) {
return Colors.amber;
} else {
return Colors.deepPurple;
}
}),
),
selectedIcon: const Icon(Icons.plus_one),
);
final ThemeData theme = ThemeData(
useMaterial3: true,
segmentedButtonTheme: global,
);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: SegmentedButtonTheme(
data: segmentedTheme,
child: Scaffold(
body: Center(
child: SegmentedButton<int>(
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) { },
),
),
),
),
),
);
// Test first segment, should be enabled
{
final Finder text = find.text('1');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.plus_one));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.deepPurple);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
// Test second segment, should be enabled and selected
{
final Finder text = find.text('2');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.plus_one));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.lightGreen);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.amber);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsOneWidget);
}
// Test last segment, should be disabled
{
final Finder text = find.text('3');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.plus_one));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.lightBlue);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.lime);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
});
testWidgets('Widget parameters overrides SegmentedTheme, ThemeData and defaults', (WidgetTester tester) async {
final SegmentedButtonThemeData global = SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.blue;
}
if (states.contains(MaterialState.selected)) {
return Colors.purple;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.yellow;
}
if (states.contains(MaterialState.selected)) {
return Colors.brown;
} else {
return Colors.cyan;
}
}),
),
selectedIcon: const Icon(Icons.error),
);
final SegmentedButtonThemeData segmentedTheme = SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.lightBlue;
}
if (states.contains(MaterialState.selected)) {
return Colors.lightGreen;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.lime;
}
if (states.contains(MaterialState.selected)) {
return Colors.amber;
} else {
return Colors.deepPurple;
}
}),
),
selectedIcon: const Icon(Icons.plus_one),
);
final ThemeData theme = ThemeData(
useMaterial3: true,
segmentedButtonTheme: global,
);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: SegmentedButtonTheme(
data: segmentedTheme,
child: Scaffold(
body: Center(
child: SegmentedButton<int>(
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) { },
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.black12;
}
if (states.contains(MaterialState.selected)) {
return Colors.grey;
}
return null;
}),
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return Colors.amberAccent;
}
if (states.contains(MaterialState.selected)) {
return Colors.deepOrange;
} else {
return Colors.deepPurpleAccent;
}
}),
),
selectedIcon: const Icon(Icons.alarm),
),
),
),
),
),
);
// Test first segment, should be enabled
{
final Finder text = find.text('1');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.alarm));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.transparent);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.deepPurpleAccent);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
// Test second segment, should be enabled and selected
{
final Finder text = find.text('2');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.alarm));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.grey);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.deepOrange);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsOneWidget);
}
// Test last segment, should be disabled
{
final Finder text = find.text('3');
final Finder parent = find.ancestor(of: text, matching: find.byType(Material)).first;
final Finder selectedIcon = find.descendant(of: parent, matching: find.byIcon(Icons.alarm));
final Material material = tester.widget<Material>(parent);
expect(material.animationDuration, const Duration(milliseconds: 200));
expect(material.borderRadius, null);
expect(material.color, Colors.black12);
expect(material.shape, const RoundedRectangleBorder());
expect(material.textStyle!.color, Colors.amberAccent);
expect(material.textStyle!.fontFamily, 'Roboto');
expect(material.textStyle!.fontSize, 14);
expect(material.textStyle!.fontWeight, FontWeight.w500);
expect(selectedIcon, findsNothing);
}
});
}

View file

@ -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',