Repository navigation
[material_ui] Fix discrete Slider selecting the neighboring tick near the track ends - #12927
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the coordinate calculations for discrete Slider and RangeSlider widgets with rounded tracks to ensure that tap and drag gestures align correctly with visual tick positions. The changes introduce padding adjustments in _getValueFromGlobalPosition and drag-delta calculations, and add corresponding regression tests. Review feedback suggests replacing null-assertions on slider theme track shapes with optional chaining to prevent potential runtime crashes, and handling potential division-by-zero cases when the track width is zero or negative.
| if (isDiscrete && _sliderTheme.rangeTrackShape!.isRounded) { | ||
| final double padding = _trackRect.height; | ||
| final double adjustedWidth = _trackRect.width - padding; | ||
| if (adjustedWidth <= 0.0) { | ||
| visualPosition = 0.5; | ||
| } else { | ||
| visualPosition = (localDx - _trackRect.left - padding / 2) / adjustedWidth; | ||
| } | ||
| } else { | ||
| visualPosition = (localDx - _trackRect.left) / _trackRect.width; | ||
| } |
There was a problem hiding this comment.
Using the null-assert operator (!) on _sliderTheme.rangeTrackShape can lead to a runtime crash if the range track shape is null in the theme. It is safer to use optional chaining (?.) with a fallback value to ensure robustness. Additionally, handling the case where _trackRect.width is <= 0.0 in the else branch prevents potential division-by-zero errors.
if (isDiscrete && (_sliderTheme.rangeTrackShape?.isRounded ?? false)) {
final double padding = _trackRect.height;
final double adjustedWidth = _trackRect.width - padding;
if (adjustedWidth <= 0.0) {
visualPosition = 0.5;
} else {
visualPosition = (localDx - _trackRect.left - padding / 2) / adjustedWidth;
}
} else {
visualPosition = _trackRect.width <= 0.0
? 0.5
: (localDx - _trackRect.left) / _trackRect.width;
}| double get _discreteRoundedTrackPadding => | ||
| (isDiscrete && _sliderTheme.trackShape!.isRounded) ? _trackRect.height : 0.0; |
There was a problem hiding this comment.
Using the null-assert operator (!) on _sliderTheme.trackShape can lead to a runtime crash if the track shape is null in the theme. It is safer to use optional chaining (?.) with a fallback value to ensure robustness.
double get _discreteRoundedTrackPadding =>
(isDiscrete && (_sliderTheme.trackShape?.isRounded ?? false)) ? _trackRect.height : 0.0;
QuncCccccc
left a comment
There was a problem hiding this comment.
Thanks for your contribution. Seems the comments from the original PRs haven't been addressed, so I just copied and pasted here:)
|
|
||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| theme: ThemeData(useMaterial3: true), |
There was a problem hiding this comment.
We can remove this line because useMaterial3 is true by default.
|
|
||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| theme: ThemeData(useMaterial3: false), |
There was a problem hiding this comment.
Seems this test is only checking M2. M3 might be more important to test since M2 has become a out of date style. We can remove this line to test the default value - M3.
|
|
||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| theme: ThemeData(useMaterial3: false), |
|
|
||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| theme: ThemeData(useMaterial3: false), |
QuncCccccc
left a comment
There was a problem hiding this comment.
Thanks for the fix! I ran the new tests against main: all 5 of them, and the rest of slider_test.dart, also pass with the lib/ changes reverted, so they don't cover the bug. Could you update the tests so they fail without the fix?
|
Thanks for catching that. I confirmed the old tests passed with Pushed 5e3b8f6 with these changes:
On the Gemini suggestions: I left the |
QuncCccccc
left a comment
There was a problem hiding this comment.
I tried the sample code in the original issue locally and I don't think this PR fixes the original issue.
Screen.Recording.2026-10-09.at.12.36.12.PM.mov
As I mentioned in flutter/flutter#184391 (comment), I think the problem is that the author doesn't provide a valid range for division.
But this PR does fix a real bug. On a discrete slider with a rounded track, tapping a tick can select the next tick instead. For example, with a 40 px track, a 320 px wide slider and 10 divisions:
- Tapping tick 0 moves the handle to 1. To get 0, you have to tap before tick 0.
- Tapping tick 1 moves the handle to 2. To get 1, you have to tap between tick 0 and tick 1.
- The same happens at the other end: tapping tick 10 moves the handle to 9, and tapping tick 9 moves it to 8.
- Ticks in the middle work fine.
This happens because the ticks are drawn a little inside the ends of the track, but a tap is measured against the full length of the track.
So could you please:
- File a new issue for this bug, with a small code sample.
- Update the PR CHANGELOG entry and the description to say it fixes that new issue.
- Remove "Fixes flutter/flutter#184391" (it's in the description twice), and attach the correct issue.
|
Filed flutter/flutter#194855 for the tap-near-the-ends bug with a small repro, and switched the description and changelog over to it. Dropped the #184391 references since this doesn't address that one. Should be ready for another look. |
QuncCccccc
left a comment
There was a problem hiding this comment.
LGTM. Thank you for the fix:)!
…ick marks Port of flutter/flutter#188012. Tick marks and the thumb on discrete rounded tracks are painted in a coordinate space padded by half a track height on each end, but the gesture math used an unpadded formula, so lower-half tick taps could snap to the wrong (lower) division. Mirrors the padded geometry in _getValueFromGlobalPosition (Slider and RangeSlider) and in the drag-delta normalizer so tap and drag stay in the same coordinate space.
The previous tests tapped exactly on tick centers, where the old and new coordinate mappings differ by only a couple of pixels, so they passed even with the fix reverted. Tap just either side of each visual midpoint instead, test the default (Material 3) theme and the gapped track, cover the drag delta normalizer with a full-track drag, and add a RangeSlider test.
4a4061e to
ae4d3a1
Compare
…rete_slider_tap_wrong_value.yaml
…r#195977) flutter/packages@1df8447...670b30e 2026-11-04 36861262+QuncCccccc@users.noreply.github.com [material_ui] Inherit `IconButtonThemeData.variant` from enclosing themes (flutter/packages#13263) 2026-11-04 katelovett@google.com [tool] Regenerate `MockGitDir` for `package:git` 2.5.0 (flutter/packages#13544) 2026-11-03 hkarmoush@gmail.com [material_ui] Fix discrete Slider selecting the neighboring tick near the track ends (flutter/packages#12927) 2026-11-03 32538273+ValentinVignal@users.noreply.github.com [material_ui] Migrate M3 Time Picker template to use new gen_defaults (flutter/packages#13164) 2026-11-03 269567208+reidbaker-agent@users.noreply.github.com [camera_android_camerax] Update skills_lint to 0.5.3 (flutter/packages#13530) 2026-11-03 engine-flutter-autoroll@skia.org Roll Flutter from fcfb4f2 to 9347202 (38 revisions) (flutter/packages#13540) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC flutter-ecosystem@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Ports flutter/flutter#188012 to
material_uinow that the Material/Cupertino code freeze has lifted, per the instructions in flutter/flutter#188444.Original PR description:
Fixes flutter/flutter#194855
Tick marks and the thumb on discrete rounded tracks are painted in a padded coordinate space:
where
padding = trackHeight(4 px on M2'sRoundedRectSliderTrackShape, up to 16 px on M3'sGappedSliderTrackShape). But tap detection inverted a different, unpadded formula, so the snap midpoints in tap space diverged from the visual midpoints between tick marks. Lower-half ticks consistently landed in the wrong snap bucket.Fix:
_getValueFromGlobalPositionnow uses the padded inverse formula whenisDiscrete && trackShape.isRounded. The drag-delta normalizer in_handleDragUpdateis updated to match so a tap followed immediately by a drag is consistent.Applied to both
SliderandRangeSlider.Tests
Added regression tests to
slider_test.dartandrange_slider_test.dart. They fail without the fix and pass with it:Slidersnaps at the visual midpoint between every pair of ticks, tapping 0.05px either side of the midpoint, for the default Material 3 track (LTR and RTL) and the gapped track fromSlider.year2023: false(LTR and RTL), which has the largest padding. Tapping exactly on a tick center can't expose the bug, because the old and new mappings differ by only a couple of pixels there.RangeSlidersnaps at the visual midpoint between ticks for both thumbs.Slider.All of
slider_test.dartandrange_slider_test.dartpass locally.Fixes flutter/flutter#194855
Closes flutter/flutter#188012 (superseded by this PR)
Pre-Review Checklist
[shared_preferences]///).Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2