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:
Taha Tesser 2024-11-19 21:10:44 +02:00 committed by GitHub
parent 8536b96ebb
commit 56cfef73fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1171 additions and 52 deletions

View File

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

View File

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

1 Versions used 6_1_0
655 md.comp.sheet.bottom.docked.drag-handle.width
656 md.comp.sheet.bottom.docked.modal.container.elevation
657 md.comp.sheet.bottom.docked.standard.container.elevation
658 md.comp.slider.active.handle.padding
659 md.comp.slider.active.stop-indicator.container.color
660 md.comp.slider.active.stop-indicator.container.opacity
661 md.comp.slider.active.track.color
662 md.comp.slider.active.track.height
663 md.comp.slider.disabled.active.stop-indicator.container.color
664 md.comp.slider.disabled.active.track.color
665 md.comp.slider.disabled.active.track.opacity
666 md.comp.slider.disabled.handle.color
667 md.comp.slider.disabled.handle.opacity
668 md.comp.slider.disabled.handle.width
669 md.comp.slider.disabled.inactive.stop-indicator.container.color
670 md.comp.slider.disabled.inactive.track.color
671 md.comp.slider.disabled.inactive.track.opacity
672 md.comp.slider.focus.handle.width
673 md.comp.slider.handle.color
674 md.comp.slider.handle.height
675 md.comp.slider.handle.width
676 md.comp.slider.hover.handle.width
677 md.comp.slider.inactive.stop-indicator.container.color
678 md.comp.slider.inactive.stop-indicator.container.opacity
679 md.comp.slider.inactive.track.color
680 md.comp.slider.pressed.handle.width
681 md.comp.slider.stop-indicator.size
682 md.comp.slider.value-indicator.container.color
683 md.comp.slider.value-indicator.label.label-text.color
684 md.comp.slider.value-indicator.label.label-text.text-style
685 md.comp.snackbar.action.focus.label-text.color
686 md.comp.snackbar.action.hover.label-text.color
687 md.comp.snackbar.action.label-text.color

View File

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

View File

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

View File

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

View File

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

View File

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