Skip to content

Fix discrete Slider snapping to wrong value when tapping lower-half tick marks - #12927

Open
hkarmoush wants to merge 1 commit into
flutter:mainfrom
hkarmoush:fix/184391-slider-discrete-tap-wrong-value
Open

hkarmoush wants to merge 1 commit into
flutter:mainfrom
hkarmoush:fix/184391-slider-discrete-tap-wrong-value

Conversation

@hkarmoush

Copy link
Copy Markdown

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#184391

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

Ported the five regression tests from the original PR to slider_test.dart, covering M2/M3 LTR, RTL, tap-then-drag, and near-tick-boundary taps. All existing slider_test.dart (112) and range_slider_test.dart (69) tests pass locally.


Fixes flutter/flutter#184391

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

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

Slider selects the wrong value when tapping on lower-half divisions

3 participants