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_bar_template.dart';
|
||||||
import 'package:gen_defaults/search_view_template.dart';
|
import 'package:gen_defaults/search_view_template.dart';
|
||||||
import 'package:gen_defaults/segmented_button_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/snackbar_template.dart';
|
||||||
import 'package:gen_defaults/surface_tint.dart';
|
import 'package:gen_defaults/surface_tint.dart';
|
||||||
import 'package:gen_defaults/switch_template.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();
|
SearchViewTemplate('SearchView', '$materialLib/search_anchor.dart', tokens).updateFile();
|
||||||
SegmentedButtonTemplate('md.comp.outlined-segmented-button', 'SegmentedButton', '$materialLib/segmented_button.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();
|
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();
|
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
|
||||||
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
|
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
|
||||||
TimePickerTemplate('TimePicker', '$materialLib/time_picker.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.drag-handle.width,
|
||||||
md.comp.sheet.bottom.docked.modal.container.elevation,
|
md.comp.sheet.bottom.docked.modal.container.elevation,
|
||||||
md.comp.sheet.bottom.docked.standard.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.focus.label-text.color,
|
||||||
md.comp.snackbar.action.hover.label-text.color,
|
md.comp.snackbar.action.hover.label-text.color,
|
||||||
md.comp.snackbar.action.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')};
|
Color? get inactiveTrackColor => ${componentColor('$tokenGroup.inactive.track')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
|
Color? get secondaryActiveTrackColor => ${componentColor('$tokenGroup.active.track')}.withOpacity(0.54);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get disabledActiveTrackColor => ${componentColor('$tokenGroup.disabled.active.track')};
|
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')};
|
Color? get disabledInactiveTrackColor => ${componentColor('$tokenGroup.disabled.inactive.track')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);
|
Color? get disabledSecondaryActiveTrackColor => ${componentColor('$tokenGroup.disabled.active.track')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get activeTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.active.container')};
|
Color? get activeTickMarkColor => ${componentColor('$tokenGroup.active.stop-indicator.container')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get inactiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.inactive.container')};
|
Color? get inactiveTickMarkColor => ${componentColor('$tokenGroup.inactive.stop-indicator.container')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get disabledActiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.disabled.container')};
|
Color? get disabledActiveTickMarkColor => ${componentColor('$tokenGroup.disabled.active.stop-indicator.container')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get disabledInactiveTickMarkColor => ${componentColor('$tokenGroup.with-tick-marks.disabled.container')};
|
Color? get disabledInactiveTickMarkColor => ${componentColor('$tokenGroup.disabled.inactive.stop-indicator.container')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get thumbColor => ${componentColor('$tokenGroup.handle')};
|
Color? get thumbColor => ${componentColor('$tokenGroup.handle')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get disabledThumbColor => Color.alphaBlend(${componentColor('$tokenGroup.disabled.handle')}, _colors.surface);
|
Color? get disabledThumbColor => ${componentColor('$tokenGroup.disabled.handle')};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get overlayColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
|
Color? get overlayColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
|
||||||
if (states.contains(MaterialState.dragged)) {
|
if (states.contains(MaterialState.dragged)) {
|
||||||
return ${componentColor('$tokenGroup.pressed.state-layer')};
|
return _colors.primary.withOpacity(0.1);
|
||||||
}
|
}
|
||||||
if (states.contains(MaterialState.hovered)) {
|
if (states.contains(MaterialState.hovered)) {
|
||||||
return ${componentColor('$tokenGroup.hover.state-layer')};
|
return _colors.primary.withOpacity(0.08);
|
||||||
}
|
}
|
||||||
if (states.contains(MaterialState.focused)) {
|
if (states.contains(MaterialState.focused)) {
|
||||||
return ${componentColor('$tokenGroup.focus.state-layer')};
|
return _colors.primary.withOpacity(0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Colors.transparent;
|
return Colors.transparent;
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextStyle? get valueIndicatorTextStyle => ${textStyle('$tokenGroup.label.label-text')}!.copyWith(
|
TextStyle? get valueIndicatorTextStyle => ${textStyle('$tokenGroup.value-indicator.label.label-text')}!.copyWith(
|
||||||
color: ${componentColor('$tokenGroup.label.label-text')},
|
color: ${componentColor('$tokenGroup.value-indicator.label.label-text')},
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@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.autofocus = false,
|
||||||
this.allowedInteraction,
|
this.allowedInteraction,
|
||||||
this.padding,
|
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,
|
}) : _sliderType = _SliderType.material,
|
||||||
assert(min <= max),
|
assert(min <= max),
|
||||||
assert(value >= min && value <= max,
|
assert(value >= min && value <= max,
|
||||||
@ -238,6 +243,11 @@ class Slider extends StatefulWidget {
|
|||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
this.allowedInteraction,
|
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,
|
}) : _sliderType = _SliderType.adaptive,
|
||||||
padding = null,
|
padding = null,
|
||||||
assert(min <= max),
|
assert(min <= max),
|
||||||
@ -432,9 +442,10 @@ class Slider extends StatefulWidget {
|
|||||||
/// maximum value.
|
/// maximum value.
|
||||||
///
|
///
|
||||||
/// If null, [SliderThemeData.inactiveTrackColor] of the ambient [SliderTheme]
|
/// If null, [SliderThemeData.inactiveTrackColor] of the ambient [SliderTheme]
|
||||||
/// is used. If that is null and [ThemeData.useMaterial3] is true,
|
/// is used. If [Slider.year2023] is false and [ThemeData.useMaterial3] is true,
|
||||||
/// [ColorScheme.surfaceContainerHighest] will be used, otherwise [ColorScheme.primary]
|
/// then [ColorScheme.secondaryContainer] is used and if [ThemeData.useMaterial3]
|
||||||
/// with an opacity of 0.24 will be used.
|
/// 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
|
/// Using a [SliderTheme] gives much more fine-grained control over the
|
||||||
/// appearance of various components of the slider.
|
/// appearance of various components of the slider.
|
||||||
@ -555,6 +566,18 @@ class Slider extends StatefulWidget {
|
|||||||
/// overlay shape, whichever is larger.
|
/// overlay shape, whichever is larger.
|
||||||
final EdgeInsetsGeometry? padding;
|
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 ;
|
final _SliderType _sliderType ;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -787,7 +810,12 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
|||||||
Widget _buildMaterialSlider(BuildContext context) {
|
Widget _buildMaterialSlider(BuildContext context) {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
SliderThemeData sliderTheme = SliderTheme.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
|
// 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
|
// 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
|
// the default shapes and text styles are aligned to the Material
|
||||||
// Guidelines.
|
// 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 ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
|
||||||
const SliderInteraction defaultAllowedInteraction = SliderInteraction.tapAndSlide;
|
const SliderInteraction defaultAllowedInteraction = SliderInteraction.tapAndSlide;
|
||||||
|
|
||||||
@ -815,12 +838,18 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
|||||||
// (which can be defined by activeColor) if the
|
// (which can be defined by activeColor) if the
|
||||||
// RectangularSliderValueIndicatorShape is used. In all other cases, the
|
// RectangularSliderValueIndicatorShape is used. In all other cases, the
|
||||||
// value indicator is assumed to be the same as the active color.
|
// 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;
|
final Color valueIndicatorColor;
|
||||||
if (valueIndicatorShape is RectangularSliderValueIndicatorShape) {
|
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 {
|
} else {
|
||||||
valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary;
|
valueIndicatorColor = widget.activeColor
|
||||||
|
?? sliderTheme.valueIndicatorColor
|
||||||
|
?? defaults.valueIndicatorColor!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Color? effectiveOverlayColor() {
|
Color? effectiveOverlayColor() {
|
||||||
@ -851,14 +880,16 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
|
|||||||
disabledThumbColor: sliderTheme.disabledThumbColor ?? defaults.disabledThumbColor,
|
disabledThumbColor: sliderTheme.disabledThumbColor ?? defaults.disabledThumbColor,
|
||||||
overlayColor: effectiveOverlayColor(),
|
overlayColor: effectiveOverlayColor(),
|
||||||
valueIndicatorColor: valueIndicatorColor,
|
valueIndicatorColor: valueIndicatorColor,
|
||||||
trackShape: sliderTheme.trackShape ?? defaultTrackShape,
|
trackShape: sliderTheme.trackShape ?? defaults.trackShape,
|
||||||
tickMarkShape: sliderTheme.tickMarkShape ?? defaultTickMarkShape,
|
tickMarkShape: sliderTheme.tickMarkShape ?? defaults.tickMarkShape,
|
||||||
thumbShape: sliderTheme.thumbShape ?? defaultThumbShape,
|
thumbShape: sliderTheme.thumbShape ?? defaults.thumbShape,
|
||||||
overlayShape: sliderTheme.overlayShape ?? defaultOverlayShape,
|
overlayShape: sliderTheme.overlayShape ?? defaults.overlayShape,
|
||||||
valueIndicatorShape: valueIndicatorShape,
|
valueIndicatorShape: valueIndicatorShape,
|
||||||
showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator,
|
showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator,
|
||||||
valueIndicatorTextStyle: valueIndicatorTextStyle,
|
valueIndicatorTextStyle: valueIndicatorTextStyle,
|
||||||
padding: widget.padding ?? sliderTheme.padding,
|
padding: widget.padding ?? sliderTheme.padding,
|
||||||
|
thumbSize: sliderTheme.thumbSize ?? defaults.thumbSize,
|
||||||
|
trackGap: sliderTheme.trackGap ?? defaults.trackGap,
|
||||||
);
|
);
|
||||||
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
|
final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
|
||||||
?? sliderTheme.mouseCursor?.resolve(states)
|
?? sliderTheme.mouseCursor?.resolve(states)
|
||||||
@ -1683,8 +1714,8 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
: trackRect.left + visualPosition * trackRect.width;
|
: trackRect.left + visualPosition * trackRect.width;
|
||||||
// Apply padding to trackRect.left and trackRect.right if the track height is
|
// 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.
|
// greater than the thumb radius to ensure the thumb is drawn within the track.
|
||||||
final Size thumbSize = _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete);
|
final Size thumbPreferredSize = _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete);
|
||||||
final double thumbPadding = (padding > thumbSize.width / 2 ? padding / 2 : 0);
|
final double thumbPadding = (padding > thumbPreferredSize.width / 2 ? padding / 2 : 0);
|
||||||
final Offset thumbCenter = Offset(
|
final Offset thumbCenter = Offset(
|
||||||
clampDouble(
|
clampDouble(
|
||||||
thumbPosition,
|
thumbPosition,
|
||||||
@ -1697,13 +1728,32 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize(isInteractive, false);
|
final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize(isInteractive, false);
|
||||||
overlayRect = Rect.fromCircle(center: thumbCenter, radius: overlaySize.width / 2.0);
|
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(
|
_sliderTheme.trackShape!.paint(
|
||||||
context,
|
context,
|
||||||
offset,
|
offset,
|
||||||
parentBox: this,
|
parentBox: this,
|
||||||
sliderTheme: _sliderTheme,
|
sliderTheme: _sliderTheme.copyWith(trackGap: trackGap),
|
||||||
enableAnimation: _enableAnimation,
|
enableAnimation: _enableAnimation,
|
||||||
textDirection: _textDirection,
|
textDirection: _textDirection,
|
||||||
thumbCenter: thumbCenter,
|
thumbCenter: thumbCenter,
|
||||||
@ -1789,7 +1839,9 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
isDiscrete: isDiscrete,
|
isDiscrete: isDiscrete,
|
||||||
labelPainter: _labelPainter,
|
labelPainter: _labelPainter,
|
||||||
parentBox: this,
|
parentBox: this,
|
||||||
sliderTheme: _sliderTheme,
|
sliderTheme: thumbWidth != null && thumbHeight != null
|
||||||
|
? _sliderTheme.copyWith(thumbSize: MaterialStatePropertyAll<Size?>(Size(thumbWidth, thumbHeight)))
|
||||||
|
: _sliderTheme,
|
||||||
textDirection: _textDirection,
|
textDirection: _textDirection,
|
||||||
value: _value,
|
value: _value,
|
||||||
textScaleFactor: textScaleFactor,
|
textScaleFactor: textScaleFactor,
|
||||||
@ -1960,12 +2012,11 @@ class _RenderValueIndicator extends RenderBox with RelayoutWhenSystemFontsChange
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SliderDefaultsM2 extends SliderThemeData {
|
class _SliderDefaultsM2 extends SliderThemeData {
|
||||||
_SliderDefaultsM2(this.context)
|
_SliderDefaultsM2(this.context) : super(trackHeight: 4.0);
|
||||||
: _colors = Theme.of(context).colorScheme,
|
|
||||||
super(trackHeight: 4.0);
|
|
||||||
|
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
final ColorScheme _colors;
|
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||||
|
late final SliderThemeData sliderTheme = SliderTheme.of(context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get activeTrackColor => _colors.primary;
|
Color? get activeTrackColor => _colors.primary;
|
||||||
@ -2012,19 +2063,31 @@ class _SliderDefaultsM2 extends SliderThemeData {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SliderComponentShape? get valueIndicatorShape => const RectangularSliderValueIndicatorShape();
|
Color? get valueIndicatorColor {
|
||||||
|
if (sliderTheme.valueIndicatorShape is RoundedRectSliderValueIndicatorShape) {
|
||||||
|
return _colors.inverseSurface;
|
||||||
|
}
|
||||||
|
return _colors.primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(quncheng): Update M3 defaults to match the latest specs.
|
@override
|
||||||
// BEGIN GENERATED TOKEN PROPERTIES - Slider
|
SliderComponentShape? get valueIndicatorShape => const RectangularSliderValueIndicatorShape();
|
||||||
|
|
||||||
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
@override
|
||||||
// "END GENERATED" comments are generated from data in the Material
|
SliderComponentShape? get thumbShape => const RoundSliderThumbShape();
|
||||||
// Design token database by the script:
|
|
||||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
|
||||||
|
|
||||||
class _SliderDefaultsM3 extends SliderThemeData {
|
@override
|
||||||
_SliderDefaultsM3(this.context)
|
SliderTrackShape? get trackShape => const RoundedRectSliderTrackShape();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SliderComponentShape? get overlayShape => const RoundSliderOverlayShape();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliderDefaultsM3Year2023 extends SliderThemeData {
|
||||||
|
_SliderDefaultsM3Year2023(this.context)
|
||||||
: super(trackHeight: 4.0);
|
: super(trackHeight: 4.0);
|
||||||
|
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
@ -2086,8 +2149,134 @@ class _SliderDefaultsM3 extends SliderThemeData {
|
|||||||
color: _colors.onPrimary,
|
color: _colors.onPrimary,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get valueIndicatorColor => _colors.primary;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
|
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
|
// END GENERATED TOKEN PROPERTIES - Slider
|
||||||
|
@ -297,6 +297,8 @@ class SliderThemeData with Diagnosticable {
|
|||||||
this.mouseCursor,
|
this.mouseCursor,
|
||||||
this.allowedInteraction,
|
this.allowedInteraction,
|
||||||
this.padding,
|
this.padding,
|
||||||
|
this.thumbSize,
|
||||||
|
this.trackGap,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Generates a SliderThemeData from three main colors.
|
/// Generates a SliderThemeData from three main colors.
|
||||||
@ -597,6 +599,30 @@ class SliderThemeData with Diagnosticable {
|
|||||||
/// overlay shape, whichever is larger.
|
/// overlay shape, whichever is larger.
|
||||||
final EdgeInsetsGeometry? padding;
|
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
|
/// Creates a copy of this object but with the given fields replaced with the
|
||||||
/// new values.
|
/// new values.
|
||||||
SliderThemeData copyWith({
|
SliderThemeData copyWith({
|
||||||
@ -633,6 +659,8 @@ class SliderThemeData with Diagnosticable {
|
|||||||
MaterialStateProperty<MouseCursor?>? mouseCursor,
|
MaterialStateProperty<MouseCursor?>? mouseCursor,
|
||||||
SliderInteraction? allowedInteraction,
|
SliderInteraction? allowedInteraction,
|
||||||
EdgeInsetsGeometry? padding,
|
EdgeInsetsGeometry? padding,
|
||||||
|
MaterialStateProperty<Size?>? thumbSize,
|
||||||
|
double? trackGap,
|
||||||
}) {
|
}) {
|
||||||
return SliderThemeData(
|
return SliderThemeData(
|
||||||
trackHeight: trackHeight ?? this.trackHeight,
|
trackHeight: trackHeight ?? this.trackHeight,
|
||||||
@ -668,6 +696,8 @@ class SliderThemeData with Diagnosticable {
|
|||||||
mouseCursor: mouseCursor ?? this.mouseCursor,
|
mouseCursor: mouseCursor ?? this.mouseCursor,
|
||||||
allowedInteraction: allowedInteraction ?? this.allowedInteraction,
|
allowedInteraction: allowedInteraction ?? this.allowedInteraction,
|
||||||
padding: padding ?? this.padding,
|
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,
|
mouseCursor: t < 0.5 ? a.mouseCursor : b.mouseCursor,
|
||||||
allowedInteraction: t < 0.5 ? a.allowedInteraction : b.allowedInteraction,
|
allowedInteraction: t < 0.5 ? a.allowedInteraction : b.allowedInteraction,
|
||||||
padding: EdgeInsetsGeometry.lerp(a.padding, b.padding, t),
|
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,
|
mouseCursor,
|
||||||
allowedInteraction,
|
allowedInteraction,
|
||||||
padding,
|
padding,
|
||||||
|
thumbSize,
|
||||||
|
trackGap,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -794,7 +828,9 @@ class SliderThemeData with Diagnosticable {
|
|||||||
&& other.thumbSelector == thumbSelector
|
&& other.thumbSelector == thumbSelector
|
||||||
&& other.mouseCursor == mouseCursor
|
&& other.mouseCursor == mouseCursor
|
||||||
&& other.allowedInteraction == allowedInteraction
|
&& other.allowedInteraction == allowedInteraction
|
||||||
&& other.padding == padding;
|
&& other.padding == padding
|
||||||
|
&& other.thumbSize == thumbSize
|
||||||
|
&& other.trackGap == trackGap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -834,6 +870,8 @@ class SliderThemeData with Diagnosticable {
|
|||||||
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
|
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: defaultData.mouseCursor));
|
||||||
properties.add(EnumProperty<SliderInteraction>('allowedInteraction', allowedInteraction, defaultValue: defaultData.allowedInteraction));
|
properties.add(EnumProperty<SliderInteraction>('allowedInteraction', allowedInteraction, defaultValue: defaultData.allowedInteraction));
|
||||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: defaultData.padding));
|
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();
|
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', () {
|
group('Material 2', () {
|
||||||
// These tests are only relevant for Material 2. Once Material 2
|
// These tests are only relevant for Material 2. Once Material 2
|
||||||
// support is deprecated and the APIs are removed, these tests
|
// support is deprecated and the APIs are removed, these tests
|
||||||
|
Loading…
x
Reference in New Issue
Block a user