From 450401dfec79c052ce36432b141c6fe81ac09f97 Mon Sep 17 00:00:00 2001 From: Hannah Jin Date: Thu, 14 Aug 2025 11:27:51 -0700 Subject: [PATCH] [Range slider] Tap on active range, the thumb closest to the mouse cursor should move to the cursor position. (#173725) Fix: https://github.com/flutter/flutter/issues/172923 Internal issue: b/434778923 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --- .../lib/src/material/range_slider.dart | 18 ++++---- .../test/material/range_slider_test.dart | 41 ++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/flutter/lib/src/material/range_slider.dart b/packages/flutter/lib/src/material/range_slider.dart index 7d220ccece0..1b9658ed275 100644 --- a/packages/flutter/lib/src/material/range_slider.dart +++ b/packages/flutter/lib/src/material/range_slider.dart @@ -599,10 +599,13 @@ class _RangeSliderState extends State with TickerProviderStateMixin return RangeValues(_unlerp(values.start), _unlerp(values.end)); } - // Finds closest thumb. If the thumbs are close to each other, no thumb is - // immediately selected while the drag displacement is zero. If the first - // non-zero displacement is negative, then the left thumb is selected, and if its - // positive, then the right thumb is selected. + // Finds the closest thumb. If both thumbs are close to each other and within + // the touch radius, neither is selected immediately while the drag + // displacement is zero. The first non-zero displacement determines which + // thumb is selected: a negative displacement selects the left thumb, + // a positive one selects the right thumb. + // If only one or zero thumbs are within the touch radius, + // the closest one is selected. Thumb? _defaultRangeThumbSelector( TextDirection textDirection, RangeValues values, @@ -632,11 +635,10 @@ class _RangeSliderState extends State with TickerProviderStateMixin return Thumb.end; } } else { - // Snap position on the track if its in the inactive range. - if (tapValue < values.start || inStartTouchTarget) { + // Choose the closest thumb and snap position. + if (tapValue * 2 < values.start + values.end) { return Thumb.start; - } - if (tapValue > values.end || inEndTouchTarget) { + } else { return Thumb.end; } } diff --git a/packages/flutter/test/material/range_slider_test.dart b/packages/flutter/test/material/range_slider_test.dart index 05016ed02d2..fd9944dd0e0 100644 --- a/packages/flutter/test/material/range_slider_test.dart +++ b/packages/flutter/test/material/range_slider_test.dart @@ -125,7 +125,7 @@ void main() { }); testWidgets('Range Slider can move when tapped (continuous LTR)', (WidgetTester tester) async { - RangeValues values = const RangeValues(0.3, 0.7); + RangeValues values = const RangeValues(0.3, 0.8); await tester.pumpWidget( MaterialApp( @@ -151,13 +151,13 @@ void main() { ), ); - // No thumbs get select when tapping between the thumbs outside the touch + // The closest thumb is selected when tapping between the thumbs outside the touch // boundaries - expect(values, equals(const RangeValues(0.3, 0.7))); + expect(values, equals(const RangeValues(0.3, 0.8))); // taps at 0.5 await tester.tap(find.byType(RangeSlider)); await tester.pump(); - expect(values, equals(const RangeValues(0.3, 0.7))); + expect(values, equals(const RangeValues(0.5, 0.8))); // Get the bounds of the track by finding the slider edges and translating // inwards by the overlay radius. @@ -168,7 +168,7 @@ void main() { final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; await tester.tapAt(leftTarget); expect(values.start, moreOrLessEquals(0.1, epsilon: 0.01)); - expect(values.end, equals(0.7)); + expect(values.end, equals(0.8)); // The end thumb is selected when tapping the right inactive track. await tester.pump(); @@ -179,7 +179,7 @@ void main() { }); testWidgets('Range Slider can move when tapped (continuous RTL)', (WidgetTester tester) async { - RangeValues values = const RangeValues(0.3, 0.7); + RangeValues values = const RangeValues(0.3, 1.0); await tester.pumpWidget( MaterialApp( @@ -205,13 +205,13 @@ void main() { ), ); - // No thumbs get select when tapping between the thumbs outside the touch + // The closest thumb is selected when tapping between the thumbs outside the touch // boundaries - expect(values, equals(const RangeValues(0.3, 0.7))); + expect(values, equals(const RangeValues(0.3, 1.0))); // taps at 0.5 await tester.tap(find.byType(RangeSlider)); await tester.pump(); - expect(values, equals(const RangeValues(0.3, 0.7))); + expect(values, equals(const RangeValues(0.5, 1.0))); // Get the bounds of the track by finding the slider edges and translating // inwards by the overlay radius. @@ -221,7 +221,7 @@ void main() { // The end thumb is selected when tapping the left inactive track. final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; await tester.tapAt(leftTarget); - expect(values.start, 0.3); + expect(values.start, 0.5); expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01)); // The start thumb is selected when tapping the right inactive track. @@ -233,7 +233,7 @@ void main() { }); testWidgets('Range Slider can move when tapped (discrete LTR)', (WidgetTester tester) async { - RangeValues values = const RangeValues(30, 70); + RangeValues values = const RangeValues(30, 80); await tester.pumpWidget( MaterialApp( @@ -261,13 +261,13 @@ void main() { ), ); - // No thumbs get select when tapping between the thumbs outside the touch + // The closest thumb is selected when tapping between the thumbs outside the touch // boundaries - expect(values, equals(const RangeValues(30, 70))); + expect(values, equals(const RangeValues(30, 80))); // taps at 0.5 await tester.tap(find.byType(RangeSlider)); await tester.pumpAndSettle(); - expect(values, equals(const RangeValues(30, 70))); + expect(values, equals(const RangeValues(50, 80))); // Get the bounds of the track by finding the slider edges and translating // inwards by the overlay radius. @@ -279,7 +279,7 @@ void main() { await tester.tapAt(leftTarget); await tester.pumpAndSettle(); expect(values.start.round(), equals(10)); - expect(values.end.round(), equals(70)); + expect(values.end.round(), equals(80)); // The end thumb is selected when tapping the right inactive track. await tester.pump(); @@ -291,7 +291,7 @@ void main() { }); testWidgets('Range Slider can move when tapped (discrete RTL)', (WidgetTester tester) async { - RangeValues values = const RangeValues(30, 70); + RangeValues values = const RangeValues(30, 80); await tester.pumpWidget( MaterialApp( @@ -319,13 +319,13 @@ void main() { ), ); - // No thumbs get select when tapping between the thumbs outside the touch + // The closest thumb is selected when tapping between the thumbs outside the touch // boundaries - expect(values, equals(const RangeValues(30, 70))); + expect(values, equals(const RangeValues(30, 80))); // taps at 0.5 await tester.tap(find.byType(RangeSlider)); await tester.pumpAndSettle(); - expect(values, equals(const RangeValues(30, 70))); + expect(values, equals(const RangeValues(50, 80))); // Get the bounds of the track by finding the slider edges and translating // inwards by the overlay radius. @@ -336,7 +336,7 @@ void main() { final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; await tester.tapAt(leftTarget); await tester.pumpAndSettle(); - expect(values.start.round(), equals(30)); + expect(values.start.round(), equals(50)); expect(values.end.round(), equals(90)); // The end thumb is selected when tapping the right inactive track. @@ -744,6 +744,7 @@ void main() { final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; await tester.dragFrom(rightTarget, middle - rightTarget); expect(values.start, moreOrLessEquals(50, epsilon: 0.01)); + expect(values.end, moreOrLessEquals(50, epsilon: 0.01)); // Drag the start thumb apart. await tester.pumpAndSettle();