Understanding Android NestedScrolling: Interfaces, Logic Flow, and Implementation Examples
This article explains Android's NestedScrolling mechanism introduced in Android 5.0, detailing the parent‑child interaction flow, key interfaces (NestedScrollingParent and NestedScrollingChild), helper classes, and provides Kotlin/Java code examples for custom views and RecyclerView integration.
Overview
In the traditional event‑dispatch mechanism, usually only one view handles an event, but special requirements—such as coordinated scrolling between multiple views—need a nested scrolling mechanism.
NestedScrolling, introduced in Android 5.0, allows a parent view and a child view to each consume a portion of a scroll gesture. For example, when a RecyclerView is scrolled upward, the header view can first consume part of the scroll (e.g., shrink from 100 dp to 40 dp) and the remaining distance is then handled by the list.
NestedScrolling builds on the existing event‑dispatch system by adding a series of method calls during the touch/scroll process of View and ViewGroup , achieving nested scrolling while still relying on Android's view‑event framework.
Before diving into components like CoordinatorLayout , AppBarLayout , or Behavior , it is useful to understand the two core interfaces: NestedScrollingParent and NestedScrollingChild . Mastering these interfaces enables handling most nested‑scroll scenarios even without deep knowledge of the higher‑level components.
Nested Scrolling Logic Flow
The interaction between a parent ( NestedScrollingParent ) and a child ( NestedScrollingChild ) follows these steps:
During event dispatch, the parent does not intercept; the child handles the event.
Before the child starts scrolling (receiving DOWN ), it asks the parent whether to cooperate. If the parent declines, the subsequent steps are skipped.
When the child receives a MOVE event, it first passes the scroll information to the parent. The parent may consume part or all of the distance and reports the amount consumed back to the child.
The child then processes the remaining distance, possibly consuming the rest and forwarding any leftover to the parent.
If an inertial fling occurs, the child first sends the fling velocity to the parent, which can choose to consume it and inform the child of the result.
If the parent does not consume the fling, the child decides whether to handle it and reports the decision back to the parent.
When the touch/fling ends, both sides are notified that the nested‑scroll sequence has finished.
The following diagram (omitted) visualizes the process.
NestedScrollingParent & NestedScrollingChild Interfaces
Android provides several concrete classes that implement these interfaces, such as CoordinatorLayout , NestedScrollView , and RecyclerView . Below are the key methods of each interface.
NestedScrollingChild
public interface NestedScrollingChild {
void setNestedScrollingEnabled(boolean enabled);
boolean isNestedScrollingEnabled();
boolean startNestedScroll(@ScrollAxis int axes);
void stopNestedScroll();
boolean hasNestedScrollingParent();
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
public interface NestedScrollingChild2 extends NestedScrollingChild {
boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);
void stopNestedScroll(@NestedScrollType int type);
boolean hasNestedScrollingParent(@NestedScrollType int type);
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, @NestedScrollType int type);
}
public interface NestedScrollingChild3 extends NestedScrollingChild2 {
void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);
}Key methods:
setNestedScrollingEnabled / isNestedScrollingEnabled : Enable or query nested‑scroll support.
startNestedScroll : Begin a nested scroll and locate a cooperating parent.
stopNestedScroll : End the nested‑scroll sequence.
dispatchNestedPreScroll : Before the child consumes the scroll, it forwards the distance to the parent.
dispatchNestedScroll : After the child consumes the scroll, it forwards any remaining distance to the parent.
dispatchNestedPreFling / dispatchNestedFling : Same idea for fling (inertial) gestures.
NestedScrollingChild2 and NestedScrollingChild3 add a type parameter to distinguish between touch‑driven and non‑touch (fling) input. The possible types are TYPE_TOUCH and TYPE_NON_TOUCH .
NestedScrollingParent
public interface NestedScrollingParent {
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
void onStopNestedScroll(@NonNull View target);
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
int getNestedScrollAxes();
}
public interface NestedScrollingParent2 extends NestedScrollingParent {
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type);
void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type);
}
public interface NestedScrollingParent3 extends NestedScrollingParent2 {
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);
}Important parent callbacks:
onStartNestedScroll : Called when the child invokes startNestedScroll ; the parent returns true if it wants to cooperate.
onStopNestedScroll : Cleanup after the nested‑scroll ends.
onNestedScrollAccepted : Invoked after the parent has agreed to cooperate, allowing the parent to perform any preparation.
onNestedPreScroll : The parent receives the scroll distance *before* the child consumes it and can consume part of it, reporting the amount via the consumed array.
onNestedScroll : The parent receives the remaining distance *after* the child has consumed its portion.
getNestedScrollAxes : Returns the axes (horizontal/vertical) involved in the nested scroll.
onNestedPreFling / onNestedFling : Similar handling for fling gestures.
In the callbacks, target refers to the current NestedScrollingChild view, while child is the direct child of the parent that contains the target (it may be the target itself).
Nested Scrolling Interface Flow Diagram
(Diagram omitted – it mirrors the ordered list above.)
The interfaces themselves contain no implementation; developers must provide the logic. Android supplies two helper classes— NestedScrollingChildHelper and NestedScrollingParentHelper —that encapsulate the standard coordination logic.
NestedScrollingChildHelper
This helper implements the NestedScrollingChild methods, allowing a custom view to delegate the heavy lifting.
public class NestedScrollingChildHelper {
public void setNestedScrollingEnabled(boolean enabled) { /* ... */ }
public boolean isNestedScrollingEnabled() { /* ... */ }
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) { /* ... */ }
public void stopNestedScroll(@NestedScrollType int type) { /* ... */ }
public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type,
@Nullable int[] consumed) { /* ... */ }
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) { /* ... */ }
// ... other methods ...
}Constructor
public NestedScrollingChildHelper(@NonNull View view) {
mView = view;
}setNestedScrollingEnabled
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
public boolean isNestedScrollingEnabled() {
return mIsNestedScrollingEnabled;
}Enables or disables nested‑scroll support for the child view.
startNestedScroll
public boolean startNestedScroll(@ScrollAxis int axes) {
return startNestedScroll(axes, TYPE_TOUCH);
}
public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}dispatchNestedPreScroll
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow) {
return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);
}
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}
if (dx != 0 || dy != 0) {
int startX = 0, startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = offsetInWindow[1] = 0;
}
}
return false;
}The method forwards the scroll distance to the parent before the child consumes it; the parent writes the amount it consumed back into the consumed array.
NestedScrollingParentHelper
A lightweight helper that stores the axes (horizontal/vertical) for touch and non‑touch scrolls.
public class NestedScrollingParentHelper {
private int mNestedScrollAxesTouch;
private int mNestedScrollAxesNonTouch;
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
@ScrollAxis int axes) {
onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
}
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
mNestedScrollAxesNonTouch = axes;
} else {
mNestedScrollAxesTouch = axes;
}
}
@ScrollAxis
public int getNestedScrollAxes() {
return mNestedScrollAxesTouch | mNestedScrollAxesNonTouch;
}
public void onStopNestedScroll(@NonNull View target) {
onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
}
public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE;
} else {
mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE;
}
}
}Example 1 – Custom NestedScrollingChild
A custom view NestedBottomView implements NestedScrollingChild using NestedScrollingChildHelper . The view tracks the Y coordinate, starts a nested scroll on ACTION_DOWN , forwards pre‑scroll information to the parent, consumes the remaining distance itself, and finally stops the nested scroll on ACTION_UP .
class NestedBottomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), NestedScrollingChild {
private val childHelper by lazy { NestedScrollingChildHelper(this) }
private var lastY = 0f
private val consumed = intArrayOf(0, 0)
init {
isNestedScrollingEnabled = true
}
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
lastY = event.y
if (startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)) {
return true
}
}
MotionEvent.ACTION_MOVE -> {
var dy = (lastY - event.y).toInt()
lastY = event.y
if (dispatchNestedPreScroll(0, dy, consumed, null)) {
dy -= consumed[1]
}
if (dy != 0) {
var translation = translationY - dy
if (translation > 0) translation = 0f
translationY = translation
layoutParams.height = SizeUtil.getScreenHeight(context) - translation.toInt()
requestLayout()
}
dispatchNestedScroll(0, dy, 0, 0, null)
return true
}
MotionEvent.ACTION_UP -> {
stopNestedScroll()
}
}
return super.dispatchTouchEvent(event)
}
// ----- NestedScrollingChild implementation delegating to childHelper -----
override fun setNestedScrollingEnabled(enabled: Boolean) {
childHelper.isNestedScrollingEnabled = enabled
}
override fun isNestedScrollingEnabled(): Boolean = childHelper.isNestedScrollingEnabled
override fun startNestedScroll(axes: Int): Boolean = childHelper.startNestedScroll(axes)
override fun stopNestedScroll() = childHelper.stopNestedScroll()
override fun hasNestedScrollingParent(): Boolean = childHelper.hasNestedScrollingParent()
override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int,
dyUnconsumed: Int, offsetInWindow: IntArray?): Boolean =
childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean =
childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
}Example 2 – Using RecyclerView as NestedScrollingChild
Android already provides a ready‑made RecyclerView that implements NestedScrollingChild . The same parent layout ( NestedContainerView ) can be reused; only the child view type changes.
class NestedContainerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), NestedScrollingParent {
private val parentHelper by lazy { NestedScrollingParentHelper(this) }
private lateinit var topView: View
private lateinit var bottomView: RecyclerView
private val topMinHeight = SizeUtil.dp2px(50f)
private val topMaxHeight = SizeUtil.dp2px(200f)
override fun onFinishInflate() {
super.onFinishInflate()
topView = findViewById(R.id.top_view)
bottomView = findViewById(R.id.bottom_view)
bottomView.setViewHeight(SizeUtil.getScreenHeight(context) - topMinHeight)
}
override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean =
(nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0
override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
parentHelper.onNestedScrollAccepted(child, target, axes)
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
val isBottomAtTop = (bottomView.layoutManager as? LinearLayoutManager)
?.findFirstCompletelyVisibleItemPosition() == 0
val canScrollUp = dy > 0 && topView.height > topMinHeight
val canScrollDown = dy < 0 && topView.height < topMaxHeight && isBottomAtTop
if (canScrollUp || canScrollDown) {
val newHeight = (topView.height - dy).coerceIn(topMinHeight, topMaxHeight)
topView.layoutParams.height = newHeight
topView.requestLayout()
consumed[1] = dy
} else {
consumed[1] = 0
}
}
override fun onStopNestedScroll(child: View) {
parentHelper.onStopNestedScroll(child)
}
override fun getNestedScrollAxes(): Int = parentHelper.nestedScrollAxes
}Conclusion
The article introduced the NestedScrolling mechanism, explained the interaction between NestedScrollingParent and NestedScrollingChild , and demonstrated how to implement custom nested‑scrolling views as well as how to leverage existing components like RecyclerView . Future articles will explore more complex examples and dissect the inner workings of CoordinatorLayout and Behavior components.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.