// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:math' as math; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart' show timeDilation; import 'package:flutter/services.dart'; enum _SliderType { material, adaptive } /// A Material Design slider. /// /// Used to select from a range of values. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=ufb4gIPDmEs} /// /// {@tool dartpad} /// This example showcases non-discrete and discrete [VerticalSlider]s. /// The [VerticalSlider]s will show the updated ![Material 3 Design appearance](https://m3.material.io/components/sliders/overview) /// when setting the [VerticalSlider.year2023] flag to false. /// /// ** See code in examples/api/lib/material/slider/slider.0.dart ** /// {@end-tool} /// /// {@tool dartpad} /// This example shows a [VerticalSlider] widget using the [VerticalSlider.secondaryTrackValue] /// to show a secondary track in the slider. /// /// ** See code in examples/api/lib/material/slider/slider.1.dart ** /// {@end-tool} /// /// A slider can be used to select from either a continuous or a discrete set of /// values. The default is to use a continuous range of values from [min] to /// [max]. To use discrete values, use a non-null value for [divisions], which /// indicates the number of discrete intervals. For example, if [min] is 0.0 and /// [max] is 50.0 and [divisions] is 5, then the slider can take on the /// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0. /// /// The terms for the parts of a slider are: /// /// * The "thumb", which is a shape that slides horizontally when the user /// drags it. /// * The "track", which is the line that the slider thumb slides along. /// * The "value indicator", which is a shape that pops up when the user /// is dragging the thumb to indicate the value being selected. /// * The "active" side of the slider is the side between the thumb and the /// minimum value. /// * The "inactive" side of the slider is the side between the thumb and the /// maximum value. /// /// The slider will be disabled if [onChanged] is null or if the range given by /// [min]..[max] is empty (i.e. if [min] is equal to [max]). /// /// The slider widget itself does not maintain any state. Instead, when the state /// of the slider changes, the widget calls the [onChanged] callback. Most /// widgets that use a slider will listen for the [onChanged] callback and /// rebuild the slider with a new [value] to update the visual appearance of the /// slider. To know when the value starts to change, or when it is done /// changing, set the optional callbacks [onChangeStart] and/or [onChangeEnd]. /// /// By default, a slider will be as wide as possible, centered vertically. When /// given unbounded constraints, it will attempt to make the track 144 pixels /// wide (with margins on each side) and will shrink-wrap vertically. /// /// Requires one of its ancestors to be a [Material] widget. /// /// Requires one of its ancestors to be a [MediaQuery] widget. Typically, these /// are introduced by the [MaterialApp] or [WidgetsApp] widget at the top of /// your application widget tree. /// /// To determine how it should be displayed (e.g. colors, thumb shape, etc.), /// a slider uses the [SliderThemeData] available from either a [SliderTheme] /// widget or the [ThemeData.sliderTheme] a [Theme] widget above it in the /// widget tree. You can also override some of the colors with the [activeColor] /// and [inactiveColor] properties, although more fine-grained control of the /// look is achieved using a [SliderThemeData]. /// /// See also: /// /// * [SliderTheme] and [SliderThemeData] for information about controlling /// the visual appearance of the slider. /// * [Radio], for selecting among a set of explicit values. /// * [Checkbox] and [Switch], for toggling a particular value on or off. /// * /// * [MediaQuery], from which the text scale factor is obtained. class VerticalSlider extends StatefulWidget { /// Creates a Material Design slider. /// /// The slider itself does not maintain any state. Instead, when the state of /// the slider changes, the widget calls the [onChanged] callback. Most /// widgets that use a slider will listen for the [onChanged] callback and /// rebuild the slider with a new [value] to update the visual appearance of /// the slider. /// /// * [value] determines currently selected value for this slider. /// * [onChanged] is called while the user is selecting a new value for the /// slider. /// * [onChangeStart] is called when the user starts to select a new value for /// the slider. /// * [onChangeEnd] is called when the user is done selecting a new value for /// the slider. /// /// You can override some of the colors with the [activeColor] and /// [inactiveColor] properties, although more fine-grained control of the /// appearance is achieved using a [SliderThemeData]. const VerticalSlider({ super.key, required this.value, this.secondaryTrackValue, required this.onChanged, this.onChangeStart, this.onChangeEnd, this.min = 0.0, this.max = 1.0, this.divisions, this.label, this.activeColor, this.inactiveColor, this.secondaryActiveColor, this.thumbColor, this.overlayColor, this.mouseCursor, this.semanticFormatterCallback, this.focusNode, this.autofocus = false, this.allowedInteraction, this.padding, this.showValueIndicator, @Deprecated( 'Set this flag to false to opt into the 2024 slider appearance. Defaults to true. ' 'In the future, this flag will default to false. Use SliderThemeData to customize individual properties. ' 'This feature was deprecated after v3.27.0-0.2.pre.', ) this.year2023, }) : _sliderType = _SliderType.material, assert(min <= max), assert( value >= min && value <= max, 'Value $value is not between minimum $min and maximum $max', ), assert( secondaryTrackValue == null || (secondaryTrackValue >= min && secondaryTrackValue <= max), 'SecondaryValue $secondaryTrackValue is not between $min and $max', ), assert(divisions == null || divisions > 0); /// Creates an adaptive [VerticalSlider] based on the target platform, following /// Material design's /// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html). /// /// Creates a [CupertinoSlider] if the target platform is iOS or macOS, creates a /// Material Design slider otherwise. /// /// If a [CupertinoSlider] is created, the following parameters are ignored: /// [secondaryTrackValue], [label], [inactiveColor], [secondaryActiveColor], /// [semanticFormatterCallback], [showValueIndicator]. /// /// The target platform is based on the current [Theme]: [ThemeData.platform]. const VerticalSlider.adaptive({ super.key, required this.value, this.secondaryTrackValue, required this.onChanged, this.onChangeStart, this.onChangeEnd, this.min = 0.0, this.max = 1.0, this.divisions, this.label, this.mouseCursor, this.activeColor, this.inactiveColor, this.secondaryActiveColor, this.thumbColor, this.overlayColor, this.semanticFormatterCallback, this.focusNode, this.autofocus = false, this.allowedInteraction, this.showValueIndicator, @Deprecated( 'Set this flag to false to opt into the 2024 slider appearance. Defaults to true. ' 'In the future, this flag will default to false. Use SliderThemeData to customize individual properties. ' 'This feature was deprecated after v3.27.0-0.1.pre.', ) this.year2023, }) : _sliderType = _SliderType.adaptive, padding = null, assert(min <= max), assert( value >= min && value <= max, 'Value $value is not between minimum $min and maximum $max', ), assert( secondaryTrackValue == null || (secondaryTrackValue >= min && secondaryTrackValue <= max), 'SecondaryValue $secondaryTrackValue is not between $min and $max', ), assert(divisions == null || divisions > 0); /// The currently selected value for this slider. /// /// The slider's thumb is drawn at a position that corresponds to this value. final double value; /// The secondary track value for this slider. /// /// If not null, a secondary track using [VerticalSlider.secondaryActiveColor] color /// is drawn between the thumb and this value, over the inactive track. /// /// If less than [VerticalSlider.value], then the secondary track is not shown. /// /// It can be ideal for media scenarios such as showing the buffering progress /// while the [VerticalSlider.value] shows the play progress. final double? secondaryTrackValue; /// Called during a drag when the user is selecting a new value for the slider /// by dragging. /// /// The slider passes the new value to the callback but does not actually /// change state until the parent widget rebuilds the slider with the new /// value. /// /// If null, the slider will be displayed as disabled. /// /// The callback provided to onChanged should update the state of the parent /// [StatefulWidget] using the [State.setState] method, so that the parent /// gets rebuilt; for example: /// /// {@tool snippet} /// /// ```dart /// Slider( /// value: _duelCommandment.toDouble(), /// min: 1.0, /// max: 10.0, /// divisions: 10, /// label: '$_duelCommandment', /// onChanged: (double newValue) { /// setState(() { /// _duelCommandment = newValue.round(); /// }); /// }, /// ) /// ``` /// {@end-tool} /// /// See also: /// /// * [onChangeStart] for a callback that is called when the user starts /// changing the value. /// * [onChangeEnd] for a callback that is called when the user stops /// changing the value. final ValueChanged? onChanged; /// Called when the user starts selecting a new value for the slider. /// /// This callback shouldn't be used to update the slider [value] (use /// [onChanged] for that), but rather to be notified when the user has started /// selecting a new value by starting a drag or with a tap. /// /// The value passed will be the last [value] that the slider had before the /// change began. /// /// {@tool snippet} /// /// ```dart /// Slider( /// value: _duelCommandment.toDouble(), /// min: 1.0, /// max: 10.0, /// divisions: 10, /// label: '$_duelCommandment', /// onChanged: (double newValue) { /// setState(() { /// _duelCommandment = newValue.round(); /// }); /// }, /// onChangeStart: (double startValue) { /// print('Started change at $startValue'); /// }, /// ) /// ``` /// {@end-tool} /// /// See also: /// /// * [onChangeEnd] for a callback that is called when the value change is /// complete. final ValueChanged? onChangeStart; /// Called when the user is done selecting a new value for the slider. /// /// This callback shouldn't be used to update the slider [value] (use /// [onChanged] for that), but rather to know when the user has completed /// selecting a new [value] by ending a drag or a click. /// /// {@tool snippet} /// /// ```dart /// Slider( /// value: _duelCommandment.toDouble(), /// min: 1.0, /// max: 10.0, /// divisions: 10, /// label: '$_duelCommandment', /// onChanged: (double newValue) { /// setState(() { /// _duelCommandment = newValue.round(); /// }); /// }, /// onChangeEnd: (double newValue) { /// print('Ended change on $newValue'); /// }, /// ) /// ``` /// {@end-tool} /// /// See also: /// /// * [onChangeStart] for a callback that is called when a value change /// begins. final ValueChanged? onChangeEnd; /// The minimum value the user can select. /// /// Defaults to 0.0. Must be less than or equal to [max]. /// /// If the [max] is equal to the [min], then the slider is disabled. final double min; /// The maximum value the user can select. /// /// Defaults to 1.0. Must be greater than or equal to [min]. /// /// If the [max] is equal to the [min], then the slider is disabled. final double max; /// The number of discrete divisions. /// /// Typically used with [label] to show the current discrete value. /// /// If null, the slider is continuous. final int? divisions; /// A label to show above the slider when the slider is active and /// [SliderThemeData.showValueIndicator] is satisfied. /// /// It is used to display the value of a discrete slider, and it is displayed /// as part of the value indicator shape. /// /// The label is rendered using the active [ThemeData]'s [TextTheme.bodyLarge] /// text style, with the theme data's [ColorScheme.onPrimary] color. The /// label's text style can be overridden with /// [SliderThemeData.valueIndicatorTextStyle]. /// /// If null, then the value indicator will not be displayed. /// /// Ignored if this slider is created with [Slider.adaptive]. /// /// See also: /// /// * [SliderComponentShape] for how to create a custom value indicator /// shape. final String? label; /// The color to use for the portion of the slider track that is active. /// /// The "active" side of the slider is the side between the thumb and the /// minimum value. /// /// If null, [SliderThemeData.activeTrackColor] of the ambient /// [SliderTheme] is used. If that is null, [ColorScheme.primary] of the /// surrounding [ThemeData] is used. /// /// Using a [SliderTheme] gives much more fine-grained control over the /// appearance of various components of the slider. final Color? activeColor; /// The color for the inactive portion of the slider track. /// /// The "inactive" side of the slider is the side between the thumb and the /// maximum value. /// /// If null, [SliderThemeData.inactiveTrackColor] of the ambient [SliderTheme] /// is used. If [VerticalSlider.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. /// /// Ignored if this slider is created with [Slider.adaptive]. final Color? inactiveColor; /// The color to use for the portion of the slider track between the thumb and /// the [VerticalSlider.secondaryTrackValue]. /// /// Defaults to the [SliderThemeData.secondaryActiveTrackColor] of the current /// [SliderTheme]. /// /// If that is also null, defaults to [ColorScheme.primary] with an /// opacity of 0.54. /// /// Using a [SliderTheme] gives much more fine-grained control over the /// appearance of various components of the slider. /// /// Ignored if this slider is created with [Slider.adaptive]. final Color? secondaryActiveColor; /// The color of the thumb. /// /// If this color is null, [VerticalSlider] will use [activeColor], If [activeColor] /// is also null, [VerticalSlider] will use [SliderThemeData.thumbColor]. /// /// If that is also null, defaults to [ColorScheme.primary]. /// /// * [CupertinoSlider] will have a white thumb /// (like the native default iOS slider). final Color? thumbColor; /// The highlight color that's typically used to indicate that /// the slider thumb is focused, hovered, or dragged. /// /// If this property is null, [VerticalSlider] will use [activeColor] with /// an opacity of 0.12, If null, [SliderThemeData.overlayColor] /// will be used. /// /// If that is also null, If [ThemeData.useMaterial3] is true, /// Slider will use [ColorScheme.primary] with an opacity of 0.08 when /// slider thumb is hovered and with an opacity of 0.1 when slider thumb /// is focused or dragged, If [ThemeData.useMaterial3] is false, defaults /// to [ColorScheme.primary] with an opacity of 0.12. final WidgetStateProperty? overlayColor; /// {@template flutter.material.slider.mouseCursor} /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. /// /// If [mouseCursor] is a [WidgetStateMouseCursor], /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: /// /// * [WidgetState.dragged]. /// * [WidgetState.hovered]. /// * [WidgetState.focused]. /// * [WidgetState.disabled]. /// {@endtemplate} /// /// If null, then the value of [SliderThemeData.mouseCursor] is used. If that /// is also null, then [WidgetStateMouseCursor.clickable] is used. final MouseCursor? mouseCursor; /// The callback used to create a semantic value from a slider value. /// /// Defaults to formatting values as a percentage. /// /// This is used by accessibility frameworks like TalkBack on Android to /// inform users what the currently selected value is with more context. /// /// {@tool snippet} /// /// In the example below, a slider for currency values is configured to /// announce a value with a currency label. /// /// ```dart /// Slider( /// value: _dollars.toDouble(), /// min: 20.0, /// max: 330.0, /// label: '$_dollars dollars', /// onChanged: (double newValue) { /// setState(() { /// _dollars = newValue.round(); /// }); /// }, /// semanticFormatterCallback: (double newValue) { /// return '${newValue.round()} dollars'; /// } /// ) /// ``` /// {@end-tool} /// /// Ignored if this slider is created with [Slider.adaptive] final SemanticFormatterCallback? semanticFormatterCallback; /// {@macro flutter.widgets.Focus.focusNode} final FocusNode? focusNode; /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; /// Allowed way for the user to interact with the [VerticalSlider]. /// /// For example, if this is set to [SliderInteraction.tapOnly], the user can /// interact with the slider only by tapping anywhere on the track. Sliding /// will have no effect. /// /// Defaults to [SliderInteraction.tapAndSlide]. final SliderInteraction? allowedInteraction; /// Determines the padding around the [VerticalSlider]. /// /// If specified, this padding overrides the default vertical padding of /// the [VerticalSlider], defaults to the height of the overlay shape, and the /// horizontal padding, defaults to the width of the thumb shape or /// overlay shape, whichever is larger. final EdgeInsetsGeometry? padding; /// Determines the conditions under which the value indicator is shown. /// /// If [VerticalSlider.showValueIndicator] is null then the /// ambient [SliderThemeData.showValueIndicator] is used. If that is also /// null, defaults to [ShowValueIndicator.onlyForDiscrete]. final ShowValueIndicator? showValueIndicator; /// When true, the [VerticalSlider] will use the 2023 Material Design 3 appearance. /// Defaults to true. /// /// If this is set to false, the [VerticalSlider] will use the latest Material Design 3 /// appearance, which was introduced in December 2023. /// /// If [ThemeData.useMaterial3] is false, then this property is ignored. @Deprecated( 'Set this flag to false to opt into the 2024 slider appearance. Defaults to true. ' 'In the future, this flag will default to false. Use SliderThemeData to customize individual properties. ' 'This feature was deprecated after v3.27.0-0.1.pre.', ) final bool? year2023; final _SliderType _sliderType; @override State createState() => _VerticalSliderState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DoubleProperty('value', value)) ..add(DoubleProperty('secondaryTrackValue', secondaryTrackValue)) ..add( ObjectFlagProperty>( 'onChanged', onChanged, ifNull: 'disabled', ), ) ..add( ObjectFlagProperty>.has( 'onChangeStart', onChangeStart, ), ) ..add( ObjectFlagProperty>.has( 'onChangeEnd', onChangeEnd, ), ) ..add(DoubleProperty('min', min)) ..add(DoubleProperty('max', max)) ..add(IntProperty('divisions', divisions)) ..add(StringProperty('label', label)) ..add(ColorProperty('activeColor', activeColor)) ..add(ColorProperty('inactiveColor', inactiveColor)) ..add(ColorProperty('secondaryActiveColor', secondaryActiveColor)) ..add( ObjectFlagProperty>.has( 'semanticFormatterCallback', semanticFormatterCallback, ), ) ..add(ObjectFlagProperty.has('focusNode', focusNode)) ..add( FlagProperty('autofocus', value: autofocus, ifTrue: 'autofocus'), ); } } class _VerticalSliderState extends State with TickerProviderStateMixin { static const Duration enableAnimationDuration = Duration(milliseconds: 75); static const Duration valueIndicatorAnimationDuration = Duration( milliseconds: 100, ); // Animation controller that is run when the overlay (a.k.a radial reaction) // is shown in response to user interaction. late AnimationController overlayController; // Animation controller that is run when the value indicator is being shown // or hidden. late AnimationController valueIndicatorController; // Animation controller that is run when enabling/disabling the slider. late AnimationController enableController; // Animation controller that is run when transitioning between one value // and the next on a discrete slider. late AnimationController positionController; Timer? interactionTimer; final GlobalKey _renderObjectKey = GlobalKey(); // Keyboard mapping for a focused slider. static const Map _traditionalNavShortcutMap = { SingleActivator(LogicalKeyboardKey.arrowUp): _AdjustSliderIntent.up(), SingleActivator(LogicalKeyboardKey.arrowDown): _AdjustSliderIntent.down(), SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(), SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(), }; // Keyboard mapping for a focused slider when using directional navigation. // The vertical inputs are not handled to allow navigating out of the slider. static const Map _directionalNavShortcutMap = { SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(), SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(), }; // Action mapping for a focused slider. late Map> _actionMap; bool get _enabled => widget.onChanged != null; // Value Indicator Animation that appears on the Overlay. PaintValueIndicator? paintValueIndicator; bool _dragging = false; // For discrete sliders, _handleChanged might receive the same value // multiple times. To avoid calling widget.onChanged repeatedly, the // value from _handleChanged is temporarily saved here. double? _currentChangedValue; FocusNode? _focusNode; FocusNode get focusNode => widget.focusNode ?? _focusNode!; // Always keep the ValueIndicator visible on the Overlay; otherwise, it cannot be updated during the build phase. final OverlayPortalController _valueIndicatorOverlayPortalController = OverlayPortalController( debugLabel: 'Slider ValueIndicator', )..show(); @override void initState() { super.initState(); overlayController = AnimationController( duration: kRadialReactionDuration, vsync: this, ); valueIndicatorController = AnimationController( duration: valueIndicatorAnimationDuration, vsync: this, ); enableController = AnimationController( duration: enableAnimationDuration, vsync: this, ); positionController = AnimationController( duration: Duration.zero, vsync: this, ); enableController.value = widget.onChanged != null ? 1.0 : 0.0; positionController.value = _convert(widget.value); _actionMap = >{ _AdjustSliderIntent: CallbackAction<_AdjustSliderIntent>( onInvoke: _actionHandler, ), }; if (widget.focusNode == null) { // Only create a new node if the widget doesn't have one. _focusNode ??= FocusNode(); } } @override void dispose() { interactionTimer?.cancel(); overlayController.dispose(); valueIndicatorController.dispose(); enableController.dispose(); positionController.dispose(); _focusNode?.dispose(); super.dispose(); } void _handleChanged(double value) { assert(widget.onChanged != null); final double lerpValue = _lerp(value); if (_currentChangedValue != lerpValue) { _currentChangedValue = lerpValue; if (_currentChangedValue != widget.value) { widget.onChanged!(_currentChangedValue!); } } } void _handleDragStart(double value) { setState(() { _dragging = true; }); widget.onChangeStart?.call(_lerp(value)); } void _handleDragEnd(double value) { setState(() { _dragging = false; }); _currentChangedValue = null; widget.onChangeEnd?.call(_lerp(value)); } void _actionHandler(_AdjustSliderIntent intent) { final TextDirection directionality = Directionality.of( _renderObjectKey.currentContext!, ); final bool shouldIncrease = switch (intent.type) { _SliderAdjustmentType.up => true, _SliderAdjustmentType.down => false, _SliderAdjustmentType.left => directionality == TextDirection.rtl, _SliderAdjustmentType.right => directionality == TextDirection.ltr, }; final slider = _renderObjectKey.currentContext!.findRenderObject()! as _RenderSlider; return shouldIncrease ? slider.increaseAction() : slider.decreaseAction(); } bool _focused = false; void _handleFocusHighlightChanged(bool focused) { if (focused != _focused) { setState(() { _focused = focused; }); } } bool _hovering = false; void _handleHoverChanged(bool hovering) { if (hovering != _hovering) { setState(() { _hovering = hovering; }); } } // Returns a number between min and max, proportional to value, which must // be between 0.0 and 1.0. double _lerp(double value) { assert(value >= 0.0); assert(value <= 1.0); return value * (widget.max - widget.min) + widget.min; } double _discretize(double value) { assert(widget.divisions != null); assert(value >= 0.0 && value <= 1.0); final int divisions = widget.divisions!; return (value * divisions).round() / divisions; } double _convert(double value) { double ret = _unlerp(value); if (widget.divisions != null) { ret = _discretize(ret); } return ret; } // Returns a number between 0.0 and 1.0, given a value between min and max. double _unlerp(double value) { assert(value <= widget.max); assert(value >= widget.min); return widget.max > widget.min ? (value - widget.min) / (widget.max - widget.min) : 0.0; } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); assert(debugCheckHasMediaQuery(context)); switch (widget._sliderType) { case _SliderType.material: return _buildMaterialSlider(context); case _SliderType.adaptive: { final ThemeData theme = Theme.of(context); switch (theme.platform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: return _buildMaterialSlider(context); case TargetPlatform.iOS: case TargetPlatform.macOS: return _buildCupertinoSlider(context); } } } } Widget _buildMaterialSlider(BuildContext context) { final ThemeData theme = Theme.of(context); SliderThemeData sliderTheme = SliderTheme.of(context); // ignore: deprecated_member_use final bool year2023 = widget.year2023 ?? sliderTheme.year2023 ?? true; final SliderThemeData defaults = switch (theme.useMaterial3) { true => 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 // control than that, then they need to use a SliderTheme. The default // colors come from the ThemeData.colorScheme. These colors, along with // the default shapes and text styles are aligned to the Material // Guidelines. const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete; const SliderInteraction defaultAllowedInteraction = SliderInteraction.tapAndSlide; final states = { if (!_enabled) WidgetState.disabled, if (_hovering) WidgetState.hovered, if (_focused) WidgetState.focused, if (_dragging) WidgetState.dragged, }; // The value indicator's color is not the same as the thumb and active track // (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 ?? defaults.valueIndicatorShape!; final Color valueIndicatorColor; if (valueIndicatorShape is RectangularSliderValueIndicatorShape) { valueIndicatorColor = sliderTheme.valueIndicatorColor ?? Color.alphaBlend( theme.colorScheme.onSurface.withValues(alpha: 0.60), theme.colorScheme.surface.withValues(alpha: 0.90), ); } else { valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? defaults.valueIndicatorColor!; } Color? effectiveOverlayColor() { return widget.overlayColor?.resolve(states) ?? widget.activeColor?.withValues(alpha: 0.12) ?? WidgetStateProperty.resolveAs( sliderTheme.overlayColor, states, ) ?? WidgetStateProperty.resolveAs(defaults.overlayColor, states); } TextStyle valueIndicatorTextStyle = sliderTheme.valueIndicatorTextStyle ?? defaults.valueIndicatorTextStyle!; if (MediaQuery.boldTextOf(context)) { valueIndicatorTextStyle = valueIndicatorTextStyle.merge( const TextStyle(fontWeight: FontWeight.bold), ); } sliderTheme = sliderTheme.copyWith( trackHeight: sliderTheme.trackHeight ?? defaults.trackHeight, activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? defaults.activeTrackColor, inactiveTrackColor: widget.inactiveColor ?? sliderTheme.inactiveTrackColor ?? defaults.inactiveTrackColor, secondaryActiveTrackColor: widget.secondaryActiveColor ?? sliderTheme.secondaryActiveTrackColor ?? defaults.secondaryActiveTrackColor, disabledActiveTrackColor: sliderTheme.disabledActiveTrackColor ?? defaults.disabledActiveTrackColor, disabledInactiveTrackColor: sliderTheme.disabledInactiveTrackColor ?? defaults.disabledInactiveTrackColor, disabledSecondaryActiveTrackColor: sliderTheme.disabledSecondaryActiveTrackColor ?? defaults.disabledSecondaryActiveTrackColor, activeTickMarkColor: widget.inactiveColor ?? sliderTheme.activeTickMarkColor ?? defaults.activeTickMarkColor, inactiveTickMarkColor: widget.activeColor ?? sliderTheme.inactiveTickMarkColor ?? defaults.inactiveTickMarkColor, disabledActiveTickMarkColor: sliderTheme.disabledActiveTickMarkColor ?? defaults.disabledActiveTickMarkColor, disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? defaults.disabledInactiveTickMarkColor, thumbColor: widget.thumbColor ?? widget.activeColor ?? sliderTheme.thumbColor ?? defaults.thumbColor, disabledThumbColor: sliderTheme.disabledThumbColor ?? defaults.disabledThumbColor, overlayColor: effectiveOverlayColor(), valueIndicatorColor: valueIndicatorColor, trackShape: sliderTheme.trackShape ?? defaults.trackShape, tickMarkShape: sliderTheme.tickMarkShape ?? defaults.tickMarkShape, thumbShape: sliderTheme.thumbShape ?? defaults.thumbShape, overlayShape: sliderTheme.overlayShape ?? defaults.overlayShape, valueIndicatorShape: valueIndicatorShape, showValueIndicator: widget.showValueIndicator ?? sliderTheme.showValueIndicator ?? defaultShowValueIndicator, valueIndicatorTextStyle: valueIndicatorTextStyle, padding: widget.padding ?? sliderTheme.padding, thumbSize: sliderTheme.thumbSize ?? defaults.thumbSize, trackGap: sliderTheme.trackGap ?? defaults.trackGap, ); final MouseCursor effectiveMouseCursor = WidgetStateProperty.resolveAs( widget.mouseCursor, states, ) ?? sliderTheme.mouseCursor?.resolve(states) ?? WidgetStateMouseCursor.clickable.resolve(states); final SliderInteraction effectiveAllowedInteraction = widget.allowedInteraction ?? sliderTheme.allowedInteraction ?? defaultAllowedInteraction; // This size is used as the max bounds for the painting of the value // indicators It must be kept in sync with the function with the same name // in range_slider.dart. Size screenSize() => MediaQuery.sizeOf(context); VoidCallback? handleDidGainAccessibilityFocus; switch (theme.platform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: case TargetPlatform.linux: case TargetPlatform.macOS: break; case TargetPlatform.windows: handleDidGainAccessibilityFocus = () { // Automatically activate the slider when it receives a11y focus. if (!focusNode.hasFocus && focusNode.canRequestFocus) { focusNode.requestFocus(); } }; } final Map shortcutMap = switch (MediaQuery.navigationModeOf( context, )) { NavigationMode.directional => _directionalNavShortcutMap, NavigationMode.traditional => _traditionalNavShortcutMap, }; final double fontSize = sliderTheme.valueIndicatorTextStyle?.fontSize ?? kDefaultFontSize; final double fontSizeToScale = fontSize == 0.0 ? kDefaultFontSize : fontSize; final TextScaler textScaler = theme.useMaterial3 // TODO(tahatesser): This is an eye-balled value. // This needs to be updated when accessibility // guidelines are available on the material specs page // https://m3.material.io/components/sliders/accessibility. ? MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.3) : MediaQuery.textScalerOf(context); final double effectiveTextScale = textScaler.scale(fontSizeToScale) / fontSizeToScale; Widget result = CompositedTransformTarget( link: _layerLink, child: _SliderRenderObjectWidget( key: _renderObjectKey, value: _convert(widget.value), secondaryTrackValue: (widget.secondaryTrackValue != null) ? _convert(widget.secondaryTrackValue!) : null, divisions: widget.divisions, label: widget.label, sliderTheme: sliderTheme, textScaleFactor: effectiveTextScale, screenSize: screenSize(), onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, onChangeStart: _handleDragStart, onChangeEnd: _handleDragEnd, state: this, semanticFormatterCallback: widget.semanticFormatterCallback, hasFocus: _focused, hovering: _hovering, allowedInteraction: effectiveAllowedInteraction, ), ); final EdgeInsetsGeometry? padding = widget.padding ?? sliderTheme.padding; if (padding != null) { result = Padding(padding: padding, child: result); } result = OverlayPortal( controller: _valueIndicatorOverlayPortalController, overlayChildBuilder: (BuildContext context) { return _buildValueIndicator(sliderTheme.showValueIndicator!); }, child: result, ); return Semantics( label: widget.label, container: true, slider: true, onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, child: FocusableActionDetector( actions: _actionMap, shortcuts: shortcutMap, focusNode: focusNode, autofocus: widget.autofocus, enabled: _enabled, onShowFocusHighlight: _handleFocusHighlightChanged, onShowHoverHighlight: _handleHoverChanged, mouseCursor: effectiveMouseCursor, child: result, ), ); } Widget _buildCupertinoSlider(BuildContext context) { // The render box of a slider has a fixed height but takes up the available // width. Wrapping the [CupertinoSlider] in this manner will help maintain // the same size. return SizedBox( width: double.infinity, child: CupertinoSlider( value: widget.value, onChanged: widget.onChanged, onChangeStart: widget.onChangeStart, onChangeEnd: widget.onChangeEnd, min: widget.min, max: widget.max, divisions: widget.divisions, activeColor: widget.activeColor, thumbColor: widget.thumbColor ?? CupertinoColors.white, ), ); } final LayerLink _layerLink = LayerLink(); Widget _buildValueIndicator(ShowValueIndicator showValueIndicator) { final Widget valueIndicator = CompositedTransformFollower( link: _layerLink, child: _ValueIndicatorRenderObjectWidget(state: this), ); return switch (showValueIndicator) { ShowValueIndicator.never => const SizedBox.shrink(), ShowValueIndicator.onlyForDiscrete => widget.divisions != null ? valueIndicator : const SizedBox.shrink(), ShowValueIndicator.onlyForContinuous => widget.divisions == null ? valueIndicator : const SizedBox.shrink(), ShowValueIndicator.alwaysVisible || // ignore: deprecated_member_use ShowValueIndicator.always || ShowValueIndicator.onDrag => valueIndicator, }; } } class _SliderRenderObjectWidget extends LeafRenderObjectWidget { const _SliderRenderObjectWidget({ super.key, required this.value, required this.secondaryTrackValue, required this.divisions, required this.label, required this.sliderTheme, required this.textScaleFactor, required this.screenSize, required this.onChanged, required this.onChangeStart, required this.onChangeEnd, required this.state, required this.semanticFormatterCallback, required this.hasFocus, required this.hovering, required this.allowedInteraction, }); final double value; final double? secondaryTrackValue; final int? divisions; final String? label; final SliderThemeData sliderTheme; final double textScaleFactor; final Size screenSize; final ValueChanged? onChanged; final ValueChanged? onChangeStart; final ValueChanged? onChangeEnd; final SemanticFormatterCallback? semanticFormatterCallback; final _VerticalSliderState state; final bool hasFocus; final bool hovering; final SliderInteraction allowedInteraction; @override _RenderSlider createRenderObject(BuildContext context) { return _RenderSlider( value: value, secondaryTrackValue: secondaryTrackValue, divisions: divisions, label: label, sliderTheme: sliderTheme, textScaleFactor: textScaleFactor, screenSize: screenSize, onChanged: onChanged, onChangeStart: onChangeStart, onChangeEnd: onChangeEnd, state: state, textDirection: Directionality.of(context), semanticFormatterCallback: semanticFormatterCallback, platform: Theme.of(context).platform, hasFocus: hasFocus, hovering: hovering, gestureSettings: MediaQuery.gestureSettingsOf(context), allowedInteraction: allowedInteraction, ); } @override void updateRenderObject(BuildContext context, _RenderSlider renderObject) { renderObject // We should update the `divisions` ahead of `value`, because the `value` // setter dependent on the `divisions`. ..divisions = divisions ..value = value ..secondaryTrackValue = secondaryTrackValue ..label = label ..sliderTheme = sliderTheme ..textScaleFactor = textScaleFactor ..screenSize = screenSize ..onChanged = onChanged ..onChangeStart = onChangeStart ..onChangeEnd = onChangeEnd ..textDirection = Directionality.of(context) ..semanticFormatterCallback = semanticFormatterCallback ..platform = Theme.of(context).platform ..hasFocus = hasFocus ..hovering = hovering ..gestureSettings = MediaQuery.gestureSettingsOf(context) ..allowedInteraction = allowedInteraction; // Ticker provider cannot change since there's a 1:1 relationship between // the _SliderRenderObjectWidget object and the _SliderState object. } } class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { _RenderSlider({ required double value, required double? secondaryTrackValue, required int? divisions, required String? label, required SliderThemeData sliderTheme, required double textScaleFactor, required Size screenSize, required TargetPlatform platform, required ValueChanged? onChanged, required SemanticFormatterCallback? semanticFormatterCallback, required this.onChangeStart, required this.onChangeEnd, required _VerticalSliderState state, required TextDirection textDirection, required bool hasFocus, required bool hovering, required DeviceGestureSettings gestureSettings, required SliderInteraction allowedInteraction, }) : assert(value >= 0.0 && value <= 1.0), assert( secondaryTrackValue == null || (secondaryTrackValue >= 0.0 && secondaryTrackValue <= 1.0), ), _platform = platform, _semanticFormatterCallback = semanticFormatterCallback, _label = label, _value = value, _secondaryTrackValue = secondaryTrackValue, _divisions = divisions, _sliderTheme = sliderTheme, _textScaleFactor = textScaleFactor, _screenSize = screenSize, _onChanged = onChanged, _state = state, _textDirection = textDirection, _hasFocus = hasFocus, _hovering = hovering, _allowedInteraction = allowedInteraction { _updateLabelPainter(); final team = GestureArenaTeam(); _drag = VerticalDragGestureRecognizer() ..team = team ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd ..onCancel = _endInteraction ..gestureSettings = gestureSettings; _tap = TapGestureRecognizer() ..team = team ..onTapDown = _handleTapDown ..onTapUp = _handleTapUp ..gestureSettings = gestureSettings; _overlayAnimation = CurvedAnimation( parent: _state.overlayController, curve: Curves.fastOutSlowIn, ); _valueIndicatorAnimation = CurvedAnimation( parent: _state.valueIndicatorController, curve: Curves.fastOutSlowIn, ); _enableAnimation = CurvedAnimation( parent: _state.enableController, curve: Curves.easeInOut, ); } static const Duration _positionAnimationDuration = Duration(milliseconds: 75); static const Duration _minimumInteractionTime = Duration(milliseconds: 500); // This value is the touch target, 48, multiplied by 3. static const double _minPreferredTrackWidth = 144.0; // Compute the largest width and height needed to paint the slider shapes, // other than the track shape. It is assumed that these shapes are vertically // centered on the track. double get _maxSliderPartWidth => _sliderPartSizes.map((Size size) => size.width).reduce(math.max); double get _maxSliderPartHeight => _sliderPartSizes.map((Size size) => size.height).reduce(math.max); double get _thumbSizeHeight => _sliderTheme.thumbShape! .getPreferredSize(isInteractive, isDiscrete) .height; double get _overlayHeight => _sliderTheme.overlayShape! .getPreferredSize(isInteractive, isDiscrete) .height; List get _sliderPartSizes => [ Size( _sliderTheme.overlayShape! .getPreferredSize(isInteractive, isDiscrete) .width, _sliderTheme.padding != null ? _thumbSizeHeight : _overlayHeight, ), _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete), _sliderTheme.tickMarkShape!.getPreferredSize( isEnabled: isInteractive, sliderTheme: sliderTheme, ), ]; double get _minPreferredTrackHeight => _sliderTheme.trackHeight!; final _VerticalSliderState _state; late CurvedAnimation _overlayAnimation; late CurvedAnimation _valueIndicatorAnimation; late CurvedAnimation _enableAnimation; final TextPainter _labelPainter = TextPainter(); late VerticalDragGestureRecognizer _drag; late TapGestureRecognizer _tap; bool _active = false; double _currentDragValue = 0.0; Rect? overlayRect; // This rect is used in gesture calculations, where the gesture coordinates // are relative to the sliders origin. Therefore, the offset is passed as // (0,0). Rect get _trackRect => _sliderTheme.trackShape!.getPreferredRect( parentBox: this, sliderTheme: _sliderTheme, isDiscrete: false, ); bool get isInteractive => onChanged != null; bool get isDiscrete => false; // divisions != null && divisions! > 0; double get value => _value; double _value; set value(double newValue) { assert(newValue >= 0.0 && newValue <= 1.0); final double convertedValue = isDiscrete ? _discretize(newValue) : newValue; if (convertedValue == _value) { return; } _value = convertedValue; if (isDiscrete) { // Reset the duration to match the distance that we're traveling, so that // whatever the distance, we still do it in _positionAnimationDuration, // and if we get re-targeted in the middle, it still takes that long to // get to the new location. final double distance = (_value - _state.positionController.value).abs(); _state.positionController.duration = distance != 0.0 ? _positionAnimationDuration * (1.0 / distance) : Duration.zero; _state.positionController.animateTo( convertedValue, curve: Curves.easeInOut, ); } else { _state.positionController.value = convertedValue; } markNeedsSemanticsUpdate(); } double? get secondaryTrackValue => _secondaryTrackValue; double? _secondaryTrackValue; set secondaryTrackValue(double? newValue) { assert(newValue == null || (newValue >= 0.0 && newValue <= 1.0)); if (newValue == _secondaryTrackValue) { return; } _secondaryTrackValue = newValue; markNeedsPaint(); markNeedsSemanticsUpdate(); } DeviceGestureSettings? get gestureSettings => _drag.gestureSettings; set gestureSettings(DeviceGestureSettings? gestureSettings) { _drag.gestureSettings = gestureSettings; _tap.gestureSettings = gestureSettings; } TargetPlatform _platform; TargetPlatform get platform => _platform; set platform(TargetPlatform value) { if (_platform == value) { return; } _platform = value; markNeedsSemanticsUpdate(); } SemanticFormatterCallback? _semanticFormatterCallback; SemanticFormatterCallback? get semanticFormatterCallback => _semanticFormatterCallback; set semanticFormatterCallback(SemanticFormatterCallback? value) { if (_semanticFormatterCallback == value) { return; } _semanticFormatterCallback = value; markNeedsSemanticsUpdate(); } int? get divisions => _divisions; int? _divisions; set divisions(int? value) { if (value == _divisions) { return; } _divisions = value; markNeedsPaint(); } String? get label => _label; String? _label; set label(String? value) { if (value == _label) { return; } _label = value; _updateLabelPainter(); } SliderThemeData get sliderTheme => _sliderTheme; SliderThemeData _sliderTheme; set sliderTheme(SliderThemeData value) { if (value == _sliderTheme) { return; } _sliderTheme = value; _updateLabelPainter(); } double get textScaleFactor => _textScaleFactor; double _textScaleFactor; set textScaleFactor(double value) { if (value == _textScaleFactor) { return; } _textScaleFactor = value; _updateLabelPainter(); } Size get screenSize => _screenSize; Size _screenSize; set screenSize(Size value) { if (value == _screenSize) { return; } _screenSize = value; markNeedsPaint(); } ValueChanged? get onChanged => _onChanged; ValueChanged? _onChanged; set onChanged(ValueChanged? value) { if (value == _onChanged) { return; } final bool wasInteractive = isInteractive; _onChanged = value; if (wasInteractive != isInteractive) { if (isInteractive) { _state.enableController.forward(); } else { _state.enableController.reverse(); } markNeedsPaint(); markNeedsSemanticsUpdate(); } } ValueChanged? onChangeStart; ValueChanged? onChangeEnd; TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { if (value == _textDirection) { return; } _textDirection = value; _updateLabelPainter(); } /// True if this slider has the input focus. bool get hasFocus => _hasFocus; bool _hasFocus; set hasFocus(bool value) { if (value == _hasFocus) { return; } _hasFocus = value; _updateForFocus(_hasFocus); markNeedsSemanticsUpdate(); } /// True if this slider is being hovered over by a pointer. bool get hovering => _hovering; bool _hovering; set hovering(bool value) { if (value == _hovering) { return; } _hovering = value; _updateForHover(_hovering); } /// True if the slider is interactive and the slider thumb is being /// hovered over by a pointer. bool _hoveringThumb = false; bool get hoveringThumb => _hoveringThumb; set hoveringThumb(bool value) { if (value == _hoveringThumb) { return; } _hoveringThumb = value; _updateForHover(_hovering); } SliderInteraction _allowedInteraction; SliderInteraction get allowedInteraction => _allowedInteraction; set allowedInteraction(SliderInteraction value) { if (value == _allowedInteraction) { return; } _allowedInteraction = value; markNeedsSemanticsUpdate(); } void _updateForFocus(bool focused) { if (focused) { _state.overlayController.forward(); if (shouldShowValueIndicatorWhenDragged) { _state.valueIndicatorController.forward(); } } else { _state.overlayController.reverse(); if (shouldShowValueIndicatorWhenDragged) { _state.valueIndicatorController.reverse(); } } } void _updateForHover(bool hovered) { // Only show overlay when pointer is hovering the thumb. if (hovered && hoveringThumb) { _state.overlayController.forward(); } else { // Only remove overlay when Slider is inactive and unfocused. if (!_active && !hasFocus) { _state.overlayController.reverse(); } } } bool get shouldAlwaysShowValueIndicator => _sliderTheme.showValueIndicator == ShowValueIndicator.alwaysVisible; bool get shouldShowValueIndicatorWhenDragged => switch (_sliderTheme.showValueIndicator!) { ShowValueIndicator.onlyForDiscrete => isDiscrete, ShowValueIndicator.onlyForContinuous => !isDiscrete, // ignore: deprecated_member_use ShowValueIndicator.always || ShowValueIndicator.onDrag => true, ShowValueIndicator.never || ShowValueIndicator.alwaysVisible => false, }; double get _adjustmentUnit { switch (_platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: // Matches iOS implementation of material slider. return 0.1; case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: // Matches Android implementation of material slider. return 0.05; } } void _updateLabelPainter() { if (label != null) { _labelPainter ..text = TextSpan( style: _sliderTheme.valueIndicatorTextStyle, text: label, ) ..textDirection = textDirection // ignore: deprecated_member_use ..textScaleFactor = textScaleFactor ..layout(); } else { _labelPainter.text = null; } // Changing the textDirection can result in the layout changing, because the // bidi algorithm might line up the glyphs differently which can result in // different ligatures, different shapes, etc. So we always markNeedsLayout. markNeedsLayout(); } @override void systemFontsDidChange() { super.systemFontsDidChange(); _labelPainter.markNeedsLayout(); _updateLabelPainter(); } @override void attach(PipelineOwner owner) { super.attach(owner); _overlayAnimation.addListener(markNeedsPaint); _valueIndicatorAnimation.addListener(markNeedsPaint); _enableAnimation.addListener(markNeedsPaint); _state.positionController.addListener(markNeedsPaint); } @override void detach() { _overlayAnimation.removeListener(markNeedsPaint); _valueIndicatorAnimation.removeListener(markNeedsPaint); _enableAnimation.removeListener(markNeedsPaint); _state.positionController.removeListener(markNeedsPaint); super.detach(); } @override void dispose() { _drag.dispose(); _tap.dispose(); _labelPainter.dispose(); _enableAnimation.dispose(); _valueIndicatorAnimation.dispose(); _overlayAnimation.dispose(); super.dispose(); } double _getValueFromVisualPosition(double visualPosition) { return switch (textDirection) { TextDirection.rtl => 1.0 - visualPosition, TextDirection.ltr => visualPosition, }; } double _getValueFromGlobalPosition(Offset globalPosition) { final double visualPosition = (_trackRect.bottom - globalToLocal(globalPosition).dy) / _trackRect.height; return _getValueFromVisualPosition(visualPosition); } double _discretize(double value) { double result = clampDouble(value, 0.0, 1.0); if (isDiscrete) { result = (result * divisions!).round() / divisions!; } return result; } void _startInteraction(Offset globalPosition) { if (!_state.mounted) { return; } if (!_active && isInteractive) { switch (allowedInteraction) { case SliderInteraction.tapAndSlide: case SliderInteraction.tapOnly: _active = true; _currentDragValue = _getValueFromGlobalPosition(globalPosition); case SliderInteraction.slideThumb: if (_isPointerOnOverlay(globalPosition)) { _active = true; _currentDragValue = value; } case SliderInteraction.slideOnly: _active = true; _currentDragValue = value; } if (_active) { // We supply the *current* value as the start location, so that if we have // a tap, it consists of a call to onChangeStart with the previous value and // a call to onChangeEnd with the new value. onChangeStart?.call(_discretize(value)); onChanged!(_discretize(_currentDragValue)); _state.overlayController.forward(); if (shouldShowValueIndicatorWhenDragged) { _state.valueIndicatorController.forward(); _state.interactionTimer?.cancel(); _state.interactionTimer = Timer( _minimumInteractionTime * timeDilation, () { _state.interactionTimer = null; if (!_active && _state.valueIndicatorController.isCompleted) { _state.valueIndicatorController.reverse(); } }, ); } } } } void _endInteraction() { if (!_state.mounted) { return; } if (_active && _state.mounted) { onChangeEnd?.call(_discretize(_currentDragValue)); _active = false; _currentDragValue = 0.0; _state.overlayController.reverse(); if (shouldShowValueIndicatorWhenDragged && _state.interactionTimer == null) { _state.valueIndicatorController.reverse(); } } } void _handleDragStart(DragStartDetails details) { _startInteraction(details.globalPosition); } void _handleDragUpdate(DragUpdateDetails details) { if (!_state.mounted) { return; } switch (allowedInteraction) { case SliderInteraction.tapAndSlide: case SliderInteraction.slideOnly: case SliderInteraction.slideThumb: if (_active && isInteractive) { final double valueDelta = details.primaryDelta! / _trackRect.height; _currentDragValue -= valueDelta; onChanged!(_discretize(_currentDragValue)); } case SliderInteraction.tapOnly: // cannot slide (drag) as its tapOnly. break; } } void _handleDragEnd(DragEndDetails details) { _endInteraction(); } void _handleTapDown(TapDownDetails details) { _startInteraction(details.globalPosition); } void _handleTapUp(TapUpDetails details) { _endInteraction(); } bool _isPointerOnOverlay(Offset globalPosition) { return overlayRect!.contains(globalToLocal(globalPosition)); } @override bool hitTestSelf(Offset position) => true; @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { if (!_state.mounted) { return; } assert(debugHandleEvent(event, entry)); if (event is PointerDownEvent && isInteractive) { // We need to add the drag first so that it has priority. _drag.addPointer(event); _tap.addPointer(event); } if (isInteractive && overlayRect != null) { hoveringThumb = overlayRect!.contains(event.localPosition); } } @override double computeMinIntrinsicWidth(double height) => _minPreferredTrackWidth + _maxSliderPartWidth; @override double computeMaxIntrinsicWidth(double height) => _minPreferredTrackWidth + _maxSliderPartWidth; @override double computeMinIntrinsicHeight(double width) => math.max(_minPreferredTrackHeight, _maxSliderPartHeight); @override double computeMaxIntrinsicHeight(double width) => math.max(_minPreferredTrackHeight, _maxSliderPartHeight); @override bool get sizedByParent => true; @override Size computeDryLayout(BoxConstraints constraints) { return Size( constraints.hasBoundedWidth ? constraints.maxWidth : _minPreferredTrackWidth + _maxSliderPartWidth, constraints.hasBoundedHeight ? constraints.maxHeight : math.max(_minPreferredTrackHeight, _maxSliderPartHeight), ); } @override void paint(PaintingContext context, Offset offset) { final double controllerValue = _state.positionController.value; // The visual position is the position of the thumb from 0 to 1 from left // to right. In left to right, this is the same as the value, but it is // reversed for right to left text. final ( double visualPosition, double? secondaryVisualPosition, ) = switch (textDirection) { TextDirection.rtl when _secondaryTrackValue == null => ( 1.0 - controllerValue, null, ), TextDirection.rtl => (1.0 - controllerValue, 1.0 - _secondaryTrackValue!), TextDirection.ltr => (controllerValue, _secondaryTrackValue), }; final Rect trackRect = _sliderTheme.trackShape!.getPreferredRect( parentBox: this, offset: offset, sliderTheme: _sliderTheme, isDiscrete: isDiscrete, ); final double padding = _sliderTheme.trackShape!.isRounded ? trackRect.width : 0.0; final double thumbPosition = isDiscrete ? trackRect.left + visualPosition * (trackRect.width - padding) + padding / 2 : trackRect.bottom - visualPosition * trackRect.height; // 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 thumbPreferredSize = _sliderTheme.thumbShape!.getPreferredSize( isInteractive, isDiscrete, ); final double thumbPadding = (padding > thumbPreferredSize.width / 2 ? padding / 2 : 0); final thumbCenter = Offset( trackRect.center.dx, clampDouble( thumbPosition, trackRect.top + thumbPadding, trackRect.bottom - thumbPadding, ), ); if (isInteractive) { 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; // 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({}) ?.width; final double? thumbHeight = _sliderTheme.thumbSize ?.resolve({}) ?.height; double? trackGap = _sliderTheme.trackGap; final double? pressedThumbWidth = _sliderTheme.thumbSize?.resolve( { WidgetState.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.copyWith(trackGap: trackGap), enableAnimation: _enableAnimation, textDirection: _textDirection, thumbCenter: thumbCenter, secondaryOffset: secondaryOffset, isDiscrete: isDiscrete, isEnabled: isInteractive, ); if (!_overlayAnimation.isDismissed) { _sliderTheme.overlayShape!.paint( context, thumbCenter, activationAnimation: _overlayAnimation, enableAnimation: _enableAnimation, isDiscrete: isDiscrete, labelPainter: _labelPainter, parentBox: this, sliderTheme: _sliderTheme, textDirection: _textDirection, value: _value, textScaleFactor: _textScaleFactor, sizeWithOverflow: screenSize.isEmpty ? size : screenSize, ); } if (isDiscrete) { final double tickMarkWidth = _sliderTheme.tickMarkShape! .getPreferredSize(isEnabled: isInteractive, sliderTheme: _sliderTheme) .width; final double discreteTrackPadding = trackRect.height; final double adjustedTrackWidth = trackRect.width - discreteTrackPadding; // If the tick marks would be too dense, don't bother painting them. if (adjustedTrackWidth / divisions! >= 3.0 * tickMarkWidth) { final double dy = trackRect.center.dy; for (var i = 0; i <= divisions!; i++) { final double value = i / divisions!; // The ticks are mapped to be within the track, so the tick mark width // must be subtracted from the track width. final double dx = trackRect.left + value * adjustedTrackWidth + discreteTrackPadding / 2; final tickMarkOffset = Offset(dx, dy); _sliderTheme.tickMarkShape!.paint( context, tickMarkOffset, parentBox: this, sliderTheme: _sliderTheme, enableAnimation: _enableAnimation, textDirection: _textDirection, thumbCenter: thumbCenter, isEnabled: isInteractive, ); } } } if (isInteractive && label != null) { if ((shouldShowValueIndicatorWhenDragged && !_valueIndicatorAnimation.isDismissed) || shouldAlwaysShowValueIndicator) { _state.paintValueIndicator = (PaintingContext context, Offset offset) { if (attached && _labelPainter.text != null) { _sliderTheme.valueIndicatorShape!.paint( context, offset + thumbCenter, activationAnimation: shouldAlwaysShowValueIndicator ? const AlwaysStoppedAnimation(1) : _valueIndicatorAnimation, enableAnimation: shouldAlwaysShowValueIndicator ? const AlwaysStoppedAnimation(1) : _enableAnimation, isDiscrete: isDiscrete, labelPainter: _labelPainter, parentBox: this, sliderTheme: _sliderTheme, textDirection: _textDirection, value: _value, textScaleFactor: textScaleFactor, sizeWithOverflow: screenSize.isEmpty ? size : screenSize, ); } }; } } _sliderTheme.thumbShape!.paint( context, thumbCenter, activationAnimation: _overlayAnimation, enableAnimation: _enableAnimation, isDiscrete: isDiscrete, labelPainter: _labelPainter, parentBox: this, sliderTheme: thumbWidth != null && thumbHeight != null ? _sliderTheme.copyWith( thumbSize: WidgetStatePropertyAll( Size(thumbWidth, thumbHeight), ), ) : _sliderTheme, textDirection: _textDirection, value: _value, textScaleFactor: textScaleFactor, sizeWithOverflow: screenSize.isEmpty ? size : screenSize, ); } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); // The Slider widget has its own Focus widget with semantics information, // and we want that semantics node to collect the semantics information here // so that it's all in the same node: otherwise Talkback sees that the node // has focusable children, and it won't focus the Slider's Focus widget // because it thinks the Focus widget's node doesn't have anything to say // (which it doesn't, but this child does). Aggregating the semantic // information into one node means that Talkback will recognize that it has // something to say and focus it when it receives keyboard focus. // (See https://github.com/flutter/flutter/issues/57038 for context). config ..isSemanticBoundary = false ..isEnabled = isInteractive ..textDirection = textDirection; if (isInteractive) { config ..onIncrease = increaseAction ..onDecrease = decreaseAction; } if (semanticFormatterCallback != null) { config ..value = semanticFormatterCallback!(_state._lerp(value)) ..increasedValue = semanticFormatterCallback!( _state._lerp(clampDouble(value + _semanticActionUnit, 0.0, 1.0)), ) ..decreasedValue = semanticFormatterCallback!( _state._lerp(clampDouble(value - _semanticActionUnit, 0.0, 1.0)), ); } else { config ..value = '${(value * 100).round()}%' ..increasedValue = '${(clampDouble(value + _semanticActionUnit, 0.0, 1.0) * 100).round()}%' ..decreasedValue = '${(clampDouble(value - _semanticActionUnit, 0.0, 1.0) * 100).round()}%'; } } double get _semanticActionUnit => divisions != null ? 1.0 / divisions! : _adjustmentUnit; void increaseAction() { if (isInteractive) { onChangeStart!(currentValue); final double increase = increaseValue(); onChanged!(increase); onChangeEnd!(increase); if (!_state.mounted) { return; } } } void decreaseAction() { if (isInteractive) { onChangeStart!(currentValue); final double decrease = decreaseValue(); onChanged!(decrease); onChangeEnd!(decrease); if (!_state.mounted) { return; } } } double get currentValue { return clampDouble(value, 0.0, 1.0); } double increaseValue() { return clampDouble(value + _semanticActionUnit, 0.0, 1.0); } double decreaseValue() { return clampDouble(value - _semanticActionUnit, 0.0, 1.0); } } class _AdjustSliderIntent extends Intent { const _AdjustSliderIntent({required this.type}); const _AdjustSliderIntent.right() : type = _SliderAdjustmentType.right; const _AdjustSliderIntent.left() : type = _SliderAdjustmentType.left; const _AdjustSliderIntent.up() : type = _SliderAdjustmentType.up; const _AdjustSliderIntent.down() : type = _SliderAdjustmentType.down; final _SliderAdjustmentType type; } enum _SliderAdjustmentType { right, left, up, down } class _ValueIndicatorRenderObjectWidget extends LeafRenderObjectWidget { const _ValueIndicatorRenderObjectWidget({required this.state}); final _VerticalSliderState state; @override _RenderValueIndicator createRenderObject(BuildContext context) { return _RenderValueIndicator(state: state); } @override void updateRenderObject( BuildContext context, _RenderValueIndicator renderObject, ) { renderObject._state = state; } } class _RenderValueIndicator extends RenderBox with RelayoutWhenSystemFontsChangeMixin { _RenderValueIndicator({required _VerticalSliderState state}) : _state = state { _valueIndicatorAnimation = CurvedAnimation( parent: _state.valueIndicatorController, curve: Curves.fastOutSlowIn, ); } late CurvedAnimation _valueIndicatorAnimation; _VerticalSliderState _state; @override bool get sizedByParent => true; @override void attach(PipelineOwner owner) { super.attach(owner); _valueIndicatorAnimation.addListener(markNeedsPaint); _state.positionController.addListener(markNeedsPaint); } @override void detach() { _valueIndicatorAnimation.removeListener(markNeedsPaint); _state.positionController.removeListener(markNeedsPaint); super.detach(); } @override void paint(PaintingContext context, Offset offset) { _state.paintValueIndicator?.call(context, offset); } @override Size computeDryLayout(BoxConstraints constraints) { return constraints.smallest; } @override void dispose() { _valueIndicatorAnimation.dispose(); super.dispose(); } } class _SliderDefaultsM2 extends SliderThemeData { _SliderDefaultsM2(this.context) : super(trackHeight: 4.0); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; late final SliderThemeData sliderTheme = SliderTheme.of(context); @override Color? get activeTrackColor => _colors.primary; @override Color? get inactiveTrackColor => _colors.primary.withValues(alpha: 0.24); @override Color? get secondaryActiveTrackColor => _colors.primary.withValues(alpha: 0.54); @override Color? get disabledActiveTrackColor => _colors.onSurface.withValues(alpha: 0.32); @override Color? get disabledInactiveTrackColor => _colors.onSurface.withValues(alpha: 0.12); @override Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withValues(alpha: 0.12); @override Color? get activeTickMarkColor => _colors.onPrimary.withValues(alpha: 0.54); @override Color? get inactiveTickMarkColor => _colors.primary.withValues(alpha: 0.54); @override Color? get disabledActiveTickMarkColor => _colors.onPrimary.withValues(alpha: 0.12); @override Color? get disabledInactiveTickMarkColor => _colors.onSurface.withValues(alpha: 0.12); @override Color? get thumbColor => _colors.primary; @override Color? get disabledThumbColor => Color.alphaBlend( _colors.onSurface.withValues(alpha: .38), _colors.surface, ); @override Color? get overlayColor => _colors.primary.withValues(alpha: 0.12); @override TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.bodyLarge!.copyWith(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(); } class _SliderDefaultsM3Year2023 extends SliderThemeData { _SliderDefaultsM3Year2023(this.context) : super(trackHeight: 4.0); final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; @override Color? get activeTrackColor => _colors.primary; @override Color? get inactiveTrackColor => _colors.surfaceContainerHighest; @override Color? get secondaryActiveTrackColor => _colors.primary.withValues(alpha: 0.54); @override Color? get disabledActiveTrackColor => _colors.onSurface.withValues(alpha: 0.38); @override Color? get disabledInactiveTrackColor => _colors.onSurface.withValues(alpha: 0.12); @override Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withValues(alpha: 0.12); @override Color? get activeTickMarkColor => _colors.onPrimary.withValues(alpha: 0.38); @override Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.withValues(alpha: 0.38); @override Color? get disabledActiveTickMarkColor => _colors.onSurface.withValues(alpha: 0.38); @override Color? get disabledInactiveTickMarkColor => _colors.onSurface.withValues(alpha: 0.38); @override Color? get thumbColor => _colors.primary; @override Color? get disabledThumbColor => Color.alphaBlend( _colors.onSurface.withValues(alpha: 0.38), _colors.surface, ); @override Color? get overlayColor => WidgetStateColor.resolveWith((Set states) { if (states.contains(WidgetState.dragged)) { return _colors.primary.withValues(alpha: 0.1); } if (states.contains(WidgetState.hovered)) { return _colors.primary.withValues(alpha: 0.08); } if (states.contains(WidgetState.focused)) { return _colors.primary.withValues(alpha: 0.1); } return Colors.transparent; }); @override TextStyle? get valueIndicatorTextStyle => Theme.of( context, ).textTheme.labelMedium!.copyWith(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. // dart format off 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.withValues(alpha: 0.54); @override Color? get disabledActiveTrackColor => _colors.onSurface.withValues(alpha: 0.38); @override Color? get disabledInactiveTrackColor => _colors.onSurface.withValues(alpha: 0.12); @override Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withValues(alpha: 0.38); @override Color? get activeTickMarkColor => _colors.onPrimary.withValues(alpha: 1.0); @override Color? get inactiveTickMarkColor => _colors.onSecondaryContainer.withValues(alpha: 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.withValues(alpha: 0.38); @override Color? get overlayColor => WidgetStateColor.resolveWith((Set states) { if (states.contains(WidgetState.dragged)) { return _colors.primary.withValues(alpha: 0.1); } if (states.contains(WidgetState.hovered)) { return _colors.primary.withValues(alpha: 0.08); } if (states.contains(WidgetState.focused)) { return _colors.primary.withValues(alpha: 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 WidgetStateProperty? get thumbSize { return WidgetStateProperty.resolveWith((Set states) { if (states.contains(WidgetState.disabled)) { return const Size(4.0, 44.0); } if (states.contains(WidgetState.hovered)) { return const Size(4.0, 44.0); } if (states.contains(WidgetState.focused)) { return const Size(2.0, 44.0); } if (states.contains(WidgetState.pressed)) { return const Size(2.0, 44.0); } return const Size(4.0, 44.0); }); } @override double? get trackGap => 6.0; } // dart format on // END GENERATED TOKEN PROPERTIES - Slider class RoundedRectSliderTrackShape extends SliderTrackShape { /// Create a slider track that draws two rectangles with rounded outer edges. const RoundedRectSliderTrackShape(); @override Rect getPreferredRect({ required RenderBox parentBox, Offset offset = Offset.zero, required SliderThemeData sliderTheme, bool isEnabled = false, bool isDiscrete = false, }) { // final double thumbHeight = sliderTheme.thumbShape! // .getPreferredSize(isEnabled, isDiscrete) // .height; final double overlayHight = sliderTheme.overlayShape! .getPreferredSize(isEnabled, isDiscrete) .height; double trackWidth = sliderTheme.trackHeight!; assert(overlayHight >= 0); assert(trackWidth >= 0); // If the track colors are transparent, then override only the track height // to maintain overall Slider width. if (sliderTheme.activeTrackColor == Colors.transparent && sliderTheme.inactiveTrackColor == Colors.transparent) { trackWidth = 0; } final double trackLeft = offset.dx + (parentBox.size.width - trackWidth) / 2; final double trackTop = offset.dy + 10; // padding // (sliderTheme.padding == null // ? math.max(overlayHight / 2, thumbHeight / 2) // : 0); final double trackRight = trackLeft + trackWidth; final double trackBottom = trackTop + parentBox.size.height - 20; // (sliderTheme.padding == null ? math.max(thumbHeight, overlayHight) : 0); // If the parentBox's size less than slider's size the trackRight will be less than trackLeft, so switch them. return Rect.fromLTRB( trackLeft, math.min(trackTop, trackBottom), trackRight, math.max(trackTop, trackBottom), ); } @override void paint( PaintingContext context, Offset offset, { required RenderBox parentBox, required SliderThemeData sliderTheme, required Animation 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); // 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 leading: active and // trailing: inactive. final activeTrackColorTween = ColorTween( begin: sliderTheme.disabledActiveTrackColor, end: sliderTheme.activeTrackColor, ); final inactiveTrackColorTween = ColorTween( begin: sliderTheme.disabledInactiveTrackColor, end: sliderTheme.inactiveTrackColor, ); final activePaint = Paint() ..color = activeTrackColorTween.evaluate(enableAnimation)!; final inactivePaint = Paint() ..color = inactiveTrackColorTween.evaluate(enableAnimation)!; final (Paint leftTrackPaint, Paint rightTrackPaint) = ( activePaint, inactivePaint, ); final Rect trackRect = getPreferredRect( parentBox: parentBox, offset: offset, sliderTheme: sliderTheme, isEnabled: isEnabled, isDiscrete: isDiscrete, ); final trackRadius = Radius.circular(trackRect.height / 2); final activeTrackRadius = Radius.circular( (trackRect.height + additionalActiveTrackHeight) / 2, ); final bool drawInactiveTrack = thumbCenter.dy > (trackRect.top - (sliderTheme.trackHeight! / 2)); if (drawInactiveTrack) { // Draw the inactive track segment. context.canvas.drawRRect( RRect.fromLTRBR( trackRect.left, trackRect.top, trackRect.right, thumbCenter.dy - (sliderTheme.trackHeight! / 2), trackRadius, ), rightTrackPaint, ); } final bool drawActiveTrack = thumbCenter.dy < (trackRect.bottom - (sliderTheme.trackHeight! / 2)); if (drawActiveTrack) { // Draw the active track segment. context.canvas.drawRRect( RRect.fromLTRBR( trackRect.left, thumbCenter.dy + (sliderTheme.trackHeight! / 2), trackRect.right, trackRect.bottom, activeTrackRadius, ), leftTrackPaint, ); } // final bool showSecondaryTrack = // (secondaryOffset != null) && (secondaryOffset.dx > thumbCenter.dx); // if (showSecondaryTrack) { // final secondaryTrackColorTween = ColorTween( // begin: sliderTheme.disabledSecondaryActiveTrackColor, // end: sliderTheme.secondaryActiveTrackColor, // ); // final secondaryTrackPaint = Paint() // ..color = secondaryTrackColorTween.evaluate(enableAnimation)!; // context.canvas.drawRRect( // RRect.fromLTRBAndCorners( // thumbCenter.dx, // trackRect.top, // secondaryOffset.dx, // trackRect.bottom, // topRight: trackRadius, // bottomRight: trackRadius, // ), // secondaryTrackPaint, // ); // } } @override bool get isRounded => true; }