Unverified Commit 5d96d619 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Add MaterialStateProperty `overlayColor` & `mouseCursor` and fix hovering on...

Add  MaterialStateProperty `overlayColor` & `mouseCursor` and fix hovering on thumbs behavior (#116894)
parent 07bc2452
...@@ -14,6 +14,7 @@ import 'package:flutter/widgets.dart'; ...@@ -14,6 +14,7 @@ import 'package:flutter/widgets.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'material_state.dart';
import 'slider_theme.dart'; import 'slider_theme.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -144,6 +145,8 @@ class RangeSlider extends StatefulWidget { ...@@ -144,6 +145,8 @@ class RangeSlider extends StatefulWidget {
this.labels, this.labels,
this.activeColor, this.activeColor,
this.inactiveColor, this.inactiveColor,
this.overlayColor,
this.mouseCursor,
this.semanticFormatterCallback, this.semanticFormatterCallback,
}) : assert(values != null), }) : assert(values != null),
assert(min != null), assert(min != null),
...@@ -322,6 +325,26 @@ class RangeSlider extends StatefulWidget { ...@@ -322,6 +325,26 @@ class RangeSlider extends StatefulWidget {
/// appearance of various components of the slider. /// appearance of various components of the slider.
final Color? inactiveColor; 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. /// The callback used to create a semantic value from the slider's values.
/// ///
/// Defaults to formatting values as a percentage. /// Defaults to formatting values as a percentage.
...@@ -400,6 +423,16 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -400,6 +423,16 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
PaintRangeValueIndicator? paintTopValueIndicator; PaintRangeValueIndicator? paintTopValueIndicator;
PaintRangeValueIndicator? paintBottomValueIndicator; 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 @override
void initState() { void initState() {
...@@ -415,7 +448,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -415,7 +448,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
enableController = AnimationController( enableController = AnimationController(
duration: enableAnimationDuration, duration: enableAnimationDuration,
vsync: this, vsync: this,
value: widget.onChanged != null ? 1.0 : 0.0, value: _enabled ? 1.0 : 0.0,
); );
startPositionController = AnimationController( startPositionController = AnimationController(
duration: Duration.zero, duration: Duration.zero,
...@@ -436,7 +469,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -436,7 +469,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
return; return;
} }
final bool wasEnabled = oldWidget.onChanged != null; final bool wasEnabled = oldWidget.onChanged != null;
final bool isEnabled = widget.onChanged != null; final bool isEnabled = _enabled;
if (wasEnabled != isEnabled) { if (wasEnabled != isEnabled) {
if (isEnabled) { if (isEnabled) {
enableController.forward(); enableController.forward();
...@@ -462,7 +495,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -462,7 +495,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
} }
void _handleChanged(RangeValues values) { void _handleChanged(RangeValues values) {
assert(widget.onChanged != null); assert(_enabled);
final RangeValues lerpValues = _lerpRangeValues(values); final RangeValues lerpValues = _lerpRangeValues(values);
if (lerpValues != widget.values) { if (lerpValues != widget.values) {
widget.onChanged!(lerpValues); widget.onChanged!(lerpValues);
...@@ -471,11 +504,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -471,11 +504,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
void _handleDragStart(RangeValues values) { void _handleDragStart(RangeValues values) {
assert(widget.onChangeStart != null); assert(widget.onChangeStart != null);
_dragging = true;
widget.onChangeStart!(_lerpRangeValues(values)); widget.onChangeStart!(_lerpRangeValues(values));
} }
void _handleDragEnd(RangeValues values) { void _handleDragEnd(RangeValues values) {
assert(widget.onChangeEnd != null); assert(widget.onChangeEnd != null);
_dragging = false;
widget.onChangeEnd!(_lerpRangeValues(values)); widget.onChangeEnd!(_lerpRangeValues(values));
} }
...@@ -576,6 +611,12 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -576,6 +611,12 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete; const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
const double defaultMinThumbSeparation = 8; 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 // The value indicator's color is not the same as the thumb and active track
// (which can be defined by activeColor) if the // (which can be defined by activeColor) if the
// RectangularSliderValueIndicatorShape is used. In all other cases, the // RectangularSliderValueIndicatorShape is used. In all other cases, the
...@@ -588,6 +629,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -588,6 +629,13 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary; 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( sliderTheme = sliderTheme.copyWith(
trackHeight: sliderTheme.trackHeight ?? defaultTrackHeight, trackHeight: sliderTheme.trackHeight ?? defaultTrackHeight,
activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary, activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary,
...@@ -601,7 +649,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -601,7 +649,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary, thumbColor: widget.activeColor ?? sliderTheme.thumbColor ?? theme.colorScheme.primary,
overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface, overlappingShapeStrokeColor: sliderTheme.overlappingShapeStrokeColor ?? theme.colorScheme.surface,
disabledThumbColor: sliderTheme.disabledThumbColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(.38), 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, valueIndicatorColor: valueIndicatorColor,
rangeTrackShape: sliderTheme.rangeTrackShape ?? defaultTrackShape, rangeTrackShape: sliderTheme.rangeTrackShape ?? defaultTrackShape,
rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? defaultTickMarkShape, rangeTickMarkShape: sliderTheme.rangeTickMarkShape ?? defaultTickMarkShape,
...@@ -615,26 +663,36 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin ...@@ -615,26 +663,36 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
minThumbSeparation: sliderTheme.minThumbSeparation ?? defaultMinThumbSeparation, minThumbSeparation: sliderTheme.minThumbSeparation ?? defaultMinThumbSeparation,
thumbSelector: sliderTheme.thumbSelector ?? _defaultRangeThumbSelector, 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 // 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 // indicators. It must be kept in sync with the function with the same name
// in slider.dart. // in slider.dart.
Size screenSize() => MediaQuery.sizeOf(context); Size screenSize() => MediaQuery.sizeOf(context);
return CompositedTransformTarget( return FocusableActionDetector(
link: _layerLink, enabled: _enabled,
child: _RangeSliderRenderObjectWidget( onShowHoverHighlight: _handleHoverChanged,
values: _unlerpRangeValues(widget.values), includeFocusSemantics: false,
divisions: widget.divisions, mouseCursor: effectiveMouseCursor,
labels: widget.labels, child: CompositedTransformTarget(
sliderTheme: sliderTheme, link: _layerLink,
textScaleFactor: MediaQuery.textScaleFactorOf(context), child: _RangeSliderRenderObjectWidget(
screenSize: screenSize(), values: _unlerpRangeValues(widget.values),
onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null, divisions: widget.divisions,
onChangeStart: widget.onChangeStart != null ? _handleDragStart : null, labels: widget.labels,
onChangeEnd: widget.onChangeEnd != null ? _handleDragEnd : null, sliderTheme: sliderTheme,
state: this, textScaleFactor: MediaQuery.of(context).textScaleFactor,
semanticFormatterCallback: widget.semanticFormatterCallback, 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,
),
), ),
); );
} }
...@@ -673,6 +731,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -673,6 +731,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
required this.onChangeEnd, required this.onChangeEnd,
required this.state, required this.state,
required this.semanticFormatterCallback, required this.semanticFormatterCallback,
required this.hovering,
}); });
final RangeValues values; final RangeValues values;
...@@ -686,6 +745,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -686,6 +745,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
final ValueChanged<RangeValues>? onChangeEnd; final ValueChanged<RangeValues>? onChangeEnd;
final SemanticFormatterCallback? semanticFormatterCallback; final SemanticFormatterCallback? semanticFormatterCallback;
final _RangeSliderState state; final _RangeSliderState state;
final bool hovering;
@override @override
_RenderRangeSlider createRenderObject(BuildContext context) { _RenderRangeSlider createRenderObject(BuildContext context) {
...@@ -704,6 +764,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -704,6 +764,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
textDirection: Directionality.of(context), textDirection: Directionality.of(context),
semanticFormatterCallback: semanticFormatterCallback, semanticFormatterCallback: semanticFormatterCallback,
platform: Theme.of(context).platform, platform: Theme.of(context).platform,
hovering: hovering,
gestureSettings: MediaQuery.gestureSettingsOf(context), gestureSettings: MediaQuery.gestureSettingsOf(context),
); );
} }
...@@ -726,6 +787,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { ...@@ -726,6 +787,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget {
..textDirection = Directionality.of(context) ..textDirection = Directionality.of(context)
..semanticFormatterCallback = semanticFormatterCallback ..semanticFormatterCallback = semanticFormatterCallback
..platform = Theme.of(context).platform ..platform = Theme.of(context).platform
..hovering = hovering
..gestureSettings = MediaQuery.gestureSettingsOf(context); ..gestureSettings = MediaQuery.gestureSettingsOf(context);
} }
} }
...@@ -746,6 +808,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -746,6 +808,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
required this.onChangeEnd, required this.onChangeEnd,
required _RangeSliderState state, required _RangeSliderState state,
required TextDirection textDirection, required TextDirection textDirection,
required bool hovering,
required DeviceGestureSettings gestureSettings, required DeviceGestureSettings gestureSettings,
}) : assert(values != null), }) : assert(values != null),
assert(values.start >= 0.0 && values.start <= 1.0), assert(values.start >= 0.0 && values.start <= 1.0),
...@@ -763,7 +826,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -763,7 +826,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_screenSize = screenSize, _screenSize = screenSize,
_onChanged = onChanged, _onChanged = onChanged,
_state = state, _state = state,
_textDirection = textDirection { _textDirection = textDirection,
_hovering = hovering {
_updateLabelPainters(); _updateLabelPainters();
final GestureArenaTeam team = GestureArenaTeam(); final GestureArenaTeam team = GestureArenaTeam();
_drag = HorizontalDragGestureRecognizer() _drag = HorizontalDragGestureRecognizer()
...@@ -842,6 +906,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -842,6 +906,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
late RangeValues _newValues; late RangeValues _newValues;
late Offset _startThumbCenter; late Offset _startThumbCenter;
late Offset _endThumbCenter; late Offset _endThumbCenter;
Rect? overlayStartRect;
Rect? overlayEndRect;
bool get isEnabled => onChanged != null; bool get isEnabled => onChanged != null;
...@@ -993,6 +1059,53 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -993,6 +1059,53 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_updateLabelPainters(); _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 { bool get showValueIndicator {
switch (_sliderTheme.showValueIndicator!) { switch (_sliderTheme.showValueIndicator!) {
case ShowValueIndicator.onlyForDiscrete: case ShowValueIndicator.onlyForDiscrete:
...@@ -1253,6 +1366,14 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1253,6 +1366,14 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
_drag.addPointer(event); _drag.addPointer(event);
_tap.addPointer(event); _tap.addPointer(event);
} }
if (isEnabled) {
if (overlayStartRect != null) {
hoveringStartThumb = overlayStartRect!.contains(event.localPosition);
}
if (overlayEndRect != null) {
hoveringEndThumb = overlayEndRect!.contains(event.localPosition);
}
}
} }
@override @override
...@@ -1307,6 +1428,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1307,6 +1428,11 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
); );
_startThumbCenter = Offset(trackRect.left + startVisualPosition * trackRect.width, trackRect.center.dy); _startThumbCenter = Offset(trackRect.left + startVisualPosition * trackRect.width, trackRect.center.dy);
_endThumbCenter = Offset(trackRect.left + endVisualPosition * 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( _sliderTheme.rangeTrackShape!.paint(
context, context,
...@@ -1326,7 +1452,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1326,7 +1452,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize; final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize;
if (!_overlayAnimation.isDismissed) { if (!_overlayAnimation.isDismissed) {
if (startThumbSelected) { if (startThumbSelected || hoveringStartThumb) {
_sliderTheme.overlayShape!.paint( _sliderTheme.overlayShape!.paint(
context, context,
_startThumbCenter, _startThumbCenter,
...@@ -1342,7 +1468,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix ...@@ -1342,7 +1468,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
sizeWithOverflow: resolvedscreenSize, sizeWithOverflow: resolvedscreenSize,
); );
} }
if (endThumbSelected) { if (endThumbSelected || hoveringEndThumb) {
_sliderTheme.overlayShape!.paint( _sliderTheme.overlayShape!.paint(
context, context,
_endThumbCenter, _endThumbCenter,
......
...@@ -2177,4 +2177,367 @@ void main() { ...@@ -2177,4 +2177,367 @@ void main() {
expect(nearEqual(activeTrackRect.left, (800.0 - 24.0 - 24.0) * (5 / 15) + 24.0, 0.01), true); expect(nearEqual(activeTrackRect.left, (800.0 - 24.0 - 24.0) * (5 / 15) + 24.0, 0.01), true);
expect(nearEqual(activeTrackRect.right, (800.0 - 24.0 - 24.0) * (8 / 15) + 24.0, 0.01), true); expect(nearEqual(activeTrackRect.right, (800.0 - 24.0 - 24.0) * (8 / 15) + 24.0, 0.01), true);
}); });
testWidgets('RangeSlider changes mouse cursor when hovered', (WidgetTester tester) async {
const RangeValues values = RangeValues(50, 70);
// Test default cursor.
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RangeSlider(
values: values,
max: 100.0,
onChanged: (RangeValues values) {},
),
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(RangeSlider)));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test custom cursor.
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RangeSlider(
values: values,
max: 100.0,
mouseCursor: const MaterialStatePropertyAll<MouseCursor?>(SystemMouseCursors.text),
onChanged: (RangeValues values) {},
),
),
),
),
),
),
);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
});
testWidgets('RangeSlider MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async {
RangeValues values = const RangeValues(50, 70);
const MouseCursor disabledCursor = SystemMouseCursors.basic;
const MouseCursor hoveredCursor = SystemMouseCursors.grab;
const MouseCursor draggedCursor = SystemMouseCursors.move;
Widget buildFrame({ required bool enabled }) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RangeSlider(
mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
if (states.contains(MaterialState.dragged)) {
return draggedCursor;
}
if (states.contains(MaterialState.hovered)) {
return hoveredCursor;
}
return SystemMouseCursors.none;
},
),
values: values,
max: 100.0,
onChanged: enabled
? (RangeValues newValues) {
setState(() {
values = newValues;
});
}
: null,
onChangeStart: enabled ? (RangeValues newValues) {} : null,
onChangeEnd: enabled ? (RangeValues newValues) {} : null,
),
),
);
},
),
),
),
);
}
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: Offset.zero);
await tester.pumpWidget(buildFrame(enabled: false));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), disabledCursor);
await tester.pumpWidget(buildFrame(enabled: true));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none);
await gesture.moveTo(tester.getCenter(find.byType(RangeSlider))); // start hover
await tester.pumpAndSettle();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor);
await tester.timedDrag(
find.byType(RangeSlider),
const Offset(20.0, 0.0),
const Duration(milliseconds: 100),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), draggedCursor);
});
testWidgets('RangeSlider can be hovered and has correct hover color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
RangeValues values = const RangeValues(50, 70);
final ThemeData theme = ThemeData();
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100.0,
onChanged: enabled
? (RangeValues newValues) {
setState(() {
values = newValues;
});
}
: null,
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildApp());
// RangeSlider does not have overlay when enabled and not hovered.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
// Start hovering.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(RangeSlider)));
// RangeSlider has overlay when enabled and hovered.
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
// RangeSlider does not have an overlay when disabled and hovered.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
});
testWidgets('RangeSlider is draggable and has correct dragged color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
RangeValues values = const RangeValues(50, 70);
final ThemeData theme = ThemeData();
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100.0,
onChanged: enabled
? (RangeValues newValues) {
setState(() {
values = newValues;
});
}
: null,
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildApp());
// RangeSlider does not have overlay when enabled and not dragged.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
// Start dragging.
final TestGesture drag = await tester.startGesture(tester.getCenter(find.byType(RangeSlider)));
await tester.pump(kPressTimeout);
// Less than configured touch slop, more than default touch slop
await drag.moveBy(const Offset(19.0, 0));
await tester.pump();
// RangeSlider has overlay when enabled and dragged.
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
});
testWidgets('RangeSlider overlayColor supports hovered and dragged states', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
RangeValues values = const RangeValues(50, 70);
const Color hoverColor = Color(0xffff0000);
const Color draggedColor = Color(0xff0000ff);
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100.0,
overlayColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.dragged)) {
return draggedColor;
}
return null;
}),
onChanged: enabled
? (RangeValues newValues) {
setState(() {
values = newValues;
});
}
: null,
onChangeStart: enabled ? (RangeValues newValues) {} : null,
onChangeEnd: enabled ? (RangeValues newValues) {} : null,
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildApp());
// RangeSlider does not have overlay when enabled and not hovered.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: hoverColor)),
);
// Hover on the range slider but outside the thumb.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getTopLeft(find.byType(RangeSlider)));
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: hoverColor)),
);
// Hover on the thumb.
await gesture.moveTo(tester.getCenter(find.byType(RangeSlider)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints..circle(color: hoverColor),
);
// Hover on the slider but outside the thumb.
await gesture.moveTo(tester.getBottomRight(find.byType(RangeSlider)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: hoverColor)),
);
// Reset range slider values.
values = const RangeValues(50, 70);
// RangeSlider does not have overlay when enabled and not dragged.
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: draggedColor)),
);
// Start dragging.
final TestGesture drag = await tester.startGesture(tester.getCenter(find.byType(RangeSlider)));
await tester.pump(kPressTimeout);
// Less than configured touch slop, more than default touch slop.
await drag.moveBy(const Offset(19.0, 0));
await tester.pump();
// RangeSlider has overlay when enabled and dragged.
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints..circle(color: draggedColor),
);
// Stop dragging.
await drag.up();
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: draggedColor)),
);
});
} }
...@@ -308,10 +308,6 @@ void main() { ...@@ -308,10 +308,6 @@ void main() {
).createShader(bounds), ).createShader(bounds),
child: const Placeholder(), child: const Placeholder(),
), ),
RangeSlider(
values: const RangeValues(0.3, 0.7),
onChanged: (RangeValues newValues) {},
),
CompositedTransformFollower( CompositedTransformFollower(
link: LayerLink(), link: LayerLink(),
), ),
...@@ -339,9 +335,6 @@ void main() { ...@@ -339,9 +335,6 @@ void main() {
renderObject = tester.firstRenderObject(find.byType(ShaderMask)); renderObject = tester.firstRenderObject(find.byType(ShaderMask));
expect(renderObject.debugLayer?.debugCreator, isNotNull); expect(renderObject.debugLayer?.debugCreator, isNotNull);
renderObject = tester.firstRenderObject(find.byType(RangeSlider));
expect(renderObject.debugLayer?.debugCreator, isNotNull);
renderObject = tester.firstRenderObject(find.byType(CompositedTransformFollower)); renderObject = tester.firstRenderObject(find.byType(CompositedTransformFollower));
expect(renderObject.debugLayer?.debugCreator, isNotNull); expect(renderObject.debugLayer?.debugCreator, isNotNull);
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment