Skip to content

Commit

Permalink
Add MaterialStateProperty overlayColor & mouseCursor and fix hove…
Browse files Browse the repository at this point in the history
…ring on thumbs behavior (#116894)
  • Loading branch information
TahaTesser authored Jan 10, 2023
1 parent 07bc245 commit 5d96d61
Show file tree
Hide file tree
Showing 3 changed files with 510 additions and 28 deletions.
168 changes: 147 additions & 21 deletions packages/flutter/lib/src/material/range_slider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:flutter/widgets.dart';

import 'constants.dart';
import 'debug.dart';
import 'material_state.dart';
import 'slider_theme.dart';
import 'theme.dart';

Expand Down Expand Up @@ -144,6 +145,8 @@ class RangeSlider extends StatefulWidget {
this.labels,
this.activeColor,
this.inactiveColor,
this.overlayColor,
this.mouseCursor,
this.semanticFormatterCallback,
}) : assert(values != null),
assert(min != null),
Expand Down Expand Up @@ -322,6 +325,26 @@ class RangeSlider extends StatefulWidget {
/// appearance of various components of the slider.
final Color? inactiveColor;

/// The highlight color that's typically used to indicate that
/// the range slider thumb is hovered or dragged.
///
/// If this property is null, [RangeSlider] will use [activeColor] with
/// with an opacity of 0.12. If null, [SliderThemeData.overlayColor]
/// will be used, otherwise defaults to [ColorScheme.primary] with
/// an opacity of 0.12.
final MaterialStateProperty<Color?>? overlayColor;

/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If null, then the value of [SliderThemeData.mouseCursor] is used. If that
/// is also null, then [MaterialStateMouseCursor.clickable] is used.
///
/// See also:
///
/// * [MaterialStateMouseCursor], which can be used to create a [MouseCursor].
final MaterialStateProperty<MouseCursor?>? mouseCursor;

/// The callback used to create a semantic value from the slider's values.
///
/// Defaults to formatting values as a percentage.
Expand Down Expand Up @@ -400,6 +423,16 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
PaintRangeValueIndicator? paintTopValueIndicator;
PaintRangeValueIndicator? paintBottomValueIndicator;

bool get _enabled => widget.onChanged != null;

bool _dragging = false;

bool _hovering = false;
void _handleHoverChanged(bool hovering) {
if (hovering != _hovering) {
setState(() { _hovering = hovering; });
}
}

@override
void initState() {
Expand All @@ -415,7 +448,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
enableController = AnimationController(
duration: enableAnimationDuration,
vsync: this,
value: widget.onChanged != null ? 1.0 : 0.0,
value: _enabled ? 1.0 : 0.0,
);
startPositionController = AnimationController(
duration: Duration.zero,
Expand All @@ -436,7 +469,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
return;
}
final bool wasEnabled = oldWidget.onChanged != null;
final bool isEnabled = widget.onChanged != null;
final bool isEnabled = _enabled;
if (wasEnabled != isEnabled) {
if (isEnabled) {
enableController.forward();
Expand All @@ -462,7 +495,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
}

void _handleChanged(RangeValues values) {
assert(widget.onChanged != null);
assert(_enabled);
final RangeValues lerpValues = _lerpRangeValues(values);
if (lerpValues != widget.values) {
widget.onChanged!(lerpValues);
Expand All @@ -471,11 +504,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin

void _handleDragStart(RangeValues values) {
assert(widget.onChangeStart != null);
_dragging = true;
widget.onChangeStart!(_lerpRangeValues(values));
}

void _handleDragEnd(RangeValues values) {
assert(widget.onChangeEnd != null);
_dragging = false;
widget.onChangeEnd!(_lerpRangeValues(values));
}

Expand Down Expand Up @@ -576,6 +611,12 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
const double defaultMinThumbSeparation = 8;

final Set<MaterialState> states = <MaterialState>{
if (!_enabled) MaterialState.disabled,
if (_hovering) MaterialState.hovered,
if (_dragging) MaterialState.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
Expand All @@ -588,6 +629,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary;
}

Color? effectiveOverlayColor() {
return widget.overlayColor?.resolve(states)
?? widget.activeColor?.withOpacity(0.12)
?? MaterialStateProperty.resolveAs<Color?>(sliderTheme.overlayColor, states)
?? theme.colorScheme.primary.withOpacity(0.12);
}

sliderTheme = sliderTheme.copyWith(
trackHeight: sliderTheme.trackHeight ?? defaultTrackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
Expand All @@ -601,7 +649,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface,
disabledThumbColor: sliderTheme.disabledThumbColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(.38), theme.colorScheme.surface),
overlayColor: widget.activeColor?.withOpacity(0.12) ?? sliderTheme.overlayColor ?? theme.colorScheme.primary.withOpacity(0.12),
overlayColor: effectiveOverlayColor(),
valueIndicatorColor: valueIndicatorColor,
rangeTrackShape: sliderTheme.rangeTrackShape ?? defaultTrackShape,
rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? defaultTickMarkShape,
Expand All @@ -615,26 +663,36 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
minThumbSeparation: sliderTheme.minThumbSeparation ?? defaultMinThumbSeparation,
thumbSelector: sliderTheme.thumbSelector ?? _defaultRangeThumbSelector,
);
final MouseCursor effectiveMouseCursor = widget.mouseCursor?.resolve(states)
?? sliderTheme.mouseCursor?.resolve(states)
?? MaterialStateMouseCursor.clickable.resolve(states);

// 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 slider.dart.
Size screenSize() => MediaQuery.sizeOf(context);

return CompositedTransformTarget(
link: _layerLink,
child: _RangeSliderRenderObjectWidget(
values: _unlerpRangeValues(widget.values),
divisions: widget.divisions,
labels: widget.labels,
sliderTheme: sliderTheme,
textScaleFactor: MediaQuery.textScaleFactorOf(context),
screenSize: screenSize(),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
state: this,
semanticFormatterCallback: widget.semanticFormatterCallback,
return FocusableActionDetector(
enabled: _enabled,
onShowHoverHighlight: _handleHoverChanged,
includeFocusSemantics: false,
mouseCursor: effectiveMouseCursor,
child: CompositedTransformTarget(
link: _layerLink,
child: _RangeSliderRenderObjectWidget(
values: _unlerpRangeValues(widget.values),
divisions: widget.divisions,
labels: widget.labels,
sliderTheme: sliderTheme,
textScaleFactor: MediaQuery.of(context).textScaleFactor,
screenSize: screenSize(),
onChanged: _enabled && (widget.max > widget.min) ? _handleChanged : null,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null,
state: this,
semanticFormatterCallback: widget.semanticFormatterCallback,
hovering: _hovering,
),
),
);
}
Expand Down Expand Up @@ -673,6 +731,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
required this.onChangeEnd,
required this.state,
required this.semanticFormatterCallback,
required this.hovering,
});

final RangeValues values;
Expand All @@ -686,6 +745,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
final ValueChanged<RangeValues>? onChangeEnd;
final SemanticFormatterCallback? semanticFormatterCallback;
final _RangeSliderState state;
final bool hovering;

@override
_RenderRangeSlider createRenderObject(BuildContext context) {
Expand All @@ -704,6 +764,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
textDirection: Directionality.of(context),
semanticFormatterCallback: semanticFormatterCallback,
platform: Theme.of(context).platform,
hovering: hovering,
gestureSettings: MediaQuery.gestureSettingsOf(context),
);
}
Expand All @@ -726,6 +787,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
..textDirection = Directionality.of(context)
..semanticFormatterCallback = semanticFormatterCallback
..platform = Theme.of(context).platform
..hovering = hovering
..gestureSettings = MediaQuery.gestureSettingsOf(context);
}
}
Expand All @@ -746,6 +808,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
required this.onChangeEnd,
required _RangeSliderState state,
required TextDirection textDirection,
required bool hovering,
required DeviceGestureSettings gestureSettings,
}) : assert(values != null),
assert(values.start >= 0.0 && values.start <= 1.0),
Expand All @@ -763,7 +826,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_screenSize = screenSize,
_onChanged = onChanged,
_state = state,
_textDirection = textDirection {
_textDirection = textDirection,
_hovering = hovering {
_updateLabelPainters();
final GestureArenaTeam team = GestureArenaTeam();
_drag = HorizontalDragGestureRecognizer()
Expand Down Expand Up @@ -842,6 +906,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
late RangeValues _newValues;
late Offset _startThumbCenter;
late Offset _endThumbCenter;
Rect? overlayStartRect;
Rect? overlayEndRect;

bool get isEnabled => onChanged != null;

Expand Down Expand Up @@ -993,6 +1059,53 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_updateLabelPainters();
}

/// True if this slider is being hovered over by a pointer.
bool get hovering => _hovering;
bool _hovering;
set hovering(bool value) {
assert(value != null);
if (value == _hovering) {
return;
}
_hovering = value;
_updateForHover(_hovering);
}

/// True if the slider is interactive and the start thumb is being
/// hovered over by a pointer.
bool _hoveringStartThumb = false;
bool get hoveringStartThumb => _hoveringStartThumb;
set hoveringStartThumb(bool value) {
assert(value != null);
if (value == _hoveringStartThumb) {
return;
}
_hoveringStartThumb = value;
_updateForHover(_hovering);
}

/// True if the slider is interactive and the end thumb is being
/// hovered over by a pointer.
bool _hoveringEndThumb = false;
bool get hoveringEndThumb => _hoveringEndThumb;
set hoveringEndThumb(bool value) {
assert(value != null);
if (value == _hoveringEndThumb) {
return;
}
_hoveringEndThumb = value;
_updateForHover(_hovering);
}

void _updateForHover(bool hovered) {
// Only show overlay when pointer is hovering the thumb.
if (hovered && (hoveringStartThumb || hoveringEndThumb)) {
_state.overlayController.forward();
} else {
_state.overlayController.reverse();
}
}

bool get showValueIndicator {
switch (_sliderTheme.showValueIndicator!) {
case ShowValueIndicator.onlyForDiscrete:
Expand Down Expand Up @@ -1253,6 +1366,14 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_drag.addPointer(event);
_tap.addPointer(event);
}
if (isEnabled) {
if (overlayStartRect != null) {
hoveringStartThumb = overlayStartRect!.contains(event.localPosition);
}
if (overlayEndRect != null) {
hoveringEndThumb = overlayEndRect!.contains(event.localPosition);
}
}
}

@override
Expand Down Expand Up @@ -1307,6 +1428,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
);
_startThumbCenter = Offset(trackRect.left + startVisualPosition * trackRect.width, trackRect.center.dy);
_endThumbCenter = Offset(trackRect.left + endVisualPosition * trackRect.width, trackRect.center.dy);
if (isEnabled) {
final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize(isEnabled, false);
overlayStartRect = Rect.fromCircle(center: _startThumbCenter, radius: overlaySize.width / 2.0);
overlayEndRect = Rect.fromCircle(center: _endThumbCenter, radius: overlaySize.width / 2.0);
}

_sliderTheme.rangeTrackShape!.paint(
context,
Expand All @@ -1326,7 +1452,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize;

if (!_overlayAnimation.isDismissed) {
if (startThumbSelected) {
if (startThumbSelected || hoveringStartThumb) {
_sliderTheme.overlayShape!.paint(
context,
_startThumbCenter,
Expand All @@ -1342,7 +1468,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
sizeWithOverflow: resolvedscreenSize,
);
}
if (endThumbSelected) {
if (endThumbSelected || hoveringEndThumb) {
_sliderTheme.overlayShape!.paint(
context,
_endThumbCenter,
Expand Down
Loading

0 comments on commit 5d96d61

Please sign in to comment.