跪拜 Guibai
← All articles
Android

Building a Zhihu-Style Collapsing Header with Android's NestedScrolling Protocol

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

CoordinatorLayout abstracts away nested scrolling, but understanding the raw protocol is essential when its behavior doesn't match a design spec or when building custom containers that need to coordinate scroll consumption across multiple children. The V2/V3 protocol's conversion of fling into per-frame scroll events is the detail that makes inertial scrolling feel continuous in a collapsing header, and getting the consumed-pixel accounting wrong produces visible glitches.

Summary

Android's traditional event dispatch cannot hand off a scroll gesture from a parent to a child mid-gesture without lifting the finger, which breaks the continuous collapse-then-scroll behavior seen in apps like Zhihu. The NestedScrolling protocol solves this by letting the child View drive the coordination, asking the parent how much of each scroll delta it wants to consume before the child uses the remainder.

The implementation walks through a custom `MyCollapsingLayout` that implements `NestedScrollingParent3`. Measurement logic ensures the RecyclerView height accounts for the always-visible toolbar and sticky header so no blank space appears after the profile collapses. Layout uses `translationY` offsets rather than resizing to avoid repeated layout passes and jank. The core callbacks, `onNestedPreScroll` and `onNestedScroll`, consume vertical deltas to collapse or expand the header while returning exact consumed pixel values to prevent scroll misalignment.

Fling transitions work seamlessly because the V2/V3 protocol converts inertial scrolling into `TYPE_NON_TOUCH` scroll events dispatched through the same `onNestedPreScroll` path. The piece also traces the protocol's evolution from V1's broken fling handling through V3's precise post-scroll consumption feedback.

Takeaways
Traditional event dispatch cannot transfer scroll ownership mid-gesture; the NestedScrolling protocol lets the child drive coordination by querying the parent before and after consuming scroll deltas.
Setting `android:overScrollMode="never"` on the RecyclerView prevents the edge glow effect from blocking nested scroll event handoff and causing stutter.
The RecyclerView's measured height must equal the container height minus the toolbar and sticky header heights, or a blank gap appears after the profile collapses.
Collapsing is implemented via `translationY` offsets on the profile, sticky header, and list, avoiding `requestLayout` calls that cause jank during scroll.
In `onNestedPreScroll` and `onNestedScroll`, the `consumed` array must report the exact pixel count actually consumed; any mismatch desynchronizes the child's scroll position.
Fling continuity requires no manual handling in V2/V3 because the framework converts inertial scrolling into `TYPE_NON_TOUCH` scroll events dispatched through the same pre-scroll callback.
V1's fling was a one-shot handoff to either parent or child; V2 added a `type` parameter to distinguish touch from non-touch scroll; V3 added a `consumed` array to `dispatchNestedScroll` for precise post-scroll feedback.
Conclusions

CoordinatorLayout's Behavior-based API hides the parent-child negotiation that NestedScrolling makes explicit, so developers who only know CoordinatorLayout often can't debug scroll glitches when a custom Behavior misreports consumed deltas.

The protocol's design inverts the traditional Android event model: instead of the parent intercepting from the child, the child volunteers scroll distance to the parent, which is why it can switch consumers mid-gesture without a finger lift.

Returning `false` from `onNestedPreFling` and `onNestedFling` while relying on the V2/V3 `TYPE_NON_TOUCH` dispatch is a deliberate design choice that eliminates an entire class of fling-continuity bugs present in V1 implementations.

Concepts & terms
NestedScrolling Protocol
An Android framework mechanism where a scrollable child View (NestedScrollingChild) proactively offers scroll deltas to an ancestor (NestedScrollingParent) before and after consuming them, enabling coordinated scrolling within a single gesture.
NestedScrollingParent3
The third version of the parent-side nested scrolling interface. V2 added a `type` parameter to distinguish touch scrolls from fling-generated scrolls; V3 added a `consumed` output array to `dispatchNestedScroll` so the parent can report exactly how many pixels it consumed after the child finished scrolling.
TYPE_NON_TOUCH
A scroll type constant indicating the motion was generated by inertial scrolling (fling) rather than direct finger contact. In V2/V3, the framework decomposes a fling into a sequence of TYPE_NON_TOUCH scroll events, allowing the same pre-scroll and scroll callbacks to handle both touch and inertia seamlessly.
translationY
A View property that offsets the visual position of a view vertically without triggering a layout pass. Used in collapsing headers to move content off-screen smoothly, avoiding the performance cost of repeated `requestLayout` calls.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗