@@ -14,6 +14,7 @@ import 'package:flutter/widgets.dart';
14
14
15
15
import 'constants.dart' ;
16
16
import 'debug.dart' ;
17
+ import 'material_state.dart' ;
17
18
import 'slider_theme.dart' ;
18
19
import 'theme.dart' ;
19
20
@@ -144,6 +145,8 @@ class RangeSlider extends StatefulWidget {
144
145
this .labels,
145
146
this .activeColor,
146
147
this .inactiveColor,
148
+ this .overlayColor,
149
+ this .mouseCursor,
147
150
this .semanticFormatterCallback,
148
151
}) : assert (values != null ),
149
152
assert (min != null ),
@@ -322,6 +325,26 @@ class RangeSlider extends StatefulWidget {
322
325
/// appearance of various components of the slider.
323
326
final Color ? inactiveColor;
324
327
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
+
325
348
/// The callback used to create a semantic value from the slider's values.
326
349
///
327
350
/// Defaults to formatting values as a percentage.
@@ -400,6 +423,16 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
400
423
PaintRangeValueIndicator ? paintTopValueIndicator;
401
424
PaintRangeValueIndicator ? paintBottomValueIndicator;
402
425
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
+ }
403
436
404
437
@override
405
438
void initState () {
@@ -415,7 +448,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
415
448
enableController = AnimationController (
416
449
duration: enableAnimationDuration,
417
450
vsync: this ,
418
- value: widget.onChanged != null ? 1.0 : 0.0 ,
451
+ value: _enabled ? 1.0 : 0.0 ,
419
452
);
420
453
startPositionController = AnimationController (
421
454
duration: Duration .zero,
@@ -436,7 +469,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
436
469
return ;
437
470
}
438
471
final bool wasEnabled = oldWidget.onChanged != null ;
439
- final bool isEnabled = widget.onChanged != null ;
472
+ final bool isEnabled = _enabled ;
440
473
if (wasEnabled != isEnabled) {
441
474
if (isEnabled) {
442
475
enableController.forward ();
@@ -462,7 +495,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
462
495
}
463
496
464
497
void _handleChanged (RangeValues values) {
465
- assert (widget.onChanged != null );
498
+ assert (_enabled );
466
499
final RangeValues lerpValues = _lerpRangeValues (values);
467
500
if (lerpValues != widget.values) {
468
501
widget.onChanged !(lerpValues);
@@ -471,11 +504,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
471
504
472
505
void _handleDragStart (RangeValues values) {
473
506
assert (widget.onChangeStart != null );
507
+ _dragging = true ;
474
508
widget.onChangeStart !(_lerpRangeValues (values));
475
509
}
476
510
477
511
void _handleDragEnd (RangeValues values) {
478
512
assert (widget.onChangeEnd != null );
513
+ _dragging = false ;
479
514
widget.onChangeEnd !(_lerpRangeValues (values));
480
515
}
481
516
@@ -576,6 +611,12 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
576
611
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator .onlyForDiscrete;
577
612
const double defaultMinThumbSeparation = 8 ;
578
613
614
+ final Set <MaterialState > states = < MaterialState > {
615
+ if (! _enabled) MaterialState .disabled,
616
+ if (_hovering) MaterialState .hovered,
617
+ if (_dragging) MaterialState .dragged,
618
+ };
619
+
579
620
// The value indicator's color is not the same as the thumb and active track
580
621
// (which can be defined by activeColor) if the
581
622
// RectangularSliderValueIndicatorShape is used. In all other cases, the
@@ -588,6 +629,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
588
629
valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary;
589
630
}
590
631
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
+
591
639
sliderTheme = sliderTheme.copyWith (
592
640
trackHeight: sliderTheme.trackHeight ?? defaultTrackHeight,
593
641
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
@@ -601,7 +649,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
601
649
thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
602
650
overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface,
603
651
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 ( ),
605
653
valueIndicatorColor: valueIndicatorColor,
606
654
rangeTrackShape: sliderTheme.rangeTrackShape ?? defaultTrackShape,
607
655
rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? defaultTickMarkShape,
@@ -615,26 +663,36 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
615
663
minThumbSeparation: sliderTheme.minThumbSeparation ?? defaultMinThumbSeparation,
616
664
thumbSelector: sliderTheme.thumbSelector ?? _defaultRangeThumbSelector,
617
665
);
666
+ final MouseCursor effectiveMouseCursor = widget.mouseCursor? .resolve (states)
667
+ ?? sliderTheme.mouseCursor? .resolve (states)
668
+ ?? MaterialStateMouseCursor .clickable.resolve (states);
618
669
619
670
// This size is used as the max bounds for the painting of the value
620
671
// indicators. It must be kept in sync with the function with the same name
621
672
// in slider.dart.
622
673
Size screenSize () => MediaQuery .sizeOf (context);
623
674
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
+ ),
638
696
),
639
697
);
640
698
}
@@ -673,6 +731,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
673
731
required this .onChangeEnd,
674
732
required this .state,
675
733
required this .semanticFormatterCallback,
734
+ required this .hovering,
676
735
});
677
736
678
737
final RangeValues values;
@@ -686,6 +745,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
686
745
final ValueChanged <RangeValues >? onChangeEnd;
687
746
final SemanticFormatterCallback ? semanticFormatterCallback;
688
747
final _RangeSliderState state;
748
+ final bool hovering;
689
749
690
750
@override
691
751
_RenderRangeSlider createRenderObject (BuildContext context) {
@@ -704,6 +764,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
704
764
textDirection: Directionality .of (context),
705
765
semanticFormatterCallback: semanticFormatterCallback,
706
766
platform: Theme .of (context).platform,
767
+ hovering: hovering,
707
768
gestureSettings: MediaQuery .gestureSettingsOf (context),
708
769
);
709
770
}
@@ -726,6 +787,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
726
787
..textDirection = Directionality .of (context)
727
788
..semanticFormatterCallback = semanticFormatterCallback
728
789
..platform = Theme .of (context).platform
790
+ ..hovering = hovering
729
791
..gestureSettings = MediaQuery .gestureSettingsOf (context);
730
792
}
731
793
}
@@ -746,6 +808,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
746
808
required this .onChangeEnd,
747
809
required _RangeSliderState state,
748
810
required TextDirection textDirection,
811
+ required bool hovering,
749
812
required DeviceGestureSettings gestureSettings,
750
813
}) : assert (values != null ),
751
814
assert (values.start >= 0.0 && values.start <= 1.0 ),
@@ -763,7 +826,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
763
826
_screenSize = screenSize,
764
827
_onChanged = onChanged,
765
828
_state = state,
766
- _textDirection = textDirection {
829
+ _textDirection = textDirection,
830
+ _hovering = hovering {
767
831
_updateLabelPainters ();
768
832
final GestureArenaTeam team = GestureArenaTeam ();
769
833
_drag = HorizontalDragGestureRecognizer ()
@@ -842,6 +906,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
842
906
late RangeValues _newValues;
843
907
late Offset _startThumbCenter;
844
908
late Offset _endThumbCenter;
909
+ Rect ? overlayStartRect;
910
+ Rect ? overlayEndRect;
845
911
846
912
bool get isEnabled => onChanged != null ;
847
913
@@ -993,6 +1059,53 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
993
1059
_updateLabelPainters ();
994
1060
}
995
1061
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
+
996
1109
bool get showValueIndicator {
997
1110
switch (_sliderTheme.showValueIndicator! ) {
998
1111
case ShowValueIndicator .onlyForDiscrete:
@@ -1253,6 +1366,14 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
1253
1366
_drag.addPointer (event);
1254
1367
_tap.addPointer (event);
1255
1368
}
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
+ }
1256
1377
}
1257
1378
1258
1379
@override
@@ -1307,6 +1428,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
1307
1428
);
1308
1429
_startThumbCenter = Offset (trackRect.left + startVisualPosition * trackRect.width, trackRect.center.dy);
1309
1430
_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
+ }
1310
1436
1311
1437
_sliderTheme.rangeTrackShape! .paint (
1312
1438
context,
@@ -1326,7 +1452,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
1326
1452
final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize;
1327
1453
1328
1454
if (! _overlayAnimation.isDismissed) {
1329
- if (startThumbSelected) {
1455
+ if (startThumbSelected || hoveringStartThumb ) {
1330
1456
_sliderTheme.overlayShape! .paint (
1331
1457
context,
1332
1458
_startThumbCenter,
@@ -1342,7 +1468,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
1342
1468
sizeWithOverflow: resolvedscreenSize,
1343
1469
);
1344
1470
}
1345
- if (endThumbSelected) {
1471
+ if (endThumbSelected || hoveringEndThumb ) {
1346
1472
_sliderTheme.overlayShape! .paint (
1347
1473
context,
1348
1474
_endThumbCenter,
0 commit comments