Skip to content

WheelPicker stops between two items when fling is interrupted by touch#184

Open
daihieptn97 wants to merge 3 commits intodevaige:mainfrom
daihieptn97:WheelPicker-stops-between-two-items
Open

WheelPicker stops between two items when fling is interrupted by touch#184
daihieptn97 wants to merge 3 commits intodevaige:mainfrom
daihieptn97:WheelPicker-stops-between-two-items

Conversation

@daihieptn97
Copy link

Fix: WheelPicker stops between two items when fling is interrupted by touch

Problem

When the user swipes the wheel strongly (triggering a fling), then immediately taps the wheel to stop it, the picker halts at an intermediate position between two items. As a result:

  • No item gets selected (OnItemSelectedListener is not triggered)
  • The wheel visually sits between two rows with no clear selection
  • The user must scroll again to land on a valid item

This happens because ACTION_DOWN calls scroller.abortAnimation(), which stops the scroll immediately at whatever scrollOffsetY value the fling was at — without snapping to the nearest item boundary. When the finger lifts (ACTION_UP), the event is treated as a click (isClick = true), so the snap logic inside the ACTION_UP block is never executed.

Root Cause

In onTouchEvent, the ACTION_DOWN branch:

MotionEvent.ACTION_DOWN -> {
    if (!scroller.isFinished) {
        scroller.abortAnimation()  // stops at arbitrary scrollOffsetY
        isForceFinishScroll = true
        // ← no snap correction here
    }
    ...
}

After abortAnimation(), scrollOffsetY can be a value like 347 when itemHeight is 100, leaving the wheel stuck 47px between two items. Since the subsequent ACTION_UP detects a tap (not a scroll), it skips the snap logic entirely.

Fix

After calling scroller.abortAnimation() in ACTION_DOWN, immediately correct scrollOffsetY to the nearest item boundary using the existing computeDistanceToEndPoint() helper:

MotionEvent.ACTION_DOWN -> {
    parent?.requestDisallowInterceptTouchEvent(true)
    if (!scroller.isFinished) {
        scroller.abortAnimation()
        isForceFinishScroll = true

        // Snap scrollOffsetY to the nearest item boundary
        // so the wheel does not freeze between two items
        val remainder = scrollOffsetY.rem(itemHeight)
        val snapOffset = computeDistanceToEndPoint(remainder)
        if (snapOffset != 0) {
            scrollOffsetY += snapOffset
            if (!isCyclic) {
                scrollOffsetY = scrollOffsetY.coerceIn(minFlingY, maxFlingY)
            }
            invalidate()
        }
    }
    isScrolling = false
    lastPointY = event.y.toInt()
    downPointY = lastPointY
}

Why this approach

  • Reuses the existing computeDistanceToEndPoint() method — no new logic introduced
  • Applies the clamp guard (coerceIn) already used in ACTION_UP for non-cyclic mode
  • Calls invalidate() so the wheel visually snaps before the user lifts their finger
  • Does not affect normal scroll or fling behaviour — only runs when a fling is actively interrupted

Steps to Reproduce

  1. Populate the WheelPicker with any dataset (5+ items)
  2. Swipe the wheel quickly downward to trigger a fling
  3. While the wheel is still spinning, tap and hold briefly, then release
  4. Expected: Wheel snaps to the nearest item and fires OnItemSelectedListener
  5. Actual: Wheel stops between two items; no selection is reported

Testing

Tested on:

  • Android API 26 (Oreo)
  • Android API 10 (Quince Tart)
  • Both cyclic and non-cyclic modes
  • Physical device and emulator

No regressions observed in normal tap, slow scroll, or fast fling scenarios.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant