From 5d96d619d88d6e7305fc3cae474db6762941b2fe Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Tue, 10 Jan 2023 15:40:04 +0200 Subject: [PATCH] Add MaterialStateProperty `overlayColor` & `mouseCursor` and fix hovering on thumbs behavior (#116894) --- .../lib/src/material/range_slider.dart | 168 +++++++- .../test/material/range_slider_test.dart | 363 ++++++++++++++++++ packages/flutter/test/widgets/debug_test.dart | 7 - 3 files changed, 510 insertions(+), 28 deletions(-) diff --git a/packages/flutter/lib/src/material/range_slider.dart b/packages/flutter/lib/src/material/range_slider.dart index fd26eda179f5..be0109be4008 100644 --- a/packages/flutter/lib/src/material/range_slider.dart +++ b/packages/flutter/lib/src/material/range_slider.dart @@ -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'; @@ -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), @@ -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? 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; + /// The callback used to create a semantic value from the slider's values. /// /// Defaults to formatting values as a percentage. @@ -400,6 +423,16 @@ class _RangeSliderState extends State 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() { @@ -415,7 +448,7 @@ class _RangeSliderState extends State 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, @@ -436,7 +469,7 @@ class _RangeSliderState extends State 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(); @@ -462,7 +495,7 @@ class _RangeSliderState extends State with TickerProviderStateMixin } void _handleChanged(RangeValues values) { - assert(widget.onChanged != null); + assert(_enabled); final RangeValues lerpValues = _lerpRangeValues(values); if (lerpValues != widget.values) { widget.onChanged!(lerpValues); @@ -471,11 +504,13 @@ class _RangeSliderState extends State 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)); } @@ -576,6 +611,12 @@ class _RangeSliderState extends State with TickerProviderStateMixin const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete; const double defaultMinThumbSeparation = 8; + final Set states = { + 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 @@ -588,6 +629,13 @@ class _RangeSliderState extends State with TickerProviderStateMixin valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary; } + Color? effectiveOverlayColor() { + return widget.overlayColor?.resolve(states) + ?? widget.activeColor?.withOpacity(0.12) + ?? MaterialStateProperty.resolveAs(sliderTheme.overlayColor, states) + ?? theme.colorScheme.primary.withOpacity(0.12); + } + sliderTheme = sliderTheme.copyWith( trackHeight: sliderTheme.trackHeight ?? defaultTrackHeight, activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? theme.colorScheme.primary, @@ -601,7 +649,7 @@ class _RangeSliderState extends State 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, @@ -615,26 +663,36 @@ class _RangeSliderState extends State 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, + ), ), ); } @@ -673,6 +731,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { required this.onChangeEnd, required this.state, required this.semanticFormatterCallback, + required this.hovering, }); final RangeValues values; @@ -686,6 +745,7 @@ class _RangeSliderRenderObjectWidget extends LeafRenderObjectWidget { final ValueChanged? onChangeEnd; final SemanticFormatterCallback? semanticFormatterCallback; final _RangeSliderState state; + final bool hovering; @override _RenderRangeSlider createRenderObject(BuildContext context) { @@ -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), ); } @@ -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); } } @@ -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), @@ -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() @@ -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; @@ -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: @@ -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 @@ -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, @@ -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, @@ -1342,7 +1468,7 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix sizeWithOverflow: resolvedscreenSize, ); } - if (endThumbSelected) { + if (endThumbSelected || hoveringEndThumb) { _sliderTheme.overlayShape!.paint( context, _endThumbCenter, diff --git a/packages/flutter/test/material/range_slider_test.dart b/packages/flutter/test/material/range_slider_test.dart index bf046f2bd7a3..ecf788285869 100644 --- a/packages/flutter/test/material/range_slider_test.dart +++ b/packages/flutter/test/material/range_slider_test.dart @@ -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.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(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( + (Set 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((Set 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)), + ); + }); } diff --git a/packages/flutter/test/widgets/debug_test.dart b/packages/flutter/test/widgets/debug_test.dart index 476e420bce42..30e2329ff195 100644 --- a/packages/flutter/test/widgets/debug_test.dart +++ b/packages/flutter/test/widgets/debug_test.dart @@ -308,10 +308,6 @@ void main() { ).createShader(bounds), child: const Placeholder(), ), - RangeSlider( - values: const RangeValues(0.3, 0.7), - onChanged: (RangeValues newValues) {}, - ), CompositedTransformFollower( link: LayerLink(), ), @@ -339,9 +335,6 @@ void main() { renderObject = tester.firstRenderObject(find.byType(ShaderMask)); expect(renderObject.debugLayer?.debugCreator, isNotNull); - renderObject = tester.firstRenderObject(find.byType(RangeSlider)); - expect(renderObject.debugLayer?.debugCreator, isNotNull); - renderObject = tester.firstRenderObject(find.byType(CompositedTransformFollower)); expect(renderObject.debugLayer?.debugCreator, isNotNull); });