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 early
onNestedPreFling 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+
Description:
BottomSheetBehaviorfling-to-dismiss doesn't work when the sheet contains a nested scrolling child (RecyclerView, ScrollView, etc.). Works fine for direct drags.isHideableis true in both cases.There are two bugs here and they're independent of each other.
1.
activePointerIdgets reset beforeonStopNestedScrollreads itonInterceptTouchEvent(ACTION_UP)resetsactivePointerIdtoINVALID_POINTER_ID(line 665). This fires before the child processesACTION_UPand callsstopNestedScroll(). So whenonStopNestedScrolleventually callsgetYVelocity()(line 887), it queriesvelocityTracker.getYVelocity(-1)and gets 0.2.
onNestedPreFlingdiscards velocityonNestedPreFling(line 948) gets the real fling velocity as a parameter, consumes it by returningtrue, 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,
ViewDragHelperhandles everything and passes velocity toonViewReleased()directly. That path works.Code paths
Direct touch (works):
Nested scroll (broken):
skipCollapsed = truePosition-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 usingchild.getTop() + yvel * hideFriction.Source code:
onInterceptTouchEventresetsactivePointerIdbeforeonStopNestedScrollruns:onNestedPreFlingreceives velocity but doesn't keep it:onStopNestedScrolltries to use the velocity:getYVelocityqueries with the dead pointer ID:Debug logs from a subclassed
BottomSheetBehaviorwith logging inonNestedPreFlingandonStopNestedScroll:Android API version: 35
Material Library version: 1.12.0
Device: Samsung S21+