Repository navigation
[BottomSheet] Velocity-based dismiss does not work when fling originates from nested scrolling child #5076
Description
Description:
BottomSheetBehavior fling-to-dismiss doesn't work when the sheet contains a nested scrolling child (RecyclerView, ScrollView, etc.). Works fine for direct drags. isHideable is true in both cases.
There are two bugs here and they're independent of each other.
1. activePointerId gets reset before onStopNestedScroll reads it
onInterceptTouchEvent(ACTION_UP) resets activePointerId to INVALID_POINTER_ID (line 665). This fires before the child processes ACTION_UP and calls stopNestedScroll(). So when onStopNestedScroll eventually calls getYVelocity() (line 887), it queries velocityTracker.getYVelocity(-1) and gets 0.
2. onNestedPreFling discards velocity
onNestedPreFling (line 948) gets the real fling velocity as a parameter, consumes it by returning true, and never stores it anywhere.
Between these two, shouldHide() (line 1707) always sees velocity 0 on the nested scroll path. It falls back to position only (Math.abs(child.getTop() - collapsedOffset) / peek > 0.5). Fast short flings don't move the sheet far enough to pass that, so it snaps back to collapsed.
For direct touches, ViewDragHelper handles everything and passes velocity to onViewReleased() directly. That path works.
Code paths
Direct touch (works):
onTouchEvent
→ ViewDragHelper.processTouchEvent
→ dragCallback.onViewReleased(child, xvel, yvel) // real velocity
→ shouldHide(child, yvel)
→ STATE_HIDDEN
Nested scroll (broken):
onNestedPreScroll // moves sheet, STATE_DRAGGING
→ onInterceptTouchEvent(ACTION_UP) // activePointerId = -1
→ onNestedPreFling(velocityY) // real velocity, discarded
→ onStopNestedScroll
→ getYVelocity() // pointer -1 → returns 0
→ shouldHide(child, 0.0) // position-only → false
→ STATE_COLLAPSED
| Gesture | Direct touch | Nested scroll |
|---|---|---|
| Slow drag past midpoint | Hides | Hides |
| Fast short fling down | Hides | Settles to collapsed |
skipCollapsed = true |
Hides | Hides |
Position-based dismiss still works. Only velocity-based dismiss is broken through nested scroll.
Expected behavior:
A fast downward fling on scrollable content (scrolled to top) should dismiss the sheet the same way it does on non-scrollable content. shouldHide() projects position using child.getTop() + yvel * hideFriction.
Source code:
onInterceptTouchEvent resets activePointerId before onStopNestedScroll runs:
// line 661
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
touchingScrollingChild = false;
currentTouchedScrollChildRef = null;
activePointerId = MotionEvent.INVALID_POINTER_ID; // reset too earlyonNestedPreFling receives velocity but doesn't keep it:
// line 948
public boolean onNestedPreFling(..., float velocityX, float velocityY) {
if (isNestedScrollingCheckEnabled() && hasScrollingChild()) {
return isViewScrollingChild(target)
&& ((state != STATE_EXPANDED && !draggableOnNestedScrollLastDragIgnored)
|| super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY));
// velocityY not stored
} else {
return false;
}
}onStopNestedScroll tries to use the velocity:
// line 887
} else if (hideable && shouldHide(child, getYVelocity())) {
targetState = STATE_HIDDEN;getYVelocity queries with the dead pointer ID:
// line 1954
private float getYVelocity() {
if (velocityTracker == null) {
return 0;
}
velocityTracker.computeCurrentVelocity(1000, maximumVelocity);
return velocityTracker.getYVelocity(activePointerId); // -1 → 0
}Debug logs from a subclassed BottomSheetBehavior with logging in onNestedPreFling and onStopNestedScroll:
Nested scroll path (fling on scrollable content at top of scroll):
[onNestedPreFling] velocity: 2121 dp/s DOWN, state: STATE_DRAGGING, isHideable: true
[onNestedPreFling] super returned: true (consumed)
[onStopNestedScroll] state: STATE_DRAGGING, sheetTop: 699
[onStopNestedScroll] after super — state: STATE_SETTLING ← not STATE_HIDDEN
Direct touch path (same gesture on non-scrollable content):
[onTouchEvent] ACTION_UP velocity: 3328 dp/s DOWN, state: STATE_DRAGGING, isHideable: true, sheetTop: 1353
[onTouchEvent] STATE_DRAGGING → STATE_SETTLING → STATE_HIDDEN ← works
Android API version: 35
Material Library version: 1.12.0
Device: Samsung S21+
Activity