跪拜 Guibai
← All articles
Android

Nested ScrollView Inner-First Scrolling Without the Fight

By 雨白 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Nested scrolling is a recurring source of jank in Android apps. This pattern gives developers a direct, copy-pasteable custom view that avoids the gesture fights and coordinate bugs that make nested scroll containers feel broken.

Summary

Nesting two scrollable containers and getting the inner one to scroll first — then smoothly handing control to the outer container at the top or bottom — requires overriding `dispatchTouchEvent`, not `onTouchEvent`. The inner view must claim events on `ACTION_DOWN`, check `canScrollVertically` on `ACTION_MOVE`, and release its hold only when it hits a boundary. Using `ev.rawY` instead of `ev.y` prevents coordinate jumps that cause the two containers to fight over the gesture.

A common mistake is placing the logic in `onTouchEvent`. ScrollView's internal interception will send `ACTION_CANCEL` to a child that consumed `ACTION_DOWN` once the finger moves past the touch slop, so `onTouchEvent` never sees the move events. Moving everything into `dispatchTouchEvent` guarantees the inner view can intercept before the parent does.

A complete `NestedLogScrollView` implementation is provided, along with a demo Activity that appends log lines into the inner scroll area while the outer ScrollView holds surrounding content. The approach works for any `ScrollView` subclass and avoids the jitter that comes from misusing relative coordinates.

Takeaways
Place inner-first interception logic in `dispatchTouchEvent`, not `onTouchEvent`, because ScrollView will cancel events sent to a child that consumed `ACTION_DOWN` once the touch slop is exceeded.
On `ACTION_DOWN`, immediately call `parent?.requestDisallowInterceptTouchEvent(true)` to claim the gesture stream.
On `ACTION_MOVE`, compare `ev.rawY` (absolute screen coordinate) rather than `ev.y` to avoid coordinate jumps caused by container scrolling.
Use `canScrollVertically(-1)` to check if there is still content above, and `canScrollVertically(1)` to check below; release the parent's interception only when the boundary is reached.
Filter out movements smaller than `ViewConfiguration.get(context).scaledTouchSlop` to prevent jitter from tiny finger motions.
On `ACTION_UP` or `ACTION_CANCEL`, restore the parent's interception with `parent?.requestDisallowInterceptTouchEvent(false)`.
Conclusions

ScrollView's internal interception mechanism makes `onTouchEvent` a trap for nested scrolling: the framework will forcibly steal events from a child that consumed the down event, so any logic placed there is bypassed.

Using absolute screen coordinates (`rawY`) is the critical detail that prevents the inner and outer containers from oscillating — relative coordinates change as the container scrolls, creating feedback loops in the gesture decision.

The `canScrollVertically` check with a `-1` offset for boundary tolerance is a small but necessary guard against off-by-one errors that would otherwise block the handoff to the outer container.

Concepts & terms
requestDisallowInterceptTouchEvent
A method on View that tells its parent ViewGroup not to intercept touch events. The child calls it with `true` to claim the gesture stream, or `false` to release it back to the parent.
dispatchTouchEvent vs onTouchEvent
`dispatchTouchEvent` is the first method called in the event delivery pipeline, before any interception or handling. `onTouchEvent` is called later and only if the view is the target. For nested scrolling control, `dispatchTouchEvent` is the correct override point because it runs before the parent can intercept.
canScrollVertically
A View method that checks whether the view can scroll in a given vertical direction. A negative argument checks upward (content above), a positive argument checks downward (content below). It compares the current scroll offset against the total scrollable range.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗