Skip to content

Commit 5d96d61

Browse files
authored
Add MaterialStateProperty overlayColor & mouseCursor and fix hovering on thumbs behavior (#116894)
1 parent 07bc245 commit 5d96d61

File tree

3 files changed

+510
-28
lines changed

3 files changed

+510
-28
lines changed

packages/flutter/lib/src/material/range_slider.dart

Lines changed: 147 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:flutter/widgets.dart';
1414

1515
import 'constants.dart';
1616
import 'debug.dart';
17+
import 'material_state.dart';
1718
import 'slider_theme.dart';
1819
import '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

Comments
 (0)