Skip to content

[material_ui] Fix discrete Slider selecting the neighboring tick near the track ends - #12927

Merged
auto-submit[bot] merged 4 commits into
flutter:mainfrom
hkarmoush:fix/184391-slider-discrete-tap-wrong-value
Nov 3, 2026
Merged

auto-submit[bot] merged 4 commits into
flutter:mainfrom
hkarmoush:fix/184391-slider-discrete-tap-wrong-value

Conversation

@hkarmoush

@hkarmoush hkarmoush commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Ports flutter/flutter#188012 to material_ui now 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:

dx = trackLeft + i/d * (trackWidth − padding) + padding/2

where padding = trackHeight (4 px on M2's RoundedRectSliderTrackShape, up to 16 px on M3's GappedSliderTrackShape). 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: _getValueFromGlobalPosition now uses the padded inverse formula when isDiscrete && trackShape.isRounded. The drag-delta normalizer in _handleDragUpdate is updated to match so a tap followed immediately by a drag is consistent.

Applied to both Slider and RangeSlider.

Tests

Added regression tests to slider_test.dart and range_slider_test.dart. They fail without the fix and pass with it:

  • Slider snaps 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 from Slider.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.
  • A full-track drag after a tap on a 100-division gapped slider, which covers the drag delta normalizer.
  • RangeSlider snaps at the visual midpoint between ticks for both thumbs.
  • A tick-center sanity check for Slider.

All of slider_test.dart and range_slider_test.dart pass locally.


Fixes flutter/flutter#194855

Closes flutter/flutter#188012 (superseded by this PR)

Pre-Review Checklist

Footnotes

  1. 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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +1426 to +1436
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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;
    }

Comment on lines +1301 to +1302
double get _discreteRoundedTrackPadding =>
(isDiscrete && _sliderTheme.trackShape!.isRounded) ? _trackRect.height : 0.0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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;

@dkwingsmt dkwingsmt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@dkwingsmt dkwingsmt added the CICD Run CI/CD label Sep 18, 2026

@QuncCccccc QuncCccccc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can remove this line because useMaterial3 is true by default.


await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here.


await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here.

@QuncCccccc QuncCccccc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

@QuncCccccc QuncCccccc added the waiting for response The Flutter team cannot make further progress on this PR until the author responds label Oct 2, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 3, 2026
@hkarmoush hkarmoush changed the title Fix discrete Slider snapping to wrong value when tapping lower-half tick marks [material_ui] Fix discrete Slider snapping to wrong value when tapping lower-half tick marks Oct 3, 2026
@hkarmoush

Copy link
Copy Markdown
Contributor Author

Thanks for catching that. I confirmed the old tests passed with lib/ reverted: they tapped exactly on tick centers, where the old and new mappings differ by only a couple of pixels, so they could never fail.

Pushed 5e3b8f6 with these changes:

  • Tests now tap 0.05px either side of the visual midpoint between every pair of ticks, which is where the old mapping puts the snap boundary in the wrong place. I reverted lib/ to the base commit and confirmed the new tests fail (the midpoint tests and the full-track drag test), and pass with the fix.
  • Removed the explicit useMaterial3: false, so the default M3 theme is tested. Also added the Slider.year2023: false gapped track (16px padding), each in LTR and RTL.
  • The drag test uses 100 divisions and a full-track drag so a width mismatch in the delta normalizer can't be hidden by rounding.
  • Added a RangeSlider test, since the fix covers both widgets and only Slider was tested before.

On the Gemini suggestions: I left the ! on trackShape and rangeTrackShape as is. Both are always non-null after theme resolution, and the surrounding code (_trackRect, paint) already relies on that. The zero-width guard is already in place for Slider. I also updated the PR title and description, including the checklist.

@github-actions github-actions Bot removed the waiting for response The Flutter team cannot make further progress on this PR until the author responds label Oct 3, 2026
@QuncCccccc QuncCccccc added the CICD Run CI/CD label Oct 6, 2026
@QuncCccccc
QuncCccccc self-requested a review October 6, 2026 21:24

@QuncCccccc QuncCccccc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  1. File a new issue for this bug, with a small code sample.
  2. Update the PR CHANGELOG entry and the description to say it fixes that new issue.
  3. Remove "Fixes flutter/flutter#184391" (it's in the description twice), and attach the correct issue.

@QuncCccccc QuncCccccc added the waiting for response The Flutter team cannot make further progress on this PR until the author responds label Oct 9, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 20, 2026
@github-actions github-actions Bot removed the waiting for response The Flutter team cannot make further progress on this PR until the author responds label Oct 20, 2026
@hkarmoush

Copy link
Copy Markdown
Contributor Author

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 QuncCccccc changed the title [material_ui] Fix discrete Slider snapping to wrong value when tapping lower-half tick marks [material_ui] Fix discrete Slider selecting the neighboring tick near the track ends Oct 21, 2026

@QuncCccccc QuncCccccc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
@QuncCccccc
QuncCccccc force-pushed the fix/184391-slider-discrete-tap-wrong-value branch from 4a4061e to ae4d3a1 Compare October 21, 2026 18:50
@QuncCccccc QuncCccccc added the CICD Run CI/CD label Oct 21, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 21, 2026
@QuncCccccc QuncCccccc added the CICD Run CI/CD label Oct 21, 2026
@QuncCccccc QuncCccccc added the autosubmit Merge PR when tree becomes green via auto submit App label Nov 3, 2026
@auto-submit
auto-submit Bot merged commit e244038 into flutter:main Nov 3, 2026
13 checks passed
jpohhhh pushed a commit to Telosnex/flutter that referenced this pull request Nov 4, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: material_ui triage-design Should be looked at in design triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[material_ui] Discrete Slider with rounded track selects the neighbouring division when tapping ticks near the ends

3 participants