Skip to content

[BottomSheet] Velocity-based dismiss does not work when fling originates from nested scrolling child #5076

@harveylx

Description

@harveylx

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+

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions