mirror of
https://github.com/flutter/flutter
synced 2024-08-28 04:21:14 +00:00
M3 Segmented Button widget (#113723)
This commit is contained in:
parent
877276812b
commit
6ec2bd0a80
|
@ -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();
|
||||
|
|
124
dev/tools/gen_defaults/lib/segmented_button_template.dart
Normal file
124
dev/tools/gen_defaults/lib/segmented_button_template.dart
Normal 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);
|
||||
}
|
||||
''';
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
813
packages/flutter/lib/src/material/segmented_button.dart
Normal file
813
packages/flutter/lib/src/material/segmented_button.dart
Normal 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
|
172
packages/flutter/lib/src/material/segmented_button_theme.dart
Normal file
172
packages/flutter/lib/src/material/segmented_button_theme.dart
Normal 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;
|
||||
}
|
|
@ -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));
|
||||
|
|
456
packages/flutter/test/material/segmented_button_test.dart
Normal file
456
packages/flutter/test/material/segmented_button_test.dart
Normal 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();
|
||||
});
|
||||
}
|
473
packages/flutter/test/material/segmented_button_theme_test.dart
Normal file
473
packages/flutter/test/material/segmented_button_theme_test.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue