@@ -14,6 +14,7 @@ import 'package:flutter/widgets.dart';
1414
1515import 'constants.dart' ;
1616import 'debug.dart' ;
17+ import 'material_state.dart' ;
1718import 'slider_theme.dart' ;
1819import 'theme.dart' ;
1920
@@ -144,6 +145,8 @@ class RangeSlider extends StatefulWidget {
144145 this .labels,
145146 this .activeColor,
146147 this .inactiveColor,
148+ this .overlayColor,
149+ this .mouseCursor,
147150 this .semanticFormatterCallback,
148151 }) : assert (values != null ),
149152 assert (min != null ),
@@ -322,6 +325,26 @@ class RangeSlider extends StatefulWidget {
322325 /// appearance of various components of the slider.
323326 final Color ? inactiveColor;
324327
328+ /// The highlight color that's typically used to indicate that
329+ /// the range slider thumb is hovered or dragged.
330+ ///
331+ /// If this property is null, [RangeSlider] will use [activeColor] with
332+ /// with an opacity of 0.12. If null, [SliderThemeData.overlayColor]
333+ /// will be used, otherwise defaults to [ColorScheme.primary] with
334+ /// an opacity of 0.12.
335+ final MaterialStateProperty <Color ?>? overlayColor;
336+
337+ /// The cursor for a mouse pointer when it enters or is hovering over the
338+ /// widget.
339+ ///
340+ /// If null, then the value of [SliderThemeData.mouseCursor] is used. If that
341+ /// is also null, then [MaterialStateMouseCursor.clickable] is used.
342+ ///
343+ /// See also:
344+ ///
345+ /// * [MaterialStateMouseCursor] , which can be used to create a [MouseCursor] .
346+ final MaterialStateProperty <MouseCursor ?>? mouseCursor;
347+
325348 /// The callback used to create a semantic value from the slider's values.
326349 ///
327350 /// Defaults to formatting values as a percentage.
@@ -400,6 +423,16 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
400423 PaintRangeValueIndicator ? paintTopValueIndicator;
401424 PaintRangeValueIndicator ? paintBottomValueIndicator;
402425
426+ bool get _enabled => widget.onChanged != null ;
427+
428+ bool _dragging = false ;
429+
430+ bool _hovering = false ;
431+ void _handleHoverChanged (bool hovering) {
432+ if (hovering != _hovering) {
433+ setState (() { _hovering = hovering; });
434+ }
435+ }
403436
404437 @override
405438 void initState () {
@@ -415,7 +448,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
415448 enableController = AnimationController (
416449 duration: enableAnimationDuration,
417450 vsync: this ,
418- value: widget.onChanged != null ? 1.0 : 0.0 ,
451+ value: _enabled ? 1.0 : 0.0 ,
419452 );
420453 startPositionController = AnimationController (
421454 duration: Duration .zero,
@@ -436,7 +469,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
436469 return ;
437470 }
438471 final bool wasEnabled = oldWidget.onChanged != null ;
439- final bool isEnabled = widget.onChanged != null ;
472+ final bool isEnabled = _enabled ;
440473 if (wasEnabled != isEnabled) {
441474 if (isEnabled) {
442475 enableController.forward ();
@@ -462,7 +495,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
462495 }
463496
464497 void _handleChanged (RangeValues values) {
465- assert (widget.onChanged != null );
498+ assert (_enabled );
466499 final RangeValues lerpValues = _lerpRangeValues (values);
467500 if (lerpValues != widget.values) {
468501 widget.onChanged !(lerpValues);
@@ -471,11 +504,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
471504
472505 void _handleDragStart (RangeValues values) {
473506 assert (widget.onChangeStart != null );
507+ _dragging = true ;
474508 widget.onChangeStart !(_lerpRangeValues (values));
475509 }
476510
477511 void _handleDragEnd (RangeValues values) {
478512 assert (widget.onChangeEnd != null );
513+ _dragging = false ;
479514 widget.onChangeEnd !(_lerpRangeValues (values));
480515 }
481516
@@ -576,6 +611,12 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
576611 const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator .onlyForDiscrete;
577612 const double defaultMinThumbSeparation = 8 ;
578613
614+ final Set <MaterialState > states = < MaterialState > {
615+ if (! _enabled) MaterialState .disabled,
616+ if (_hovering) MaterialState .hovered,
617+ if (_dragging) MaterialState .dragged,
618+ };
619+
579620 // The value indicator's color is not the same as the thumb and active track
580621 // (which can be defined by activeColor) if the
581622 // RectangularSliderValueIndicatorShape is used. In all other cases, the
@@ -588,6 +629,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
588629 valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary;
589630 }
590631
632+ Color ? effectiveOverlayColor () {
633+ return widget.overlayColor? .resolve (states)
634+ ?? widget.activeColor? .withOpacity (0.12 )
635+ ?? MaterialStateProperty .resolveAs <Color ?>(sliderTheme.overlayColor, states)
636+ ?? theme.colorScheme.primary.withOpacity (0.12 );
637+ }
638+
591639 sliderTheme = sliderTheme.copyWith (
592640 trackHeight: sliderTheme.trackHeight ?? defaultTrackHeight,
593641 activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
@@ -601,7 +649,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
601649 thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
602650 overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface,
603651 disabledThumbColor: sliderTheme.disabledThumbColor ?? Color .alphaBlend (theme.colorScheme.onSurface.withOpacity (.38 ), theme.colorScheme.surface),
604- overlayColor: widget.activeColor ? . withOpacity ( 0.12 ) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary. withOpacity ( 0.12 ),
652+ overlayColor: effectiveOverlayColor ( ),
605653 valueIndicatorColor: valueIndicatorColor,
606654 rangeTrackShape: sliderTheme.rangeTrackShape ?? defaultTrackShape,
607655 rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? defaultTickMarkShape,
@@ -615,26 +663,36 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
615663 minThumbSeparation: sliderTheme.minThumbSeparation ?? defaultMinThumbSeparation,
616664 thumbSelector: sliderTheme.thumbSelector ?? _defaultRangeThumbSelector,
617665 );
666+ final MouseCursor effectiveMouseCursor = widget.mouseCursor? .resolve (states)
667+ ?? sliderTheme.mouseCursor? .resolve (states)
668+ ?? MaterialStateMouseCursor .clickable.resolve (states);
618669
619670 // This size is used as the max bounds for the painting of the value
620671 // indicators. It must be kept in sync with the function with the same name
621672 // in slider.dart.
622673 Size screenSize () => MediaQuery .sizeOf (context);
623674
624- return CompositedTransformTarget (
625- link: _layerLink,
626- child: _RangeSliderRenderObjectWidget (
627- values: _unlerpRangeValues (widget.values),
628- divisions: widget.divisions,
629- labels: widget.labels,
630- sliderTheme: sliderTheme,
631- textScaleFactor: MediaQuery .textScaleFactorOf (context),
632- screenSize: screenSize (),
633- onChanged: (widget.onChanged != null ) && (widget.max > widget.min) ? _handleChanged : null ,
634- onChangeStart: widget.onChangeStart != null ? _handleDragStart : null ,
635- onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null ,
636- state: this ,
637- semanticFormatterCallback: widget.semanticFormatterCallback,
675+ return FocusableActionDetector (
676+ enabled: _enabled,
677+ onShowHoverHighlight: _handleHoverChanged,
678+ includeFocusSemantics: false ,
679+ mouseCursor: effectiveMouseCursor,
680+ child: CompositedTransformTarget (
681+ link: _layerLink,
682+ child: _RangeSliderRenderObjectWidget (
683+ values: _unlerpRangeValues (widget.values),
684+ divisions: widget.divisions,
685+ labels: widget.labels,
686+ sliderTheme: sliderTheme,
687+ textScaleFactor: MediaQuery .of (context).textScaleFactor,
688+ screenSize: screenSize (),
689+ onChanged: _enabled && (widget.max > widget.min) ? _handleChanged : null ,
690+ onChangeStart: widget.onChangeStart != null ? _handleDragStart : null ,
691+ onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null ,
692+ state: this ,
693+ semanticFormatterCallback: widget.semanticFormatterCallback,
694+ hovering: _hovering,
695+ ),
638696 ),
639697 );
640698 }
@@ -673,6 +731,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
673731 required this .onChangeEnd,
674732 required this .state,
675733 required this .semanticFormatterCallback,
734+ required this .hovering,
676735 });
677736
678737 final RangeValues values;
@@ -686,6 +745,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
686745 final ValueChanged <RangeValues >? onChangeEnd;
687746 final SemanticFormatterCallback ? semanticFormatterCallback;
688747 final _RangeSliderState state;
748+ final bool hovering;
689749
690750 @override
691751 _RenderRangeSlider createRenderObject (BuildContext context) {
@@ -704,6 +764,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
704764 textDirection: Directionality .of (context),
705765 semanticFormatterCallback: semanticFormatterCallback,
706766 platform: Theme .of (context).platform,
767+ hovering: hovering,
707768 gestureSettings: MediaQuery .gestureSettingsOf (context),
708769 );
709770 }
@@ -726,6 +787,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
726787 ..textDirection = Directionality .of (context)
727788 ..semanticFormatterCallback = semanticFormatterCallback
728789 ..platform = Theme .of (context).platform
790+ ..hovering = hovering
729791 ..gestureSettings = MediaQuery .gestureSettingsOf (context);
730792 }
731793}
@@ -746,6 +808,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
746808 required this .onChangeEnd,
747809 required _RangeSliderState state,
748810 required TextDirection textDirection,
811+ required bool hovering,
749812 required DeviceGestureSettings gestureSettings,
750813 }) : assert (values != null ),
751814 assert (values.start >= 0.0 && values.start <= 1.0 ),
@@ -763,7 +826,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
763826 _screenSize = screenSize,
764827 _onChanged = onChanged,
765828 _state = state,
766- _textDirection = textDirection {
829+ _textDirection = textDirection,
830+ _hovering = hovering {
767831 _updateLabelPainters ();
768832 final GestureArenaTeam team = GestureArenaTeam ();
769833 _drag = HorizontalDragGestureRecognizer ()
@@ -842,6 +906,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
842906 late RangeValues _newValues;
843907 late Offset _startThumbCenter;
844908 late Offset _endThumbCenter;
909+ Rect ? overlayStartRect;
910+ Rect ? overlayEndRect;
845911
846912 bool get isEnabled => onChanged != null ;
847913
@@ -993,6 +1059,53 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
9931059 _updateLabelPainters ();
9941060 }
9951061
1062+ /// True if this slider is being hovered over by a pointer.
1063+ bool get hovering => _hovering;
1064+ bool _hovering;
1065+ set hovering (bool value) {
1066+ assert (value != null );
1067+ if (value == _hovering) {
1068+ return ;
1069+ }
1070+ _hovering = value;
1071+ _updateForHover (_hovering);
1072+ }
1073+
1074+ /// True if the slider is interactive and the start thumb is being
1075+ /// hovered over by a pointer.
1076+ bool _hoveringStartThumb = false ;
1077+ bool get hoveringStartThumb => _hoveringStartThumb;
1078+ set hoveringStartThumb (bool value) {
1079+ assert (value != null );
1080+ if (value == _hoveringStartThumb) {
1081+ return ;
1082+ }
1083+ _hoveringStartThumb = value;
1084+ _updateForHover (_hovering);
1085+ }
1086+
1087+ /// True if the slider is interactive and the end thumb is being
1088+ /// hovered over by a pointer.
1089+ bool _hoveringEndThumb = false ;
1090+ bool get hoveringEndThumb => _hoveringEndThumb;
1091+ set hoveringEndThumb (bool value) {
1092+ assert (value != null );
1093+ if (value == _hoveringEndThumb) {
1094+ return ;
1095+ }
1096+ _hoveringEndThumb = value;
1097+ _updateForHover (_hovering);
1098+ }
1099+
1100+ void _updateForHover (bool hovered) {
1101+ // Only show overlay when pointer is hovering the thumb.
1102+ if (hovered && (hoveringStartThumb || hoveringEndThumb)) {
1103+ _state.overlayController.forward ();
1104+ } else {
1105+ _state.overlayController.reverse ();
1106+ }
1107+ }
1108+
9961109 bool get showValueIndicator {
9971110 switch (_sliderTheme.showValueIndicator! ) {
9981111 case ShowValueIndicator .onlyForDiscrete:
@@ -1253,6 +1366,14 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
12531366 _drag.addPointer (event);
12541367 _tap.addPointer (event);
12551368 }
1369+ if (isEnabled) {
1370+ if (overlayStartRect != null ) {
1371+ hoveringStartThumb = overlayStartRect! .contains (event.localPosition);
1372+ }
1373+ if (overlayEndRect != null ) {
1374+ hoveringEndThumb = overlayEndRect! .contains (event.localPosition);
1375+ }
1376+ }
12561377 }
12571378
12581379 @override
@@ -1307,6 +1428,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
13071428 );
13081429 _startThumbCenter = Offset (trackRect.left + startVisualPosition * trackRect.width, trackRect.center.dy);
13091430 _endThumbCenter = Offset (trackRect.left + endVisualPosition * trackRect.width, trackRect.center.dy);
1431+ if (isEnabled) {
1432+ final Size overlaySize = sliderTheme.overlayShape! .getPreferredSize (isEnabled, false );
1433+ overlayStartRect = Rect .fromCircle (center: _startThumbCenter, radius: overlaySize.width / 2.0 );
1434+ overlayEndRect = Rect .fromCircle (center: _endThumbCenter, radius: overlaySize.width / 2.0 );
1435+ }
13101436
13111437 _sliderTheme.rangeTrackShape! .paint (
13121438 context,
@@ -1326,7 +1452,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
13261452 final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize;
13271453
13281454 if (! _overlayAnimation.isDismissed) {
1329- if (startThumbSelected) {
1455+ if (startThumbSelected || hoveringStartThumb ) {
13301456 _sliderTheme.overlayShape! .paint (
13311457 context,
13321458 _startThumbCenter,
@@ -1342,7 +1468,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
13421468 sizeWithOverflow: resolvedscreenSize,
13431469 );
13441470 }
1345- if (endThumbSelected) {
1471+ if (endThumbSelected || hoveringEndThumb ) {
13461472 _sliderTheme.overlayShape! .paint (
13471473 context,
13481474 _endThumbCenter,
0 commit comments