Introduce new Material 3 Slider
shapes (#152237)
fixes [Update `Slider` for Material 3 redesign](https://github.com/flutter/flutter/issues/141842) previous implementation https://github.com/flutter/flutter/pull/147783 ### Description This PR introduces new Material 3 Slider design. ### Slider Preview <img width="912" alt="Screenshot 2024-07-24 at 16 38 11" src="https://github.com/user-attachments/assets/9645ff6c-b72a-40aa-ae95-4f76994f8302"> <img width="912" alt="Screenshot 2024-07-24 at 16 38 24" src="https://github.com/user-attachments/assets/fbaed8bb-2717-43a9-9415-ea1365165d9a"> ### Value indicator Preview https://github.com/user-attachments/assets/45fa001c-de81-433a-a8e9-6c0d6a2335c0 ### New stop indicator https://github.com/user-attachments/assets/ad05621d-042d-4b17-9dbb-7f7b802a2593 ### Customized <img width="912" alt="Screenshot 2024-07-24 at 16 41 49" src="https://github.com/user-attachments/assets/2f279240-5af8-4bc8-9c65-a4b4ac718101"> ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
8536b96ebb
commit
56cfef73fc
@ -47,6 +47,7 @@ import 'package:gen_defaults/radio_template.dart';
|
||||
import 'package:gen_defaults/search_bar_template.dart';
|
||||
import 'package:gen_defaults/search_view_template.dart';
|
||||
import 'package:gen_defaults/segmented_button_template.dart';
|
||||
import 'package:gen_defaults/slider_template.dart';
|
||||
import 'package:gen_defaults/snackbar_template.dart';
|
||||
import 'package:gen_defaults/surface_tint.dart';
|
||||
import 'package:gen_defaults/switch_template.dart';
|
||||
@ -142,8 +143,7 @@ Future<void> main(List<String> args) async {
|
||||
SearchViewTemplate('SearchView', '$materialLib/search_anchor.dart', tokens).updateFile();
|
||||
SegmentedButtonTemplate('md.comp.outlined-segmented-button', 'SegmentedButton', '$materialLib/segmented_button.dart', tokens).updateFile();
|
||||
SnackbarTemplate('md.comp.snackbar', 'Snackbar', '$materialLib/snack_bar.dart', tokens).updateFile();
|
||||
// TODO(QuncCccccc): uncomment `SliderTemplate` once `Slider` widget is updated to match the latest M3 specs.
|
||||
// SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.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();
|
||||
TimePickerTemplate('TimePicker', '$materialLib/time_picker.dart', tokens).updateFile();
|
||||
|
@ -655,6 +655,33 @@ md.comp.sheet.bottom.docked.drag-handle.height,
|
||||
md.comp.sheet.bottom.docked.drag-handle.width,
|
||||
md.comp.sheet.bottom.docked.modal.container.elevation,
|
||||
md.comp.sheet.bottom.docked.standard.container.elevation,
|
||||
md.comp.slider.active.handle.padding,
|
||||
md.comp.slider.active.stop-indicator.container.color,
|
||||
md.comp.slider.active.stop-indicator.container.opacity,
|
||||
md.comp.slider.active.track.color,
|
||||
md.comp.slider.active.track.height,
|
||||
md.comp.slider.disabled.active.stop-indicator.container.color,
|
||||
md.comp.slider.disabled.active.track.color,
|
||||
md.comp.slider.disabled.active.track.opacity,
|
||||
md.comp.slider.disabled.handle.color,
|
||||
md.comp.slider.disabled.handle.opacity,
|
||||
md.comp.slider.disabled.handle.width,
|
||||
md.comp.slider.disabled.inactive.stop-indicator.container.color,
|
||||
md.comp.slider.disabled.inactive.track.color,
|
||||
md.comp.slider.disabled.inactive.track.opacity,
|
||||
md.comp.slider.focus.handle.width,
|
||||
md.comp.slider.handle.color,
|
||||
md.comp.slider.handle.height,
|
||||
md.comp.slider.handle.width,
|
||||
md.comp.slider.hover.handle.width,
|
||||
md.comp.slider.inactive.stop-indicator.container.color,
|
||||
md.comp.slider.inactive.stop-indicator.container.opacity,
|
||||
md.comp.slider.inactive.track.color,
|
||||
md.comp.slider.pressed.handle.width,
|
||||
md.comp.slider.stop-indicator.size,
|
||||
md.comp.slider.value-indicator.container.color,
|
||||
md.comp.slider.value-indicator.label.label-text.color,
|
||||
md.comp.slider.value-indicator.label.label-text.text-style,
|
||||
md.comp.snackbar.action.focus.label-text.color,
|
||||
md.comp.snackbar.action.hover.label-text.color,
|
||||
md.comp.snackbar.action.label-text.color,
|
||||
|
|
@ -27,7 +27,7 @@ class _${blockName}DefaultsM3 extends SliderThemeData {
|
||||
Color? get inactiveTrackColor => ${componentColor('$tokenGroup.inactive.track')};
|
||||
|
||||
@override
|
||||
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
|
||||
Color? get secondaryActiveTrackColor => ${componentColor('$tokenGroup.active.track')}.withOpacity(0.54);
|
||||
|
||||
@override
|
||||
Color? get disabledActiveTrackColor => ${componentColor('$tokenGroup.disabled.active.track')};
|
||||
@ -36,49 +36,85 @@ class _${blockName}DefaultsM3 extends SliderThemeData {
|
||||
Color? get disabledInactiveTrackColor => ${componentColor('$tokenGroup.disabled.inactive.track')};
|
||||
|
||||
@override
|
||||
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);
|
||||
Color? get disabledSecondaryActiveTrackColor => ${componentColor('$tokenGroup.disabled.active.track')};
|
||||
|
||||
@override
|
||||
Color? get activeTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.active.container')};
|
||||
Color? get activeTickMarkColor => ${componentColor('$tokenGroup.active.stop-indicator.container')};
|
||||
|
||||
@override
|
||||
Color? get inactiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.inactive.container')};
|
||||
Color? get inactiveTickMarkColor => ${componentColor('$tokenGroup.inactive.stop-indicator.container')};
|
||||
|
||||
@override
|
||||
Color? get disabledActiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.disabled.container')};
|
||||
Color? get disabledActiveTickMarkColor => ${componentColor('$tokenGroup.disabled.active.stop-indicator.container')};
|
||||
|
||||
@override
|
||||
Color? get disabledInactiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.disabled.container')};
|
||||
Color? get disabledInactiveTickMarkColor => ${componentColor('$tokenGroup.disabled.inactive.stop-indicator.container')};
|
||||
|
||||
@override
|
||||
Color? get thumbColor => ${componentColor('$tokenGroup.handle')};
|
||||
|
||||
@override
|
||||
Color? get disabledThumbColor => Color.alphaBlend(${componentColor('$tokenGroup.disabled.handle')}, _colors.surface);
|
||||
Color? get disabledThumbColor => ${componentColor('$tokenGroup.disabled.handle')};
|
||||
|
||||
@override
|
||||
Color? get overlayColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.dragged)) {
|
||||
return ${componentColor('$tokenGroup.pressed.state-layer')};
|
||||
return _colors.primary.withOpacity(0.1);
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return ${componentColor('$tokenGroup.hover.state-layer')};
|
||||
return _colors.primary.withOpacity(0.08);
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return ${componentColor('$tokenGroup.focus.state-layer')};
|
||||
return _colors.primary.withOpacity(0.1);
|
||||
}
|
||||
|
||||
return Colors.transparent;
|
||||
});
|
||||
|
||||
@override
|
||||
TextStyle? get valueIndicatorTextStyle => ${textStyle('$tokenGroup.label.label-text')}!.copyWith(
|
||||
color: ${componentColor('$tokenGroup.label.label-text')},
|
||||
TextStyle? get valueIndicatorTextStyle => ${textStyle('$tokenGroup.value-indicator.label.label-text')}!.copyWith(
|
||||
color: ${componentColor('$tokenGroup.value-indicator.label.label-text')},
|
||||
);
|
||||
|
||||
@override
|
||||
SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
|
||||
Color? get valueIndicatorColor => ${componentColor('$tokenGroup.value-indicator.container')};
|
||||
|
||||
@override
|
||||
SliderComponentShape? get valueIndicatorShape => const RoundedRectSliderValueIndicatorShape();
|
||||
|
||||
@override
|
||||
SliderComponentShape? get thumbShape => const HandleThumbShape();
|
||||
|
||||
@override
|
||||
SliderTrackShape? get trackShape => const GappedSliderTrackShape();
|
||||
|
||||
@override
|
||||
SliderComponentShape? get overlayShape => const RoundSliderOverlayShape();
|
||||
|
||||
@override
|
||||
SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape(tickMarkRadius: ${getToken("$tokenGroup.stop-indicator.size")} / 2);
|
||||
|
||||
@override
|
||||
MaterialStateProperty<Size?>? get thumbSize {
|
||||
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
return const Size(${getToken("$tokenGroup.disabled.handle.width")}, ${getToken("$tokenGroup.handle.height")});
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return const Size(${getToken("$tokenGroup.hover.handle.width")}, ${getToken("$tokenGroup.handle.height")});
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return const Size(${getToken("$tokenGroup.focus.handle.width")}, ${getToken("$tokenGroup.handle.height")});
|
||||
}
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return const Size(${getToken("$tokenGroup.pressed.handle.width")}, ${getToken("$tokenGroup.handle.height")});
|
||||
}
|
||||
return const Size(${getToken("$tokenGroup.handle.width")}, ${getToken("$tokenGroup.handle.height")});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
double? get trackGap => ${getToken("$tokenGroup.active.handle.padding")};
|
||||
}
|
||||
''';
|
||||
|
||||
}
|
||||
|
@ -197,6 +197,11 @@ class Slider extends StatefulWidget {
|
||||
this.autofocus = false,
|
||||
this.allowedInteraction,
|
||||
this.padding,
|
||||
@Deprecated(
|
||||
'Use SliderTheme to customize the Slider appearance. '
|
||||
'This feature was deprecated after v3.27.0-0.1.pre.'
|
||||
)
|
||||
this.year2023 = true,
|
||||
}) : _sliderType = _SliderType.material,
|
||||
assert(min <= max),
|
||||
assert(value >= min && value <= max,
|
||||
@ -238,6 +243,11 @@ class Slider extends StatefulWidget {
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.allowedInteraction,
|
||||
@Deprecated(
|
||||
'Use SliderTheme to customize the Slider appearance. '
|
||||
'This feature was deprecated after v3.27.0-0.1.pre.'
|
||||
)
|
||||
this.year2023 = true,
|
||||
}) : _sliderType = _SliderType.adaptive,
|
||||
padding = null,
|
||||
assert(min <= max),
|
||||
@ -432,9 +442,10 @@ class Slider extends StatefulWidget {
|
||||
/// maximum value.
|
||||
///
|
||||
/// If null, [SliderThemeData.inactiveTrackColor] of the ambient [SliderTheme]
|
||||
/// is used. If that is null and [ThemeData.useMaterial3] is true,
|
||||
/// [ColorScheme.surfaceContainerHighest] will be used, otherwise [ColorScheme.primary]
|
||||
/// with an opacity of 0.24 will be used.
|
||||
/// is used. If [Slider.year2023] is false and [ThemeData.useMaterial3] is true,
|
||||
/// then [ColorScheme.secondaryContainer] is used and if [ThemeData.useMaterial3]
|
||||
/// is false, [ColorScheme.primary] with an opacity of 0.24 is used. Otherwise,
|
||||
/// [ColorScheme.surfaceContainerHighest] is used.
|
||||
///
|
||||
/// Using a [SliderTheme] gives much more fine-grained control over the
|
||||
/// appearance of various components of the slider.
|
||||
@ -555,6 +566,18 @@ class Slider extends StatefulWidget {
|
||||
/// overlay shape, whichever is larger.
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// When true, the [Slider] will use the 2023 Material 3 esign appearance.
|
||||
///
|
||||
/// Defaults to true. If false, the [Slider] will use the latest Material 3
|
||||
/// Design appearance, which was introduced in December 2023.
|
||||
///
|
||||
/// If [ThemeData.useMaterial3] is false, then this property is ignored.
|
||||
@Deprecated(
|
||||
'Use SliderTheme to customize the Slider appearance. '
|
||||
'This feature was deprecated after v3.27.0-0.1.pre.'
|
||||
)
|
||||
final bool year2023;
|
||||
|
||||
final _SliderType _sliderType ;
|
||||
|
||||
@override
|
||||
@ -787,7 +810,12 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
Widget _buildMaterialSlider(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
SliderThemeData sliderTheme = SliderTheme.of(context);
|
||||
final SliderThemeData defaults = theme.useMaterial3 ? _SliderDefaultsM3(context) : _SliderDefaultsM2(context);
|
||||
final SliderThemeData defaults = switch (theme.useMaterial3) {
|
||||
true => widget.year2023
|
||||
? _SliderDefaultsM3Year2023(context)
|
||||
: _SliderDefaultsM3(context),
|
||||
false => _SliderDefaultsM2(context),
|
||||
};
|
||||
|
||||
// If the widget has active or inactive colors specified, then we plug them
|
||||
// in to the slider theme as best we can. If the developer wants more
|
||||
@ -796,11 +824,6 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
// the default shapes and text styles are aligned to the Material
|
||||
// Guidelines.
|
||||
|
||||
const SliderTrackShape defaultTrackShape = RoundedRectSliderTrackShape();
|
||||
const SliderTickMarkShape defaultTickMarkShape = RoundSliderTickMarkShape();
|
||||
const SliderComponentShape defaultOverlayShape = RoundSliderOverlayShape();
|
||||
const SliderComponentShape defaultThumbShape = RoundSliderThumbShape();
|
||||
final SliderComponentShape defaultValueIndicatorShape = defaults.valueIndicatorShape!;
|
||||
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
|
||||
const SliderInteraction defaultAllowedInteraction = SliderInteraction.tapAndSlide;
|
||||
|
||||
@ -815,12 +838,18 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
// (which can be defined by activeColor) if the
|
||||
// RectangularSliderValueIndicatorShape is used. In all other cases, the
|
||||
// value indicator is assumed to be the same as the active color.
|
||||
final SliderComponentShape valueIndicatorShape = sliderTheme.valueIndicatorShape ?? defaultValueIndicatorShape;
|
||||
final SliderComponentShape valueIndicatorShape = sliderTheme.valueIndicatorShape ?? defaults.valueIndicatorShape!;
|
||||
final Color valueIndicatorColor;
|
||||
if (valueIndicatorShape is RectangularSliderValueIndicatorShape) {
|
||||
valueIndicatorColor = sliderTheme.valueIndicatorColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(0.60), theme.colorScheme.surface.withOpacity(0.90));
|
||||
valueIndicatorColor = sliderTheme.valueIndicatorColor
|
||||
?? Color.alphaBlend(
|
||||
theme.colorScheme.onSurface.withOpacity(0.60),
|
||||
theme.colorScheme.surface.withOpacity(0.90),
|
||||
);
|
||||
} else {
|
||||
valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary;
|
||||
valueIndicatorColor = widget.activeColor
|
||||
?? sliderTheme.valueIndicatorColor
|
||||
?? defaults.valueIndicatorColor!;
|
||||
}
|
||||
|
||||
Color? effectiveOverlayColor() {
|
||||
@ -851,14 +880,16 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
||||
disabledThumbColor: sliderTheme.disabledThumbColor ?? defaults.disabledThumbColor,
|
||||
overlayColor: effectiveOverlayColor(),
|
||||
valueIndicatorColor: valueIndicatorColor,
|
||||
trackShape: sliderTheme.trackShape ?? defaultTrackShape,
|
||||
tickMarkShape: sliderTheme.tickMarkShape ?? defaultTickMarkShape,
|
||||
thumbShape: sliderTheme.thumbShape ?? defaultThumbShape,
|
||||
overlayShape: sliderTheme.overlayShape ?? defaultOverlayShape,
|
||||
trackShape: sliderTheme.trackShape ?? defaults.trackShape,
|
||||
tickMarkShape: sliderTheme.tickMarkShape ?? defaults.tickMarkShape,
|
||||
thumbShape: sliderTheme.thumbShape ?? defaults.thumbShape,
|
||||
overlayShape: sliderTheme.overlayShape ?? defaults.overlayShape,
|
||||
valueIndicatorShape: valueIndicatorShape,
|
||||
showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator,
|
||||
valueIndicatorTextStyle: valueIndicatorTextStyle,
|
||||
padding: widget.padding ?? sliderTheme.padding,
|
||||
thumbSize: sliderTheme.thumbSize ?? defaults.thumbSize,
|
||||
trackGap: sliderTheme.trackGap ?? defaults.trackGap,
|
||||
);
|
||||
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
|
||||
?? sliderTheme.mouseCursor?.resolve(states)
|
||||
@ -1683,8 +1714,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
: trackRect.left + visualPosition * trackRect.width;
|
||||
// Apply padding to trackRect.left and trackRect.right if the track height is
|
||||
// greater than the thumb radius to ensure the thumb is drawn within the track.
|
||||
final Size thumbSize = _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete);
|
||||
final double thumbPadding = (padding > thumbSize.width / 2 ? padding / 2 : 0);
|
||||
final Size thumbPreferredSize = _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete);
|
||||
final double thumbPadding = (padding > thumbPreferredSize.width / 2 ? padding / 2 : 0);
|
||||
final Offset thumbCenter = Offset(
|
||||
clampDouble(
|
||||
thumbPosition,
|
||||
@ -1697,13 +1728,32 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize(isInteractive, false);
|
||||
overlayRect = Rect.fromCircle(center: thumbCenter, radius: overlaySize.width / 2.0);
|
||||
}
|
||||
final Offset? secondaryOffset = (secondaryVisualPosition != null) ? Offset(trackRect.left + secondaryVisualPosition * trackRect.width, trackRect.center.dy) : null;
|
||||
final Offset? secondaryOffset = (secondaryVisualPosition != null)
|
||||
? Offset(trackRect.left + secondaryVisualPosition * trackRect.width, trackRect.center.dy)
|
||||
: null;
|
||||
|
||||
// If [Slider.year2023] is false, the thumb uses handle thumb shape and gapped track shape.
|
||||
// The handle width and track gap are adjusted when the thumb is pressed.
|
||||
double? thumbWidth = _sliderTheme.thumbSize?.resolve(<MaterialState>{})?.width;
|
||||
final double? thumbHeight = _sliderTheme.thumbSize?.resolve(<MaterialState>{})?.height;
|
||||
double? trackGap = _sliderTheme.trackGap;
|
||||
final double? pressedThumbWidth = _sliderTheme.thumbSize?.resolve(<MaterialState>{ MaterialState.pressed })?.width;
|
||||
final double delta;
|
||||
if (_active && thumbWidth != null && pressedThumbWidth != null && trackGap != null) {
|
||||
delta = thumbWidth - pressedThumbWidth;
|
||||
if (thumbWidth > 0.0) {
|
||||
thumbWidth = pressedThumbWidth;
|
||||
}
|
||||
if (trackGap > 0.0) {
|
||||
trackGap = trackGap - delta / 2;
|
||||
}
|
||||
}
|
||||
|
||||
_sliderTheme.trackShape!.paint(
|
||||
context,
|
||||
offset,
|
||||
parentBox: this,
|
||||
sliderTheme: _sliderTheme,
|
||||
sliderTheme: _sliderTheme.copyWith(trackGap: trackGap),
|
||||
enableAnimation: _enableAnimation,
|
||||
textDirection: _textDirection,
|
||||
thumbCenter: thumbCenter,
|
||||
@ -1789,7 +1839,9 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
isDiscrete: isDiscrete,
|
||||
labelPainter: _labelPainter,
|
||||
parentBox: this,
|
||||
sliderTheme: _sliderTheme,
|
||||
sliderTheme: thumbWidth != null && thumbHeight != null
|
||||
? _sliderTheme.copyWith(thumbSize: MaterialStatePropertyAll<Size?>(Size(thumbWidth, thumbHeight)))
|
||||
: _sliderTheme,
|
||||
textDirection: _textDirection,
|
||||
value: _value,
|
||||
textScaleFactor: textScaleFactor,
|
||||
@ -1960,12 +2012,11 @@ class _RenderValueIndicator extends RenderBox with RelayoutWhenSystemFontsChange
|
||||
}
|
||||
|
||||
class _SliderDefaultsM2 extends SliderThemeData {
|
||||
_SliderDefaultsM2(this.context)
|
||||
: _colors = Theme.of(context).colorScheme,
|
||||
super(trackHeight: 4.0);
|
||||
_SliderDefaultsM2(this.context) : super(trackHeight: 4.0);
|
||||
|
||||
final BuildContext context;
|
||||
final ColorScheme _colors;
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
late final SliderThemeData sliderTheme = SliderTheme.of(context);
|
||||
|
||||
@override
|
||||
Color? get activeTrackColor => _colors.primary;
|
||||
@ -2011,20 +2062,32 @@ class _SliderDefaultsM2 extends SliderThemeData {
|
||||
color: _colors.onPrimary,
|
||||
);
|
||||
|
||||
@override
|
||||
Color? get valueIndicatorColor {
|
||||
if (sliderTheme.valueIndicatorShape is RoundedRectSliderValueIndicatorShape) {
|
||||
return _colors.inverseSurface;
|
||||
}
|
||||
return _colors.primary;
|
||||
}
|
||||
|
||||
@override
|
||||
SliderComponentShape? get valueIndicatorShape => const RectangularSliderValueIndicatorShape();
|
||||
|
||||
@override
|
||||
SliderComponentShape? get thumbShape => const RoundSliderThumbShape();
|
||||
|
||||
@override
|
||||
SliderTrackShape? get trackShape => const RoundedRectSliderTrackShape();
|
||||
|
||||
@override
|
||||
SliderComponentShape? get overlayShape => const RoundSliderOverlayShape();
|
||||
|
||||
@override
|
||||
SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape();
|
||||
}
|
||||
|
||||
// TODO(quncheng): Update M3 defaults to match the latest specs.
|
||||
// BEGIN GENERATED TOKEN PROPERTIES - Slider
|
||||
|
||||
// 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 _SliderDefaultsM3 extends SliderThemeData {
|
||||
_SliderDefaultsM3(this.context)
|
||||
class _SliderDefaultsM3Year2023 extends SliderThemeData {
|
||||
_SliderDefaultsM3Year2023(this.context)
|
||||
: super(trackHeight: 4.0);
|
||||
|
||||
final BuildContext context;
|
||||
@ -2086,8 +2149,134 @@ class _SliderDefaultsM3 extends SliderThemeData {
|
||||
color: _colors.onPrimary,
|
||||
);
|
||||
|
||||
@override
|
||||
Color? get valueIndicatorColor => _colors.primary;
|
||||
|
||||
@override
|
||||
SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
|
||||
|
||||
@override
|
||||
SliderComponentShape? get thumbShape => const RoundSliderThumbShape();
|
||||
|
||||
@override
|
||||
SliderTrackShape? get trackShape => const RoundedRectSliderTrackShape();
|
||||
|
||||
@override
|
||||
SliderComponentShape? get overlayShape => const RoundSliderOverlayShape();
|
||||
|
||||
@override
|
||||
SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape();
|
||||
}
|
||||
|
||||
// BEGIN GENERATED TOKEN PROPERTIES - Slider
|
||||
|
||||
// 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 _SliderDefaultsM3 extends SliderThemeData {
|
||||
_SliderDefaultsM3(this.context)
|
||||
: super(trackHeight: 16.0);
|
||||
|
||||
final BuildContext context;
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
|
||||
@override
|
||||
Color? get activeTrackColor => _colors.primary;
|
||||
|
||||
@override
|
||||
Color? get inactiveTrackColor => _colors.secondaryContainer;
|
||||
|
||||
@override
|
||||
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
|
||||
|
||||
@override
|
||||
Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38);
|
||||
|
||||
@override
|
||||
Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);
|
||||
|
||||
@override
|
||||
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.38);
|
||||
|
||||
@override
|
||||
Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(1.0);
|
||||
|
||||
@override
|
||||
Color? get inactiveTickMarkColor => _colors.onSecondaryContainer.withOpacity(1.0);
|
||||
|
||||
@override
|
||||
Color? get disabledActiveTickMarkColor => _colors.onInverseSurface;
|
||||
|
||||
@override
|
||||
Color? get disabledInactiveTickMarkColor => _colors.onSurface;
|
||||
|
||||
@override
|
||||
Color? get thumbColor => _colors.primary;
|
||||
|
||||
@override
|
||||
Color? get disabledThumbColor => _colors.onSurface.withOpacity(0.38);
|
||||
|
||||
@override
|
||||
Color? get overlayColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.dragged)) {
|
||||
return _colors.primary.withOpacity(0.1);
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return _colors.primary.withOpacity(0.08);
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return _colors.primary.withOpacity(0.1);
|
||||
}
|
||||
|
||||
return Colors.transparent;
|
||||
});
|
||||
|
||||
@override
|
||||
TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.labelLarge!.copyWith(
|
||||
color: _colors.onInverseSurface,
|
||||
);
|
||||
|
||||
@override
|
||||
Color? get valueIndicatorColor => _colors.inverseSurface;
|
||||
|
||||
@override
|
||||
SliderComponentShape? get valueIndicatorShape => const RoundedRectSliderValueIndicatorShape();
|
||||
|
||||
@override
|
||||
SliderComponentShape? get thumbShape => const HandleThumbShape();
|
||||
|
||||
@override
|
||||
SliderTrackShape? get trackShape => const GappedSliderTrackShape();
|
||||
|
||||
@override
|
||||
SliderComponentShape? get overlayShape => const RoundSliderOverlayShape();
|
||||
|
||||
@override
|
||||
SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape(tickMarkRadius: 4.0 / 2);
|
||||
|
||||
@override
|
||||
MaterialStateProperty<Size?>? get thumbSize {
|
||||
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
return const Size(4.0, 44.0);
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return const Size(4.0, 44.0);
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return const Size(2.0, 44.0);
|
||||
}
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return const Size(2.0, 44.0);
|
||||
}
|
||||
return const Size(4.0, 44.0);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
double? get trackGap => 6.0;
|
||||
}
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Slider
|
||||
|
@ -297,6 +297,8 @@ class SliderThemeData with Diagnosticable {
|
||||
this.mouseCursor,
|
||||
this.allowedInteraction,
|
||||
this.padding,
|
||||
this.thumbSize,
|
||||
this.trackGap,
|
||||
});
|
||||
|
||||
/// Generates a SliderThemeData from three main colors.
|
||||
@ -597,6 +599,30 @@ class SliderThemeData with Diagnosticable {
|
||||
/// overlay shape, whichever is larger.
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// The size of the [HandleThumbShape] thumb.
|
||||
///
|
||||
/// If [SliderThemeData.thumbShape] is [HandleThumbShape], this property is used to
|
||||
/// set the size of the thumb. Otherwise, the default thumb size is 4 pixels for the
|
||||
/// width and 44 pixels for the height.
|
||||
final MaterialStateProperty<Size?>? thumbSize;
|
||||
|
||||
/// The size of the gap between the active and inactive tracks of the [GappedSliderTrackShape].
|
||||
///
|
||||
/// If [SliderThemeData.trackShape] is [GappedSliderTrackShape], this property
|
||||
/// is used to set the gap between the active and inactive tracks. Otherwise,
|
||||
/// the default gap size is 6.0 pixels.
|
||||
///
|
||||
/// The Slider defaults to [GappedSliderTrackShape] when the track shape is
|
||||
/// not specified, and the [trackGap] can be used to adjust the gap size.
|
||||
///
|
||||
/// If [Slider.year2023] is false or [ThemeData.useMaterial3] is false, then
|
||||
/// the Slider track shape defaults to [RoundedRectSliderTrackShape] and the
|
||||
/// [trackGap] is ignored. In this case, set the track shape to
|
||||
/// [GappedSliderTrackShape] to use the [trackGap].
|
||||
///
|
||||
/// Defaults to 6.0 pixels of gap between the active and inactive tracks.
|
||||
final double? trackGap;
|
||||
|
||||
/// Creates a copy of this object but with the given fields replaced with the
|
||||
/// new values.
|
||||
SliderThemeData copyWith({
|
||||
@ -633,6 +659,8 @@ class SliderThemeData with Diagnosticable {
|
||||
MaterialStateProperty<MouseCursor?>? mouseCursor,
|
||||
SliderInteraction? allowedInteraction,
|
||||
EdgeInsetsGeometry? padding,
|
||||
MaterialStateProperty<Size?>? thumbSize,
|
||||
double? trackGap,
|
||||
}) {
|
||||
return SliderThemeData(
|
||||
trackHeight: trackHeight ?? this.trackHeight,
|
||||
@ -668,6 +696,8 @@ class SliderThemeData with Diagnosticable {
|
||||
mouseCursor: mouseCursor ?? this.mouseCursor,
|
||||
allowedInteraction: allowedInteraction ?? this.allowedInteraction,
|
||||
padding: padding ?? this.padding,
|
||||
thumbSize: thumbSize ?? this.thumbSize,
|
||||
trackGap: trackGap ?? this.trackGap,
|
||||
);
|
||||
}
|
||||
|
||||
@ -712,6 +742,8 @@ class SliderThemeData with Diagnosticable {
|
||||
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
|
||||
allowedInteraction: t < 0.5 ? a.allowedInteraction : b.allowedInteraction,
|
||||
padding: EdgeInsetsGeometry.lerp(a.padding, b.padding, t),
|
||||
thumbSize: MaterialStateProperty.lerp<Size?>(a.thumbSize, b.thumbSize, t, Size.lerp),
|
||||
trackGap: lerpDouble(a.trackGap, b.trackGap, t),
|
||||
);
|
||||
}
|
||||
|
||||
@ -750,6 +782,8 @@ class SliderThemeData with Diagnosticable {
|
||||
mouseCursor,
|
||||
allowedInteraction,
|
||||
padding,
|
||||
thumbSize,
|
||||
trackGap,
|
||||
),
|
||||
);
|
||||
|
||||
@ -794,7 +828,9 @@ class SliderThemeData with Diagnosticable {
|
||||
&& other.thumbSelector == thumbSelector
|
||||
&& other.mouseCursor == mouseCursor
|
||||
&& other.allowedInteraction == allowedInteraction
|
||||
&& other.padding == padding;
|
||||
&& other.padding == padding
|
||||
&& other.thumbSize == thumbSize
|
||||
&& other.trackGap == trackGap;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -834,6 +870,8 @@ class SliderThemeData with Diagnosticable {
|
||||
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
|
||||
properties.add(EnumProperty<SliderInteraction>('allowedInteraction', allowedInteraction, defaultValue: defaultData.allowedInteraction));
|
||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: defaultData.padding));
|
||||
properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('thumbSize', thumbSize, defaultValue: defaultData.thumbSize));
|
||||
properties.add(DoubleProperty('trackGap', trackGap, defaultValue: defaultData.trackGap));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3559,3 +3597,457 @@ class _DropSliderValueIndicatorPathPainter {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/// The bar shape of a [Slider]'s thumb.
|
||||
///
|
||||
/// When the slider is enabled, the [ColorScheme.primary] color is used for the
|
||||
/// thumb. When the slider is disabled, the [ColorScheme.onSurface] color with an
|
||||
/// opacity of 0.38 is used for the thumb.
|
||||
///
|
||||
/// The thumb bar shape width is reduced when the thumb is pressed.
|
||||
///
|
||||
/// If [SliderThemeData.thumbSize] is null, then the thumb size is 4 pixels for the width
|
||||
/// and 44 pixels for the height.
|
||||
///
|
||||
/// This is the default thumb shape for [Slider]. If [ThemeData.useMaterial3] is false,
|
||||
/// then the default thumb shape is [RoundSliderThumbShape].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Slider], which includes an overlay defined by this shape.
|
||||
/// * [SliderTheme], which can be used to configure the overlay shape of all
|
||||
/// sliders in a widget subtree.
|
||||
class HandleThumbShape extends SliderComponentShape {
|
||||
/// Create a slider thumb that draws a bar.
|
||||
const HandleThumbShape();
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return const Size(4.0, 44.0);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
required Animation<double> activationAnimation,
|
||||
required Animation<double> enableAnimation,
|
||||
required bool isDiscrete,
|
||||
required TextPainter labelPainter,
|
||||
required RenderBox parentBox,
|
||||
required SliderThemeData sliderTheme,
|
||||
required TextDirection textDirection,
|
||||
required double value,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
}) {
|
||||
assert(sliderTheme.disabledThumbColor != null);
|
||||
assert(sliderTheme.thumbColor != null);
|
||||
assert(sliderTheme.thumbSize != null);
|
||||
|
||||
final ColorTween colorTween = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
final Color color = colorTween.evaluate(enableAnimation)!;
|
||||
|
||||
final Canvas canvas = context.canvas;
|
||||
final Size thumbSize = sliderTheme.thumbSize!.resolve(<MaterialState>{})!; // This is resolved in the paint method.
|
||||
final RRect rrect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: center,
|
||||
width: thumbSize.width,
|
||||
height: thumbSize.height,
|
||||
),
|
||||
Radius.circular(thumbSize.shortestSide / 2),
|
||||
);
|
||||
canvas.drawRRect(rrect, Paint()..color = color);
|
||||
}
|
||||
}
|
||||
|
||||
/// The gapped shape of a [Slider]'s track.
|
||||
///
|
||||
/// The [GappedSliderTrackShape] consists of active and inactive
|
||||
/// tracks. The active track uses the [SliderThemeData.activeTrackColor] and the
|
||||
/// inactive track uses the [SliderThemeData.inactiveTrackColor].
|
||||
///
|
||||
/// The track shape uses circular corner radius for the edge corners and a corner radius
|
||||
/// of 2 pixels for the inside corners.
|
||||
///
|
||||
/// Between the active and inactive tracks there is a gap of size [SliderThemeData.trackGap].
|
||||
/// If the [SliderThemeData.thumbShape] is [HandleThumbShape] and the thumb is pressed, the thumb's
|
||||
/// width is reduced; as a result, the track gap size in [GappedSliderTrackShape]
|
||||
/// is also reduced.
|
||||
///
|
||||
/// If [SliderThemeData.trackGap] is null, then the track gap size defaults to 6 pixels.
|
||||
///
|
||||
/// This is the default track shape for [Slider]. If [ThemeData.useMaterial3] is false,
|
||||
/// then the default track shape is [RoundedRectSliderTrackShape].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Slider], which includes an overlay defined by this shape.
|
||||
/// * [SliderTheme], which can be used to configure the overlay shape of all
|
||||
/// sliders in a widget subtree.
|
||||
class GappedSliderTrackShape extends SliderTrackShape with BaseSliderTrackShape {
|
||||
/// Create a slider track that draws two rectangles with rounded outer edges.
|
||||
const GappedSliderTrackShape();
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset offset, {
|
||||
required RenderBox parentBox,
|
||||
required SliderThemeData sliderTheme,
|
||||
required Animation<double> enableAnimation,
|
||||
required TextDirection textDirection,
|
||||
required Offset thumbCenter,
|
||||
Offset? secondaryOffset,
|
||||
bool isDiscrete = false,
|
||||
bool isEnabled = false,
|
||||
double additionalActiveTrackHeight = 2,
|
||||
}) {
|
||||
assert(sliderTheme.disabledActiveTrackColor != null);
|
||||
assert(sliderTheme.disabledInactiveTrackColor != null);
|
||||
assert(sliderTheme.activeTrackColor != null);
|
||||
assert(sliderTheme.inactiveTrackColor != null);
|
||||
assert(sliderTheme.thumbShape != null);
|
||||
assert(sliderTheme.trackGap != null);
|
||||
assert(!sliderTheme.trackGap!.isNegative);
|
||||
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
|
||||
// then it makes no difference whether the track is painted or not,
|
||||
// therefore the painting can be a no-op.
|
||||
if (sliderTheme.trackHeight == null || sliderTheme.trackHeight! <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign the track segment paints, which are left: active, right: inactive,
|
||||
// but reversed for right to left text.
|
||||
final ColorTween activeTrackColorTween = ColorTween(
|
||||
begin: sliderTheme.disabledActiveTrackColor,
|
||||
end: sliderTheme.activeTrackColor,
|
||||
);
|
||||
final ColorTween inactiveTrackColorTween = ColorTween(
|
||||
begin: sliderTheme.disabledInactiveTrackColor,
|
||||
end: sliderTheme.inactiveTrackColor,
|
||||
);
|
||||
final Paint activePaint = Paint()
|
||||
..color = activeTrackColorTween.evaluate(enableAnimation)!;
|
||||
final Paint inactivePaint = Paint()
|
||||
..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
|
||||
final Paint leftTrackPaint;
|
||||
final Paint rightTrackPaint;
|
||||
switch (textDirection) {
|
||||
case TextDirection.ltr:
|
||||
leftTrackPaint = activePaint;
|
||||
rightTrackPaint = inactivePaint;
|
||||
case TextDirection.rtl:
|
||||
leftTrackPaint = inactivePaint;
|
||||
rightTrackPaint = activePaint;
|
||||
}
|
||||
|
||||
// Gap, starting from the middle of the thumb.
|
||||
final double trackGap = sliderTheme.trackGap!;
|
||||
|
||||
final Rect trackRect = getPreferredRect(
|
||||
parentBox: parentBox,
|
||||
offset: offset,
|
||||
sliderTheme: sliderTheme,
|
||||
isEnabled: isEnabled,
|
||||
isDiscrete: isDiscrete,
|
||||
);
|
||||
|
||||
final Radius trackCornerRadius = Radius.circular(trackRect.shortestSide / 2);
|
||||
const Radius trackInsideCornerRadius = Radius.circular(2.0);
|
||||
|
||||
final RRect trackRRect = RRect.fromRectAndCorners(
|
||||
trackRect,
|
||||
topLeft: trackCornerRadius,
|
||||
bottomLeft: trackCornerRadius,
|
||||
topRight: trackCornerRadius,
|
||||
bottomRight: trackCornerRadius,
|
||||
);
|
||||
|
||||
final RRect leftRRect = RRect.fromLTRBAndCorners(
|
||||
trackRect.left,
|
||||
trackRect.top,
|
||||
math.max(trackRect.left, thumbCenter.dx - trackGap),
|
||||
trackRect.bottom,
|
||||
topLeft: trackCornerRadius,
|
||||
bottomLeft: trackCornerRadius,
|
||||
topRight: trackInsideCornerRadius,
|
||||
bottomRight: trackInsideCornerRadius,
|
||||
);
|
||||
|
||||
final RRect rightRRect = RRect.fromLTRBAndCorners(
|
||||
thumbCenter.dx + trackGap,
|
||||
trackRect.top,
|
||||
trackRect.right,
|
||||
trackRect.bottom,
|
||||
topRight: trackCornerRadius,
|
||||
bottomRight: trackCornerRadius,
|
||||
topLeft: trackInsideCornerRadius,
|
||||
bottomLeft: trackInsideCornerRadius,
|
||||
);
|
||||
|
||||
context.canvas..save()..clipRRect(trackRRect);
|
||||
final bool drawLeftTrack = thumbCenter.dx > (leftRRect.left + (sliderTheme.trackHeight! / 2));
|
||||
final bool drawRightTrack = thumbCenter.dx < (rightRRect.right - (sliderTheme.trackHeight! / 2));
|
||||
if (drawLeftTrack) {
|
||||
context.canvas.drawRRect(leftRRect, leftTrackPaint);
|
||||
}
|
||||
if (drawRightTrack) {
|
||||
context.canvas.drawRRect(rightRRect, rightTrackPaint);
|
||||
}
|
||||
|
||||
final bool isLTR = textDirection == TextDirection.ltr;
|
||||
final bool showSecondaryTrack = (secondaryOffset != null) && switch (isLTR) {
|
||||
true => secondaryOffset.dx > thumbCenter.dx + trackGap,
|
||||
false => secondaryOffset.dx < thumbCenter.dx - trackGap,
|
||||
};
|
||||
|
||||
if (showSecondaryTrack) {
|
||||
final ColorTween secondaryTrackColorTween = ColorTween(begin: sliderTheme.disabledSecondaryActiveTrackColor, end: sliderTheme.secondaryActiveTrackColor);
|
||||
final Paint secondaryTrackPaint = Paint()..color = secondaryTrackColorTween.evaluate(enableAnimation)!;
|
||||
if (isLTR) {
|
||||
context.canvas.drawRRect(
|
||||
RRect.fromLTRBAndCorners(
|
||||
thumbCenter.dx + trackGap,
|
||||
trackRect.top,
|
||||
secondaryOffset.dx,
|
||||
trackRect.bottom,
|
||||
topLeft: trackInsideCornerRadius,
|
||||
bottomLeft: trackInsideCornerRadius,
|
||||
topRight: trackCornerRadius,
|
||||
bottomRight: trackCornerRadius,
|
||||
),
|
||||
secondaryTrackPaint,
|
||||
);
|
||||
} else {
|
||||
context.canvas.drawRRect(
|
||||
RRect.fromLTRBAndCorners(
|
||||
secondaryOffset.dx - trackGap,
|
||||
trackRect.top,
|
||||
thumbCenter.dx,
|
||||
trackRect.bottom,
|
||||
topLeft: trackInsideCornerRadius,
|
||||
bottomLeft: trackInsideCornerRadius,
|
||||
topRight: trackCornerRadius,
|
||||
bottomRight: trackCornerRadius,
|
||||
),
|
||||
secondaryTrackPaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
context.canvas.restore();
|
||||
|
||||
const double stopIndicatorRadius = 2.0;
|
||||
final double stopIndicatorTrailingSpace = sliderTheme.trackHeight! / 2;
|
||||
final Offset stopIndicatorOffset = Offset(
|
||||
(textDirection == TextDirection.ltr)
|
||||
? trackRect.centerRight.dx - stopIndicatorTrailingSpace
|
||||
: trackRect.centerLeft.dx + stopIndicatorTrailingSpace,
|
||||
trackRect.center.dy,
|
||||
);
|
||||
|
||||
final bool showStopIndicator = (textDirection == TextDirection.ltr)
|
||||
? thumbCenter.dx < stopIndicatorOffset.dx
|
||||
: thumbCenter.dx > stopIndicatorOffset.dx;
|
||||
if (showStopIndicator && !isDiscrete) {
|
||||
final Rect stopIndicatorRect = Rect.fromCircle(center: stopIndicatorOffset, radius: stopIndicatorRadius);
|
||||
context.canvas.drawCircle(stopIndicatorRect.center, stopIndicatorRadius, activePaint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isRounded => true;
|
||||
}
|
||||
|
||||
/// The rounded rectangle shape of a [Slider]'s value indicator.
|
||||
///
|
||||
/// If the [SliderThemeData.valueIndicatorColor] is null, then the shape uses the [ColorScheme.inverseSurface]
|
||||
/// color to draw the value indicator.
|
||||
///
|
||||
/// If the [SliderThemeData.valueIndicatorTextStyle] is null, then the indicator label text style
|
||||
/// defaults to [TextTheme.labelMedium] with the color set to [ColorScheme.onInverseSurface]. If the
|
||||
/// [ThemeData.useMaterial3] is set to false, then the indicator label text style defaults to
|
||||
/// [TextTheme.bodyLarge] with the color set to [ColorScheme.onInverseSurface].
|
||||
///
|
||||
/// If the [SliderThemeData.valueIndicatorStrokeColor] is provided, then the value indicator is drawn with a
|
||||
/// stroke border with the color provided.
|
||||
///
|
||||
/// This is the default value indicator shape for [Slider]. If [ThemeData.useMaterial3] is false,
|
||||
/// then the default value indicator shape is [RectangularSliderValueIndicatorShape].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Slider], which includes a value indicator defined by this shape.
|
||||
/// * [SliderTheme], which can be used to configure the slider value indicator
|
||||
/// of all sliders in a widget subtree.
|
||||
class RoundedRectSliderValueIndicatorShape extends SliderComponentShape {
|
||||
/// Create a slider value indicator that resembles a rounded rectangle.
|
||||
const RoundedRectSliderValueIndicatorShape();
|
||||
|
||||
static const _RoundedRectSliderValueIndicatorPathPainter _pathPainter = _RoundedRectSliderValueIndicatorPathPainter();
|
||||
|
||||
@override
|
||||
Size getPreferredSize(
|
||||
bool isEnabled,
|
||||
bool isDiscrete, {
|
||||
TextPainter? labelPainter,
|
||||
double? textScaleFactor,
|
||||
}) {
|
||||
assert(labelPainter != null);
|
||||
assert(textScaleFactor != null && textScaleFactor >= 0);
|
||||
return _pathPainter.getPreferredSize(labelPainter!, textScaleFactor!);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
required Animation<double> activationAnimation,
|
||||
required Animation<double> enableAnimation,
|
||||
required bool isDiscrete,
|
||||
required TextPainter labelPainter,
|
||||
required RenderBox parentBox,
|
||||
required SliderThemeData sliderTheme,
|
||||
required TextDirection textDirection,
|
||||
required double value,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final double scale = activationAnimation.value;
|
||||
_pathPainter.paint(
|
||||
parentBox: parentBox,
|
||||
canvas: canvas,
|
||||
center: center,
|
||||
scale: scale,
|
||||
labelPainter: labelPainter,
|
||||
textScaleFactor: textScaleFactor,
|
||||
sizeWithOverflow: sizeWithOverflow,
|
||||
backgroundPaintColor: sliderTheme.valueIndicatorColor!,
|
||||
strokePaintColor: sliderTheme.valueIndicatorStrokeColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoundedRectSliderValueIndicatorPathPainter {
|
||||
const _RoundedRectSliderValueIndicatorPathPainter();
|
||||
|
||||
static const double _labelPadding = 10.0;
|
||||
static const double _preferredHeight = 32.0;
|
||||
static const double _minLabelWidth = 16.0;
|
||||
static const double _rectYOffset = 10.0;
|
||||
static const double _bottomTipYOffset = 16.0;
|
||||
static const double _preferredHalfHeight = _preferredHeight / 2;
|
||||
|
||||
Size getPreferredSize(
|
||||
TextPainter labelPainter,
|
||||
double textScaleFactor,
|
||||
) {
|
||||
final double width = math.max(_minLabelWidth, labelPainter.width) + (_labelPadding * 2) * textScaleFactor;
|
||||
return Size(width, _preferredHeight * textScaleFactor);
|
||||
}
|
||||
|
||||
double getHorizontalShift({
|
||||
required RenderBox parentBox,
|
||||
required Offset center,
|
||||
required TextPainter labelPainter,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
required double scale,
|
||||
}) {
|
||||
assert(!sizeWithOverflow.isEmpty);
|
||||
|
||||
const double edgePadding = 8.0;
|
||||
final double rectangleWidth = _upperRectangleWidth(labelPainter, scale);
|
||||
/// Value indicator draws on the Overlay and by using the global Offset
|
||||
/// we are making sure we use the bounds of the Overlay instead of the Slider.
|
||||
final Offset globalCenter = parentBox.localToGlobal(center);
|
||||
|
||||
// The rectangle must be shifted towards the center so that it minimizes the
|
||||
// chance of it rendering outside the bounds of the render box. If the shift
|
||||
// is negative, then the lobe is shifted from right to left, and if it is
|
||||
// positive, then the lobe is shifted from left to right.
|
||||
final double overflowLeft = math.max(0, rectangleWidth / 2 - globalCenter.dx + edgePadding);
|
||||
final double overflowRight = math.max(0, rectangleWidth / 2 - (sizeWithOverflow.width - globalCenter.dx - edgePadding));
|
||||
|
||||
if (rectangleWidth < sizeWithOverflow.width) {
|
||||
return overflowLeft - overflowRight;
|
||||
} else if (overflowLeft - overflowRight > 0) {
|
||||
return overflowLeft - (edgePadding * textScaleFactor);
|
||||
} else {
|
||||
return -overflowRight + (edgePadding * textScaleFactor);
|
||||
}
|
||||
}
|
||||
|
||||
double _upperRectangleWidth(TextPainter labelPainter, double scale) {
|
||||
final double unscaledWidth = math.max(_minLabelWidth, labelPainter.width) + (_labelPadding * 2);
|
||||
return unscaledWidth * scale;
|
||||
}
|
||||
|
||||
void paint({
|
||||
required RenderBox parentBox,
|
||||
required Canvas canvas,
|
||||
required Offset center,
|
||||
required double scale,
|
||||
required TextPainter labelPainter,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
required Color backgroundPaintColor,
|
||||
Color? strokePaintColor,
|
||||
}) {
|
||||
if (scale == 0.0) {
|
||||
// Zero scale essentially means "do not draw anything", so it's safe to just return.
|
||||
return;
|
||||
}
|
||||
assert(!sizeWithOverflow.isEmpty);
|
||||
|
||||
final double rectangleWidth = _upperRectangleWidth(labelPainter, scale);
|
||||
final double horizontalShift = getHorizontalShift(
|
||||
parentBox: parentBox,
|
||||
center: center,
|
||||
labelPainter: labelPainter,
|
||||
textScaleFactor: textScaleFactor,
|
||||
sizeWithOverflow: sizeWithOverflow,
|
||||
scale: scale,
|
||||
);
|
||||
|
||||
final Rect upperRect = Rect.fromLTWH(
|
||||
-rectangleWidth / 2 + horizontalShift,
|
||||
-_rectYOffset - _preferredHeight,
|
||||
rectangleWidth,
|
||||
_preferredHeight,
|
||||
);
|
||||
|
||||
final Paint fillPaint = Paint()..color = backgroundPaintColor;
|
||||
|
||||
canvas.save();
|
||||
// Prepare the canvas for the base of the tooltip, which is relative to the
|
||||
// center of the thumb.
|
||||
canvas.translate(center.dx, center.dy - _bottomTipYOffset);
|
||||
canvas.scale(scale, scale);
|
||||
|
||||
final RRect rrect = RRect.fromRectAndRadius(upperRect, Radius.circular(upperRect.height / 2));
|
||||
if (strokePaintColor != null) {
|
||||
final Paint strokePaint = Paint()
|
||||
..color = strokePaintColor
|
||||
..strokeWidth = 1.0
|
||||
..style = PaintingStyle.stroke;
|
||||
canvas.drawRRect(rrect, strokePaint);
|
||||
}
|
||||
|
||||
canvas.drawRRect(rrect, fillPaint);
|
||||
|
||||
// The label text is centered within the value indicator.
|
||||
final double bottomTipToUpperRectTranslateY = -_preferredHalfHeight / 2 - upperRect.height;
|
||||
canvas.translate(0, bottomTipToUpperRectTranslateY);
|
||||
final Offset boxCenter = Offset(horizontalShift, upperRect.height / 2.3);
|
||||
final Offset halfLabelPainterOffset = Offset(labelPainter.width / 2, labelPainter.height / 2);
|
||||
final Offset labelOffset = boxCenter - halfLabelPainterOffset;
|
||||
labelPainter.paint(canvas, labelOffset);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
@ -4687,4 +4687,210 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Default Slider when year2023 is false', (WidgetTester tester) async {
|
||||
debugDisableShadows = false;
|
||||
try {
|
||||
final ThemeData theme = ThemeData();
|
||||
final ColorScheme colorScheme = theme.colorScheme;
|
||||
final Color activeTrackColor = colorScheme.primary;
|
||||
final Color inactiveTrackColor = colorScheme.secondaryContainer;
|
||||
final Color secondaryActiveTrackColor = colorScheme.primary.withOpacity(0.54);
|
||||
final Color disabledActiveTrackColor = colorScheme.onSurface.withOpacity(0.38);
|
||||
final Color disabledInactiveTrackColor = colorScheme.onSurface.withOpacity(0.12);
|
||||
final Color disabledSecondaryActiveTrackColor = colorScheme.onSurface.withOpacity(0.38);
|
||||
final Color activeTickMarkColor = colorScheme.onPrimary;
|
||||
final Color inactiveTickMarkColor = colorScheme.onSecondaryContainer;
|
||||
final Color disabledActiveTickMarkColor = colorScheme.onInverseSurface;
|
||||
final Color disabledInactiveTickMarkColor = colorScheme.onSurface;
|
||||
final Color thumbColor = colorScheme.primary;
|
||||
final Color disabledThumbColor = colorScheme.onSurface.withOpacity(0.38);
|
||||
final Color valueIndicatorColor = colorScheme.inverseSurface;
|
||||
double value = 0.45;
|
||||
Widget buildApp({
|
||||
int? divisions,
|
||||
bool enabled = true,
|
||||
}) {
|
||||
final ValueChanged<double>? onChanged = !enabled
|
||||
? null
|
||||
: (double d) {
|
||||
value = d;
|
||||
};
|
||||
return MaterialApp(
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Material(
|
||||
child: Center(
|
||||
child: Theme(
|
||||
data: theme,
|
||||
child: Slider(
|
||||
year2023: false,
|
||||
value: value,
|
||||
secondaryTrackValue: 0.75,
|
||||
label: '$value',
|
||||
divisions: divisions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp());
|
||||
|
||||
final MaterialInkController material = Material.of(tester.element(find.byType(Slider)));
|
||||
|
||||
// Test default track shape.
|
||||
const Radius trackOuterCornerRadius = Radius.circular(8.0);
|
||||
const Radius trackInnerCornderRadius = Radius.circular(2.0);
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
// Active track.
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBAndCorners(
|
||||
24.0, 292.0, 356.4, 308.0,
|
||||
topLeft: trackOuterCornerRadius,
|
||||
topRight: trackInnerCornderRadius,
|
||||
bottomRight: trackInnerCornderRadius,
|
||||
bottomLeft: trackOuterCornerRadius,
|
||||
),
|
||||
color: activeTrackColor,
|
||||
)
|
||||
// Inctive track.
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBAndCorners(
|
||||
368.4, 292.0, 776.0, 308.0,
|
||||
topLeft: trackInnerCornderRadius,
|
||||
topRight: trackOuterCornerRadius,
|
||||
bottomRight: trackOuterCornerRadius,
|
||||
bottomLeft: trackInnerCornderRadius,
|
||||
),
|
||||
color: inactiveTrackColor,
|
||||
)
|
||||
);
|
||||
|
||||
// Test default colors for enabled slider.
|
||||
expect(material, paints..circle()..rrect(color: thumbColor));
|
||||
expect(material, isNot(paints..circle()..circle(color: disabledThumbColor)));
|
||||
expect(material, isNot(paints..rrect(color: disabledActiveTrackColor)));
|
||||
expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor)));
|
||||
expect(material, isNot(paints..rrect(color: disabledSecondaryActiveTrackColor)));
|
||||
|
||||
// Test defaults colors for discrete slider.
|
||||
await tester.pumpWidget(buildApp(divisions: 3));
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
..rrect(color: activeTrackColor)
|
||||
..rrect(color: inactiveTrackColor)
|
||||
..rrect(color: secondaryActiveTrackColor)
|
||||
..circle(color: activeTickMarkColor)
|
||||
..circle(color: activeTickMarkColor)
|
||||
..circle(color: inactiveTickMarkColor)
|
||||
..circle(color: inactiveTickMarkColor)
|
||||
);
|
||||
expect(material, isNot(paints..circle(color: disabledThumbColor)));
|
||||
expect(material, isNot(paints..rrect(color: disabledActiveTrackColor)));
|
||||
expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor)));
|
||||
expect(material, isNot(paints..rrect(color: disabledSecondaryActiveTrackColor)));
|
||||
|
||||
// Test defaults colors for disabled slider.
|
||||
await tester.pumpWidget(buildApp(enabled: false));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
..rrect(color: disabledActiveTrackColor)
|
||||
..rrect(color: disabledInactiveTrackColor)
|
||||
..rrect(color: disabledSecondaryActiveTrackColor),
|
||||
);
|
||||
expect(material, paints..circle()..rrect(color: disabledThumbColor));
|
||||
expect(material, isNot(paints..circle()..rrect(color: thumbColor)));
|
||||
expect(material, isNot(paints..rrect(color: activeTrackColor)));
|
||||
expect(material, isNot(paints..rrect(color: inactiveTrackColor)));
|
||||
expect(material, isNot(paints..rrect(color: secondaryActiveTrackColor)));
|
||||
|
||||
// Test defaults colors for disabled discrete slider.
|
||||
await tester.pumpWidget(buildApp(divisions: 3, enabled: false));
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
..rrect(color: disabledActiveTrackColor)
|
||||
..rrect(color: disabledInactiveTrackColor)
|
||||
..rrect(color: disabledSecondaryActiveTrackColor)
|
||||
..circle(color: disabledActiveTickMarkColor)
|
||||
..circle(color: disabledActiveTickMarkColor)
|
||||
..circle(color: disabledInactiveTickMarkColor)
|
||||
..circle(color: disabledInactiveTickMarkColor)
|
||||
..rrect(color: disabledThumbColor),
|
||||
);
|
||||
expect(material, isNot(paints..circle()..rrect(color: thumbColor)));
|
||||
expect(material, isNot(paints..rrect(color: activeTrackColor)));
|
||||
expect(material, isNot(paints..rrect(color: inactiveTrackColor)));
|
||||
expect(material, isNot(paints..rrect(color: secondaryActiveTrackColor)));
|
||||
|
||||
await tester.pumpWidget(buildApp(divisions: 3));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final Offset center = tester.getCenter(find.byType(Slider));
|
||||
final TestGesture gesture = await tester.startGesture(center);
|
||||
// Wait for value indicator animation to finish.
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay));
|
||||
expect(
|
||||
valueIndicatorBox,
|
||||
paints
|
||||
..scale()
|
||||
..rrect(color: valueIndicatorColor)
|
||||
);
|
||||
await gesture.up();
|
||||
} finally {
|
||||
debugDisableShadows = true;
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Slider value indicator text when year2023 is false', (WidgetTester tester) async {
|
||||
const double value = 50;
|
||||
final List<InlineSpan> log = <InlineSpan>[];
|
||||
final LoggingValueIndicatorShape loggingValueIndicatorShape = LoggingValueIndicatorShape(log);
|
||||
final ThemeData theme = ThemeData(
|
||||
sliderTheme: SliderThemeData(
|
||||
valueIndicatorShape: loggingValueIndicatorShape,
|
||||
),
|
||||
);
|
||||
|
||||
Widget buildSlider() {
|
||||
return MaterialApp(
|
||||
theme: theme,
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: Slider(
|
||||
year2023: false,
|
||||
max: 100.0,
|
||||
divisions: 4,
|
||||
label: '${value.round()}',
|
||||
value: value,
|
||||
onChanged: (double newValue) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Normal text
|
||||
await tester.pumpWidget(buildSlider());
|
||||
final Offset center = tester.getCenter(find.byType(Slider));
|
||||
final TestGesture gesture = await tester.startGesture(center);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(log.last.toPlainText(), '50');
|
||||
expect(log.last.style!.fontSize, 14.0);
|
||||
expect(log.last.style!.color, theme.colorScheme.onInverseSurface);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
}
|
||||
|
@ -2640,6 +2640,175 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Can customize track gap when year2023 is false', (WidgetTester tester) async {
|
||||
debugDisableShadows = false;
|
||||
try {
|
||||
Widget buildSlider({ double? trackGap }) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
sliderTheme: SliderThemeData(
|
||||
trackGap: trackGap,
|
||||
),
|
||||
),
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Material(
|
||||
child: Center(
|
||||
child: Slider(
|
||||
year2023: false,
|
||||
value: 0.5,
|
||||
onChanged: (double value) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildSlider(trackGap: 0));
|
||||
|
||||
final MaterialInkController material = Material.of(tester.element(find.byType(Slider)));
|
||||
|
||||
// Test default track shape.
|
||||
const Radius trackOuterCornerRadius = Radius.circular(8.0);
|
||||
const Radius trackInnerCornderRadius = Radius.circular(2.0);
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
// Active track.
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBAndCorners(
|
||||
24.0, 292.0, 400.0, 308.0,
|
||||
topLeft: trackOuterCornerRadius,
|
||||
topRight: trackInnerCornderRadius,
|
||||
bottomRight: trackInnerCornderRadius,
|
||||
bottomLeft: trackOuterCornerRadius,
|
||||
),
|
||||
)
|
||||
// Inctive track.
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBAndCorners(
|
||||
400.0, 292.0, 776.0, 308.0,
|
||||
topLeft: trackInnerCornderRadius,
|
||||
topRight: trackOuterCornerRadius,
|
||||
bottomRight: trackOuterCornerRadius,
|
||||
bottomLeft: trackInnerCornderRadius,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildSlider(trackGap: 10));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
// Active track.
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBAndCorners(
|
||||
24.0, 292.0, 390.0, 308.0,
|
||||
topLeft: trackOuterCornerRadius,
|
||||
topRight: trackInnerCornderRadius,
|
||||
bottomRight: trackInnerCornderRadius,
|
||||
bottomLeft: trackOuterCornerRadius,
|
||||
),
|
||||
)
|
||||
// Inctive track.
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBAndCorners(
|
||||
410.0, 292.0, 776.0, 308.0,
|
||||
topLeft: trackInnerCornderRadius,
|
||||
topRight: trackOuterCornerRadius,
|
||||
bottomRight: trackOuterCornerRadius,
|
||||
bottomLeft: trackInnerCornderRadius,
|
||||
),
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
debugDisableShadows = true;
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Can customize thumb size when year2023 is false', (WidgetTester tester) async {
|
||||
debugDisableShadows = false;
|
||||
try {
|
||||
Widget buildSlider({ WidgetStateProperty<Size?>? thumbSize }) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
sliderTheme: SliderThemeData(
|
||||
thumbSize: thumbSize,
|
||||
),
|
||||
),
|
||||
home: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Material(
|
||||
child: Center(
|
||||
child: Slider(
|
||||
year2023: false,
|
||||
value: 0.5,
|
||||
onChanged: (double value) { },
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildSlider(thumbSize: const WidgetStatePropertyAll<Size>(Size(20, 20))));
|
||||
|
||||
final MaterialInkController material = Material.of(tester.element(find.byType(Slider)));
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
..circle()
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBR(
|
||||
390.0, 290.0, 410.0, 310.0,
|
||||
const Radius.circular(10.0),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.pumpWidget(buildSlider(thumbSize: const WidgetStateProperty<Size?>.fromMap(
|
||||
<WidgetStatesConstraint, Size>{
|
||||
WidgetState.pressed: Size(20, 20),
|
||||
WidgetState.any: Size(10, 10),
|
||||
},
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
..circle()
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBR(
|
||||
395.0, 295.0, 405.0, 305.0,
|
||||
const Radius.circular(5.0),
|
||||
),
|
||||
));
|
||||
|
||||
|
||||
final Offset center = tester.getCenter(find.byType(Slider));
|
||||
final TestGesture gesture = await tester.startGesture(center);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
material,
|
||||
paints
|
||||
..circle()
|
||||
..rrect(
|
||||
rrect: RRect.fromLTRBR(
|
||||
390.0, 295.0, 410.0, 305.0,
|
||||
const Radius.circular(5.0),
|
||||
),
|
||||
));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
} finally {
|
||||
debugDisableShadows = true;
|
||||
}
|
||||
});
|
||||
|
||||
group('Material 2', () {
|
||||
// These tests are only relevant for Material 2. Once Material 2
|
||||
// support is deprecated and the APIs are removed, these tests
|
||||
|
Loading…
x
Reference in New Issue
Block a user