mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
fixes [Add missing `overlayColor` property in `styleFrom` methods](https://github.com/flutter/flutter/issues/146636) ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; enum Sizes { extraSmall, small, medium, large, extraLarge } void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('styleFrom(overlayColor: Colors.red)', style: Theme.of(context).textTheme.titleLarge), TextButton( style: TextButton.styleFrom(overlayColor: Colors.red), onPressed: () {}, child: const Text('TextButton'), ), IconButton( style: IconButton.styleFrom( overlayColor: Colors.red, ), onPressed: () {}, icon: const Icon(Icons.add), ), MenuBar( children: [ MenuItemButton( style: MenuItemButton.styleFrom(overlayColor: Colors.red), child: const Text('MenuItemButton'), onPressed: () {}, ), SubmenuButton( style: SubmenuButton.styleFrom(overlayColor: Colors.red), menuChildren: [ MenuItemButton( child: const Text('MenuItemButton'), onPressed: () {}, ), ], child: const Text('SubmenuButton'), ), ], ), SegmentedButton<Sizes>( style: SegmentedButton.styleFrom(overlayColor: Colors.red), 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: const {Sizes.medium}, onSelectionChanged: (Set<Sizes> newSelection) {}, multiSelectionEnabled: true, ), ], ), ), ), ); } } ``` </details> ### Preview 
1108 lines
42 KiB
Dart
1108 lines
42 KiB
Dart
// 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 'dart:math';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import 'button_style.dart';
|
|
import 'button_style_button.dart';
|
|
import 'color_scheme.dart';
|
|
import 'colors.dart';
|
|
import 'constants.dart';
|
|
import 'icons.dart';
|
|
import 'ink_well.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';
|
|
import 'theme_data.dart';
|
|
import 'tooltip.dart';
|
|
|
|
/// Data describing a segment of a [SegmentedButton].
|
|
class ButtonSegment<T> {
|
|
/// Construct a [ButtonSegment].
|
|
///
|
|
/// One of [icon] or [label] must be non-null.
|
|
const ButtonSegment({
|
|
required this.value,
|
|
this.icon,
|
|
this.label,
|
|
this.tooltip,
|
|
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;
|
|
|
|
/// Optional tooltip for the segment
|
|
final String? tooltip;
|
|
|
|
/// 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}
|
|
///
|
|
/// {@tool dartpad}
|
|
/// This sample showcases how to customize [SegmentedButton] using [SegmentedButton.styleFrom].
|
|
///
|
|
/// ** See code in examples/api/lib/material/segmented_button/segmented_button.1.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 StatefulWidget {
|
|
/// 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 segmented 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.expandedInsets,
|
|
this.style,
|
|
this.showSelectedIcon = true,
|
|
this.selectedIcon,
|
|
}) : assert(segments.length > 0),
|
|
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 segment 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 segments 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;
|
|
|
|
/// Determines the segmented button's size and padding based on [expandedInsets].
|
|
///
|
|
/// If null (default), the button adopts its intrinsic content size. When specified,
|
|
/// the button expands to fill its parent's space, with the [EdgeInsets]
|
|
/// defining the padding.
|
|
final EdgeInsets? expandedInsets;
|
|
|
|
/// A static convenience method that constructs a segmented button
|
|
/// [ButtonStyle] given simple values.
|
|
///
|
|
/// The [foregroundColor], [selectedForegroundColor], and [disabledForegroundColor]
|
|
/// colors are used to create a [MaterialStateProperty] [ButtonStyle.foregroundColor],
|
|
/// and a derived [ButtonStyle.overlayColor] if [overlayColor] isn't specified.
|
|
///
|
|
/// If [overlayColor] is specified and its value is [Colors.transparent]
|
|
/// then the pressed/focused/hovered highlights are effectively defeated.
|
|
/// Otherwise a [MaterialStateProperty] with the same opacities as the
|
|
/// default is created.
|
|
///
|
|
/// The [backgroundColor], [selectedBackgroundColor] and [disabledBackgroundColor]
|
|
/// colors are used to create a [MaterialStateProperty] [ButtonStyle.backgroundColor].
|
|
///
|
|
/// Similarly, the [enabledMouseCursor] and [disabledMouseCursor]
|
|
/// parameters are used to construct [ButtonStyle.mouseCursor].
|
|
///
|
|
/// All of the other parameters are either used directly or used to
|
|
/// create a [MaterialStateProperty] with a single value for all
|
|
/// states.
|
|
///
|
|
/// All parameters default to null. By default this method returns
|
|
/// a [ButtonStyle] that doesn't override anything.
|
|
///
|
|
/// {@tool snippet}
|
|
///
|
|
/// For example, to override the default text and icon colors for a
|
|
/// [SegmentedButton], as well as its overlay color, with all of the
|
|
/// standard opacity adjustments for the pressed, focused, and
|
|
/// hovered states, one could write:
|
|
///
|
|
/// ** See code in examples/api/lib/material/segmented_button/segmented_button.1.dart **
|
|
///
|
|
/// ```dart
|
|
/// SegmentedButton<int>(
|
|
/// style: SegmentedButton.styleFrom(
|
|
/// foregroundColor: Colors.black,
|
|
/// selectedForegroundColor: Colors.white,
|
|
/// backgroundColor: Colors.amber,
|
|
/// selectedBackgroundColor: Colors.red,
|
|
/// ),
|
|
/// segments: const <ButtonSegment<int>>[
|
|
/// ButtonSegment<int>(
|
|
/// value: 0,
|
|
/// label: Text('0'),
|
|
/// icon: Icon(Icons.calendar_view_day),
|
|
/// ),
|
|
/// ButtonSegment<int>(
|
|
/// value: 1,
|
|
/// label: Text('1'),
|
|
/// icon: Icon(Icons.calendar_view_week),
|
|
/// ),
|
|
/// ],
|
|
/// selected: const <int>{0},
|
|
/// onSelectionChanged: (Set<int> selection) {},
|
|
/// ),
|
|
/// ```
|
|
/// {@end-tool}
|
|
static ButtonStyle styleFrom({
|
|
Color? foregroundColor,
|
|
Color? backgroundColor,
|
|
Color? selectedForegroundColor,
|
|
Color? selectedBackgroundColor,
|
|
Color? disabledForegroundColor,
|
|
Color? disabledBackgroundColor,
|
|
Color? shadowColor,
|
|
Color? surfaceTintColor,
|
|
Color? overlayColor,
|
|
double? elevation,
|
|
TextStyle? textStyle,
|
|
EdgeInsetsGeometry? padding,
|
|
Size? minimumSize,
|
|
Size? fixedSize,
|
|
Size? maximumSize,
|
|
BorderSide? side,
|
|
OutlinedBorder? shape,
|
|
MouseCursor? enabledMouseCursor,
|
|
MouseCursor? disabledMouseCursor,
|
|
VisualDensity? visualDensity,
|
|
MaterialTapTargetSize? tapTargetSize,
|
|
Duration? animationDuration,
|
|
bool? enableFeedback,
|
|
AlignmentGeometry? alignment,
|
|
InteractiveInkFeatureFactory? splashFactory,
|
|
}) {
|
|
final MaterialStateProperty<Color?>? foregroundColorProp =
|
|
(foregroundColor == null && disabledForegroundColor == null && selectedForegroundColor == null)
|
|
? null
|
|
: _SegmentButtonDefaultColor(foregroundColor, disabledForegroundColor, selectedForegroundColor);
|
|
final MaterialStateProperty<Color?>? backgroundColorProp =
|
|
(backgroundColor == null && disabledBackgroundColor == null && selectedBackgroundColor == null)
|
|
? null
|
|
: _SegmentButtonDefaultColor(backgroundColor, disabledBackgroundColor, selectedBackgroundColor);
|
|
final MaterialStateProperty<Color?>? overlayColorProp = (foregroundColor == null &&
|
|
selectedForegroundColor == null && overlayColor == null)
|
|
? null
|
|
: switch (overlayColor) {
|
|
(final Color overlayColor) when overlayColor.value == 0 => const MaterialStatePropertyAll<Color?>(Colors.transparent),
|
|
_ => _SegmentedButtonDefaultsM3.resolveStateColor(foregroundColor, selectedForegroundColor, overlayColor),
|
|
};
|
|
return TextButton.styleFrom(
|
|
textStyle: textStyle,
|
|
shadowColor: shadowColor,
|
|
surfaceTintColor: surfaceTintColor,
|
|
elevation: elevation,
|
|
padding: padding,
|
|
minimumSize: minimumSize,
|
|
fixedSize: fixedSize,
|
|
maximumSize: maximumSize,
|
|
side: side,
|
|
shape: shape,
|
|
enabledMouseCursor: enabledMouseCursor,
|
|
disabledMouseCursor: disabledMouseCursor,
|
|
visualDensity: visualDensity,
|
|
tapTargetSize: tapTargetSize,
|
|
animationDuration: animationDuration,
|
|
enableFeedback: enableFeedback,
|
|
alignment: alignment,
|
|
splashFactory: splashFactory,
|
|
).copyWith(
|
|
foregroundColor: foregroundColorProp,
|
|
backgroundColor: backgroundColorProp,
|
|
overlayColor: overlayColorProp,
|
|
);
|
|
}
|
|
|
|
/// 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 individual
|
|
/// 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;
|
|
|
|
@override
|
|
State<SegmentedButton<T>> createState() => SegmentedButtonState<T>();
|
|
}
|
|
|
|
/// State for [SegmentedButton].
|
|
@visibleForTesting
|
|
class SegmentedButtonState<T> extends State<SegmentedButton<T>> {
|
|
bool get _enabled => widget.onSelectionChanged != null;
|
|
|
|
/// Controllers for the [ButtonSegment]s.
|
|
@visibleForTesting
|
|
final Map<ButtonSegment<T>, MaterialStatesController> statesControllers = <ButtonSegment<T>, MaterialStatesController>{};
|
|
|
|
@override
|
|
void didUpdateWidget(covariant SegmentedButton<T> oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget != widget) {
|
|
statesControllers.removeWhere((ButtonSegment<T> segment, MaterialStatesController controller) {
|
|
if (widget.segments.contains(segment)) {
|
|
return false;
|
|
} else {
|
|
controller.dispose();
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void _handleOnPressed(T segmentValue) {
|
|
if (!_enabled) {
|
|
return;
|
|
}
|
|
final bool onlySelectedSegment = widget.selected.length == 1 && widget.selected.contains(segmentValue);
|
|
final bool validChange = widget.emptySelectionAllowed || !onlySelectedSegment;
|
|
if (validChange) {
|
|
final bool toggle = widget.multiSelectionEnabled || (widget.emptySelectionAllowed && onlySelectedSegment);
|
|
final Set<T> pressedSegment = <T>{segmentValue};
|
|
late final Set<T> updatedSelection;
|
|
if (toggle) {
|
|
updatedSelection = widget.selected.contains(segmentValue)
|
|
? widget.selected.difference(pressedSegment)
|
|
: widget.selected.union(pressedSegment);
|
|
} else {
|
|
updatedSelection = pressedSegment;
|
|
}
|
|
if (!setEquals(updatedSelection, widget.selected)) {
|
|
widget.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(widget.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(widget.style);
|
|
final ButtonStyle segmentThemeStyle = segmentStyleFor(theme.style).merge(segmentStyleFor(defaults.style));
|
|
final Widget? selectedIcon = widget.showSelectedIcon
|
|
? widget.selectedIcon ?? theme.selectedIcon ?? defaults.selectedIcon
|
|
: null;
|
|
|
|
Widget buttonFor(ButtonSegment<T> segment) {
|
|
final Widget label = segment.label ?? segment.icon ?? const SizedBox.shrink();
|
|
final bool segmentSelected = widget.selected.contains(segment.value);
|
|
final Widget? icon = (segmentSelected && widget.showSelectedIcon)
|
|
? selectedIcon
|
|
: segment.label != null
|
|
? segment.icon
|
|
: null;
|
|
final MaterialStatesController controller = statesControllers.putIfAbsent(segment, () => MaterialStatesController());
|
|
controller.update(MaterialState.selected, segmentSelected);
|
|
|
|
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,
|
|
);
|
|
|
|
final Widget buttonWithTooltip = segment.tooltip != null
|
|
? Tooltip(
|
|
message: segment.tooltip,
|
|
child: button,
|
|
)
|
|
: button;
|
|
|
|
return MergeSemantics(
|
|
child: Semantics(
|
|
checked: segmentSelected,
|
|
inMutuallyExclusiveGroup: widget.multiSelectionEnabled ? null : true,
|
|
child: buttonWithTooltip,
|
|
),
|
|
);
|
|
}
|
|
|
|
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 VisualDensity resolvedVisualDensity = segmentStyle.visualDensity ?? segmentThemeStyle.visualDensity ?? Theme.of(context).visualDensity;
|
|
final EdgeInsetsGeometry resolvedPadding = resolve<EdgeInsetsGeometry?>((ButtonStyle? style) => style?.padding, enabledState) ?? EdgeInsets.zero;
|
|
final MaterialTapTargetSize resolvedTapTargetSize = segmentStyle.tapTargetSize ?? segmentThemeStyle.tapTargetSize ?? Theme.of(context).materialTapTargetSize;
|
|
final double fontSize = resolve<TextStyle?>((ButtonStyle? style) => style?.textStyle, enabledState)?.fontSize ?? 20.0;
|
|
|
|
final List<Widget> buttons = widget.segments.map(buttonFor).toList();
|
|
|
|
final Offset densityAdjustment = resolvedVisualDensity.baseSizeAdjustment;
|
|
const double textButtonMinHeight = 40.0;
|
|
|
|
final double adjustButtonMinHeight = textButtonMinHeight + densityAdjustment.dy;
|
|
final double effectiveVerticalPadding = resolvedPadding.vertical + densityAdjustment.dy * 2;
|
|
final double effectedButtonHeight = max(fontSize + effectiveVerticalPadding, adjustButtonMinHeight);
|
|
final double tapTargetVerticalPadding = switch (resolvedTapTargetSize) {
|
|
MaterialTapTargetSize.shrinkWrap => 0.0,
|
|
MaterialTapTargetSize.padded => max(0, kMinInteractiveDimension + densityAdjustment.dy - effectedButtonHeight)
|
|
};
|
|
|
|
return Material(
|
|
type: MaterialType.transparency,
|
|
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: Padding(
|
|
padding: widget.expandedInsets ?? EdgeInsets.zero,
|
|
child: _SegmentedButtonRenderWidget<T>(
|
|
tapTargetVerticalPadding: tapTargetVerticalPadding,
|
|
segments: widget.segments,
|
|
enabledBorder: _enabled ? enabledBorder : disabledBorder,
|
|
disabledBorder: disabledBorder,
|
|
direction: direction,
|
|
isExpanded: widget.expandedInsets != null,
|
|
children: buttons,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
for (final MaterialStatesController controller in statesControllers.values) {
|
|
controller.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
@immutable
|
|
class _SegmentButtonDefaultColor extends MaterialStateProperty<Color?> with Diagnosticable {
|
|
_SegmentButtonDefaultColor(this.color, this.disabled, this.selected);
|
|
|
|
final Color? color;
|
|
final Color? disabled;
|
|
final Color? selected;
|
|
|
|
@override
|
|
Color? resolve(Set<MaterialState> states) {
|
|
if (states.contains(MaterialState.disabled)) {
|
|
return disabled;
|
|
}
|
|
if (states.contains(MaterialState.selected)) {
|
|
return selected;
|
|
}
|
|
return color;
|
|
}
|
|
}
|
|
|
|
class _SegmentedButtonRenderWidget<T> extends MultiChildRenderObjectWidget {
|
|
const _SegmentedButtonRenderWidget({
|
|
super.key,
|
|
required this.segments,
|
|
required this.enabledBorder,
|
|
required this.disabledBorder,
|
|
required this.direction,
|
|
required this.tapTargetVerticalPadding,
|
|
required this.isExpanded,
|
|
required super.children,
|
|
}) : assert(children.length == segments.length);
|
|
|
|
final List<ButtonSegment<T>> segments;
|
|
final OutlinedBorder enabledBorder;
|
|
final OutlinedBorder disabledBorder;
|
|
final TextDirection direction;
|
|
final double tapTargetVerticalPadding;
|
|
final bool isExpanded;
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
return _RenderSegmentedButton<T>(
|
|
segments: segments,
|
|
enabledBorder: enabledBorder,
|
|
disabledBorder: disabledBorder,
|
|
textDirection: direction,
|
|
tapTargetVerticalPadding: tapTargetVerticalPadding,
|
|
isExpanded: isExpanded,
|
|
);
|
|
}
|
|
|
|
@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,
|
|
required double tapTargetVerticalPadding,
|
|
required bool isExpanded,
|
|
}) : _segments = segments,
|
|
_enabledBorder = enabledBorder,
|
|
_disabledBorder = disabledBorder,
|
|
_textDirection = textDirection,
|
|
_tapTargetVerticalPadding = tapTargetVerticalPadding,
|
|
_isExpanded = isExpanded;
|
|
|
|
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();
|
|
}
|
|
|
|
double get tapTargetVerticalPadding => _tapTargetVerticalPadding;
|
|
double _tapTargetVerticalPadding;
|
|
set tapTargetVerticalPadding(double value) {
|
|
if (value == _tapTargetVerticalPadding) {
|
|
return;
|
|
}
|
|
_tapTargetVerticalPadding = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
bool get isExpanded => _isExpanded;
|
|
bool _isExpanded;
|
|
set isExpanded(bool value) {
|
|
if (value == _isExpanded) {
|
|
return;
|
|
}
|
|
_isExpanded = 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;
|
|
RenderBox? child = firstChild;
|
|
double childWidth;
|
|
if (_isExpanded) {
|
|
childWidth = constraints.maxWidth / childCount;
|
|
} else {
|
|
childWidth = constraints.minWidth / childCount;
|
|
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
|
|
double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
|
|
final Size childSize = _calculateChildSize(constraints);
|
|
final BoxConstraints childConstraints = BoxConstraints.tight(childSize);
|
|
|
|
BaselineOffset baselineOffset = BaselineOffset.noBaseline;
|
|
for (RenderBox? child = firstChild; child != null; child = childAfter(child)) {
|
|
baselineOffset = baselineOffset.minOf(BaselineOffset(child.getDryBaseline(childConstraints, baseline)));
|
|
}
|
|
return baselineOffset.offset;
|
|
}
|
|
|
|
@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,
|
|
);
|
|
case TextDirection.ltr:
|
|
_layoutRects(
|
|
childAfter,
|
|
firstChild,
|
|
lastChild,
|
|
);
|
|
}
|
|
|
|
size = _computeOverallSizeFromChildSize(childSize);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
|
|
final Rect borderRect = (offset + Offset(0, tapTargetVerticalPadding / 2)) & (Size(size.width, size.height - tapTargetVerticalPadding));
|
|
final Path borderClipPath = enabledBorder.getInnerPath(borderRect, textDirection: textDirection);
|
|
RenderBox? child = firstChild;
|
|
RenderBox? previousChild;
|
|
int index = 0;
|
|
Path? enabledClipPath;
|
|
Path? disabledClipPath;
|
|
|
|
context.canvas..save()..clipPath(borderClipPath);
|
|
while (child != null) {
|
|
final _SegmentedButtonContainerBoxParentData childParentData = child.parentData! as _SegmentedButtonContainerBoxParentData;
|
|
final Rect childRect = childParentData.surroundingRect!.outerRect.shift(offset);
|
|
|
|
context.canvas..save()..clipRect(childRect);
|
|
context.paintChild(child, childParentData.offset + offset);
|
|
context.canvas.restore();
|
|
|
|
// Compute a clip rect for the outer border of the child.
|
|
final double segmentLeft;
|
|
final double segmentRight;
|
|
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;
|
|
case TextDirection.ltr:
|
|
segmentLeft = child == firstChild ? borderRect.left - borderOutset : childRect.left;
|
|
segmentRight = child == lastChild ? borderRect.right + borderOutset : childRect.right;
|
|
dividerPos = segmentLeft;
|
|
}
|
|
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);
|
|
context.canvas.drawLine(top, bottom, divider.toPaint());
|
|
}
|
|
|
|
previousChild = child;
|
|
child = childAfter(child);
|
|
index += 1;
|
|
}
|
|
context.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.
|
|
context.canvas..save()..clipPath(enabledClipPath);
|
|
enabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
|
|
context.canvas..restore()..save()..clipPath(disabledClipPath);
|
|
disabledBorder.paint(context.canvas, borderRect, textDirection: textDirection);
|
|
context.canvas.restore();
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
|
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.
|
|
|
|
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 _colors.onSurface;
|
|
}
|
|
}),
|
|
overlayColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
|
if (states.contains(MaterialState.selected)) {
|
|
if (states.contains(MaterialState.pressed)) {
|
|
return _colors.onSecondaryContainer.withOpacity(0.1);
|
|
}
|
|
if (states.contains(MaterialState.hovered)) {
|
|
return _colors.onSecondaryContainer.withOpacity(0.08);
|
|
}
|
|
if (states.contains(MaterialState.focused)) {
|
|
return _colors.onSecondaryContainer.withOpacity(0.1);
|
|
}
|
|
} else {
|
|
if (states.contains(MaterialState.pressed)) {
|
|
return _colors.onSurface.withOpacity(0.1);
|
|
}
|
|
if (states.contains(MaterialState.hovered)) {
|
|
return _colors.onSurface.withOpacity(0.08);
|
|
}
|
|
if (states.contains(MaterialState.focused)) {
|
|
return _colors.onSurface.withOpacity(0.1);
|
|
}
|
|
}
|
|
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()),
|
|
minimumSize: const MaterialStatePropertyAll<Size?>(Size.fromHeight(40.0)),
|
|
);
|
|
}
|
|
@override
|
|
Widget? get selectedIcon => const Icon(Icons.check);
|
|
|
|
static MaterialStateProperty<Color?> resolveStateColor(Color? unselectedColor, Color? selectedColor, Color? overlayColor){
|
|
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
|
if (states.contains(MaterialState.selected)) {
|
|
if (states.contains(MaterialState.pressed)) {
|
|
return (overlayColor ?? selectedColor)?.withOpacity(0.1);
|
|
}
|
|
if (states.contains(MaterialState.hovered)) {
|
|
return (overlayColor ?? selectedColor)?.withOpacity(0.08);
|
|
}
|
|
if (states.contains(MaterialState.focused)) {
|
|
return (overlayColor ?? selectedColor)?.withOpacity(0.1);
|
|
}
|
|
} else {
|
|
if (states.contains(MaterialState.pressed)) {
|
|
return (overlayColor ?? unselectedColor)?.withOpacity(0.1);
|
|
}
|
|
if (states.contains(MaterialState.hovered)) {
|
|
return (overlayColor ?? unselectedColor)?.withOpacity(0.08);
|
|
}
|
|
if (states.contains(MaterialState.focused)) {
|
|
return (overlayColor ?? unselectedColor)?.withOpacity(0.1);
|
|
}
|
|
}
|
|
return Colors.transparent;
|
|
});
|
|
}
|
|
}
|
|
|
|
// END GENERATED TOKEN PROPERTIES - SegmentedButton
|