Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix: Range slider show overlay for both thumbs on hovering one #165393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/flutter/lib/src/material/range_slider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1517,8 +1517,8 @@ class _RenderRangeSlider extends RenderBox with RelayoutWhenSystemFontsChangeMix
isEnabled: isEnabled,
);

final bool startThumbSelected = _lastThumbSelection == Thumb.start;
final bool endThumbSelected = _lastThumbSelection == Thumb.end;
final bool startThumbSelected = _lastThumbSelection == Thumb.start && !hoveringEndThumb;
Copy link
Contributor

@dkwingsmt dkwingsmt Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that startThumbSelected and endThumbSelected are also used in later code, deciding whether the thumb paint shows isPressed variant. I suspect that we don't want to change that (I'm not 100% sure, and there's no unit test to verify this.)

You might want to verify that (and even better, add a test). It's ok if you don't want to, but then I suggest a safer option of moving these extra conditions to L1525 and L1541.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkwingsmt If we paint thumbShape with previous condition, then shadow and other decoration effect will still be shown which is not intentional.

Copy link
Contributor Author

@rkishan516 rkishan516 Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I think current update is as expected. if you want i can update current test to verify that later part of code should be also painted with new condition.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rkishan516 That would be awesome! Yeah can you add that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

Copy link
Contributor Author

@rkishan516 rkishan516 Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated test as required.

final bool endThumbSelected = _lastThumbSelection == Thumb.end && !hoveringStartThumb;
final Size resolvedscreenSize = screenSize.isEmpty ? size : screenSize;

if (!_overlayAnimation.isDismissed) {
Expand Down
136 changes: 136 additions & 0 deletions packages/flutter/test/material/range_slider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2810,4 +2810,140 @@ void main() {

semantics.dispose();
}, semanticsEnabled: false);

testWidgets('RangeSlider overlay appears correctly for specific thumb interactions', (
WidgetTester tester,
) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
RangeValues values = const RangeValues(50, 70);
const Color hoverColor = Color(0xffff0000);
const Color dragColor = Color(0xff0000ff);

Widget buildApp() {
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: WidgetStateProperty.resolveWith<Color?>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.hovered)) {
return hoverColor;
}
if (states.contains(WidgetState.dragged)) {
return dragColor;
}

return null;
}),
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
onChangeStart: (RangeValues newValues) {},
onChangeEnd: (RangeValues newValues) {},
),
),
);
},
),
),
);
}

await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();

// Initial state - no overlay.
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: dragColor)),
);

// Drag start thumb to left.
final Offset topThumbLocation = tester.getCenter(find.byType(RangeSlider));
final TestGesture dragStartThumb = await tester.startGesture(topThumbLocation);
await tester.pump(kPressTimeout);
await dragStartThumb.moveBy(const Offset(-20.0, 0));
await tester.pumpAndSettle();

// Verify overlay is visible and shadow is visible on single thumb.
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints
..circle(color: dragColor)
..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0)
..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 12.0),
);

// Move back and release.
await dragStartThumb.moveBy(const Offset(20.0, 0));
await dragStartThumb.up();
await tester.pumpAndSettle();

// Verify overlay and shadow disappears
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(
paints
..circle(color: dragColor)
..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0)
..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0),
),
);

// Drag end thumb and return to original position.
final Offset bottomThumbLocation = tester
.getCenter(find.byType(RangeSlider))
.translate(220.0, 0.0);
final TestGesture dragEndThumb = await tester.startGesture(bottomThumbLocation);
await tester.pump(kPressTimeout);
await dragEndThumb.moveBy(const Offset(20.0, 0));
await tester.pump(kPressTimeout);
await dragEndThumb.moveBy(const Offset(-20.0, 0));
await dragEndThumb.up();
await tester.pumpAndSettle();

// Verify overlay disappears.
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: dragColor)),
);

// Hover on start thumb.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(topThumbLocation);
await tester.pumpAndSettle();

// Verify overlay appears only for start thumb and no shadow is visible.
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints
..circle(color: hoverColor)
..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0)
..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0),
);

final RenderObject renderObject = tester.renderObject(find.byType(RangeSlider));
// 2 thumbs and 1 overlay.
expect(renderObject, paintsExactlyCountTimes(#drawCircle, 3));

// Move away from thumb
await gesture.moveTo(tester.getTopRight(find.byType(RangeSlider)));
await tester.pumpAndSettle();

// Verify overlay disappears
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: hoverColor)),
);
});
}