After Android released version 5.0 (Lollipop), Google provided us with the feature of nested sliding. Next, we analyze the implementation mechanism of Android nested sliding from the perspective of source code.
First, let's look at the implementation of four core classes related to nested sliding:
NestedScrollingChild
package android.support.v4.view; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; public interface NestedScrollingChild { /** * Set whether nested sliding is allowed * * @param enabled */ public void setNestedScrollingEnabled(boolean enabled); /** * Determine whether nested sliding is allowed * * @return true if nested scrolling is enabled */ public boolean isNestedScrollingEnabled(); /** * Start nested sliding * * @param axes Represents the sliding direction (3 possible values) * ViewCompat#SCROLL_AXIS_HORIZONTAL * ViewCompat#SCROLL_AXIS_VERTICAL * ViewCompat#SCROLL_AXIS_HORIZONTAL | ViewCompat#SCROLL_AXIS_VERTICAL * @return true if a cooperative parent was found and nested scrolling has been enabled for the current gesture. */ public boolean startNestedScroll(int axes); /** * Stop nested sliding */ public void stopNestedScroll(); /** * Determine whether the parent class supports nested sliding */ public boolean hasNestedScrollingParent(); /** * ChildView Called after scroll is executed, notifying ParentView of its actual sliding distance and unused sliding distance * * @param dxConsumed ChildView The actual sliding distance on the x-axis * @param dyConsumed ChildView The actual sliding distance on the y-axis * @param dxUnconsumed ChildView Unconsumed distance on the x-axis (General ParentView slides on the x-axis of the data processing itself) * @param dyUnconsumed ChildView Unconsumed distances on the y-axis (General ParentView slides on the y-axis itself based on this data processing) * @param offsetInWindow ChildView Form offset */ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); /** * ChildView Before scroll is executed, it is used to notify ParentView of the distance it intends to slide (called in onInterceptTouchEvent or onTouch) * * @param dx ChildView The distance intended to slip on the x-axis * @param dy ChildView The Distance of Intention to Slide on the y Axis * @param consumed Output parameters to record the sliding distance of ParentView consumption (x axis: consumed[0], y axis: consumed[1]) * @param offsetInWindow ChildView Form offset */ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); /** * ChildView Call when fling slides, notify ParentView * * @param velocityX ChildView The fling rate on the x-axis * @param velocityY ChildView The fling rate on the y-axis * @param consumed ParentView Whether to consume this fling operation * @return true if the nested scrolling parent consumed or otherwise reacted to the fling */ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); /** * ChildView Call before fling operation to notify ParentView of its fling rate * * @param velocityX ChildView The fling rate on the x-axis * @param velocityY ChildView The fling rate on the y-axis * @return true if a nested scrolling parent consumed the fling */ public boolean dispatchNestedPreFling(float velocityX, float velocityY); }
The method description of NestedScrollingChild interface mentioned above is more detailed. When implementing nested sliding, ChildView must implement this interface. By looking at the RecycleView source code, we can see that the interface implementation of NestedScrollingChild is entirely implemented by the NestedScrollingChildHelper agent. So let's analyze the following NestedScrolling ChildHelper code implementation!
NestedScrollingChildHelper
package android.support.v4.view; import android.view.View; import android.view.ViewParent; public class NestedScrollingChildHelper { /** * Helper The corresponding ChildView */ private final View mView; /** * A nested scrolling parent view currently receiving events for a nested scroll in progress. */ private ViewParent mNestedScrollingParent; /** * Whether nested sliding is allowed */ private boolean mIsNestedScrollingEnabled; /** * Record the sliding distance consumed by ParentView in the last scroll event */ private int[] mTempNestedScrollConsumed; public NestedScrollingChildHelper(View view) { mView = view; } /** * Set whether nested sliding is allowed * * @param enabled true to enable nested scrolling dispatch from this view, false otherwise */ public void setNestedScrollingEnabled(boolean enabled) { if (mIsNestedScrollingEnabled) { ViewCompat.stopNestedScroll(mView); } mIsNestedScrollingEnabled = enabled; } /** * Determine whether nested sliding is allowed * * @return true if nested scrolling is enabled for this view */ public boolean isNestedScrollingEnabled() { return mIsNestedScrollingEnabled; } /** * Determine whether there is a ParentView that receives nested sliding events at this point in ChildView * * @return true if this view has a nested scrolling parent, false otherwise */ public boolean hasNestedScrollingParent() { return mNestedScrollingParent != null; } /** * ChildView Called when a sliding request is received *(Find Nested Scrolling ParentView that meets the requirements * * @param axes ChildView Sliding direction * @return true if a cooperating parent view was found and nested scrolling started successfully */ public boolean startNestedScroll(int axes) { if (hasNestedScrollingParent()) { // At present, it is in the stage of nested sliding processing. return true; } if (isNestedScrollingEnabled()) { // PARANTView Accepting Nested Sliding ViewParent p = mView.getParent(); // Direct child control of ParentView: ChildView itself or contains ChildView View child = mView; while (p != null) { if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { mNestedScrollingParent = p; // When you find Nested Scrolling ParentView that meets the requirements, inform ParentView to prepare ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); return true; } if (p instanceof View) { child = (View) p; } p = p.getParent(); } } return false; } /** * Stop the current nested sliding */ public void stopNestedScroll() { if (mNestedScrollingParent != null) { // Notify ParentView to stop nested sliding ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView); mNestedScrollingParent = null; } } /** * ChildView After the scroll operation is completed, notify ParentView *(Refer to the annotations for the NestedScrollingChild#dispatchNestedScroll function. */ public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } // Notify ParentView about the actual sliding of ChildView (ParentView generally handles its own sliding based on dxUnconsumed| dyUnconsumed) ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); offsetInWindow[0] -= startX; offsetInWindow[1] -= startY; } return true; } else if (offsetInWindow != null) { // No motion, no dispatch. Keep offsetInWindow up to date. offsetInWindow[0] = 0; offsetInWindow[1] = 0; } } return false; } /** * ChildView Notify Parent of the distance it intends to slide before scroll is executed *(Refer to the Notes for NestedScrollingChild#dispatchNestedPreScroll Function) */ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { if (dx != 0 || dy != 0) { int startX = 0; int startY = 0; if (offsetInWindow != null) { mView.getLocationInWindow(offsetInWindow); startX = offsetInWindow[0]; startY = offsetInWindow[1]; } if (consumed == null) { if (mTempNestedScrollConsumed == null) { mTempNestedScrollConsumed = new int[2]; } consumed = mTempNestedScrollConsumed; } consumed[0] = 0; consumed[1] = 0; ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed); 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] = 0; offsetInWindow[1] = 0; } } return false; } /** * ChildView Notify ParentView after performing the fling sliding operation * (Refer to the NestedScrollingChild#dispatchNestedFling function */ public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX, velocityY, consumed); } return false; } /** * ChildView Before performing the fling sliding operation, notify ParentView to determine whether to consume the fling sliding event * (Refer to the NestedScrollingChild#dispatchNestedPreFling function) * * @return arentView Whether to consume fling sliding events */ public boolean dispatchNestedPreFling(float velocityX, float velocityY) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX, velocityY); } return false; } /** * Called in ChildView's onDetachedFromWindow callback function to end nested sliding */ public void onDetachedFromWindow() { ViewCompat.stopNestedScroll(mView); } /** * Stop nested sliding *(nested scroll ChildView When stopping this nested sliding, process the status of ChildView itself */ public void onStopNestedScroll(View child) { ViewCompat.stopNestedScroll(mView); } }
NestedScrolling ChildHelper is arguably the most important class in nested sliding mechanism. When ChildView implements the NestedScrolling Child interface, all NestedScrolling Child functions are implemented by NestedScrolling ChildHelper proxy, including: finding a ParentView that accepts nested sliding and notifying ParentView of nested sliding. Motion distance, direction, notify ParentView to end nested sliding, etc.
NestedScrollingParent
package android.support.v4.view; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; /** * This interface must be implemented in the arentView to achieve nested sliding */ public interface NestedScrollingParent { /** * ParentView Judging whether nested sliding is performed * * @param child ParentView Direct child control (including target) * @param target ChildView with Nested Sliding * @param nestedScrollAxes ChildView Nested sliding direction * @return true if this ViewParent accepts the nested scroll operation */ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); /** * Called when onStartNestedScroll returns true *(Nested Scrolling Parent Helper#onNested ScrollAccepted is typically called directly to record nested sliding directions. */ public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); /** * ParentView End nested sliding *(NestedScrolling ParentHelper#onStopNestedScroll is usually called directly, and nested sliding direction flag is reset to 0) */ public void onStopNestedScroll(View target); /** * Receiving sliding notifications from ChildView (after ChildView scroll) *(When ChildView calls dispatchNestedScroll function time after time, the general ParentView implements sliding operation in the function. */ public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed); /** * Receiving sliding notifications from ChildView (before ChildView scroll) *(When ChildView calls dispatchNestedPreScroll function time after time, ParentView determines the sliding distance consumed by itself. */ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); /** * Similar to onNestedScroll, only for fling operations */ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); /** * Similar to onNested PreScroll, only for fling operations */ public boolean onNestedPreFling(View target, float velocityX, float velocityY); /** * Returns the direction of nested sliding * (Direct call to NestedScrollingParentHelper#getNestedScrollAxes */ public int getNestedScrollAxes(); }
To achieve nested sliding, ParentView must implement NestedScrollingParent interface class, which can be used to judge whether to accept nested sliding, calculate the sliding distance consumed by ParentView, and slide the ParentView itself after receiving the sliding notification of ChildView. These functions are not called directly by ParentView itself, but indirectly by the method of implementing NestedScrollingChild interface by ChildView!
NestedScrollingParentHelper
public class NestedScrollingParentHelper { /** * PARANT VIEW TO IMPLEMENT NESTED SLIDING */ private final ViewGroup mViewGroup; /** * Recording the direction flag of nested sliding */ private int mNestedScrollAxes; public NestedScrollingParentHelper(ViewGroup viewGroup) { mViewGroup = viewGroup; } /** * Return the direction flag of nested sliding */ public int getNestedScrollAxes() { return mNestedScrollAxes; } /** * Called when ParentView receives nested sliding operations * (Called in the implementation of the NestedScrollingParent#onNestedScrollAccepted interface) */ public void onNestedScrollAccepted(View child, View target, int axes) { // Recording Nested Sliding Direction mNestedScrollAxes = axes; } /** * Called when nested sliding ends * (Called in the implementation of the NestedScrollingParent#onStopNestedScroll interface) */ public void onStopNestedScroll(View target) { // Reset nested sliding direction mNestedScrollAxes = 0; } }
The above implementation of NestedScrolling Parent Helper is relatively simple. Basically, it is called on the corresponding interface of NestedScrolling Parent. Only when ParentView accepts nested sliding and ends nested sliding, the value of mNestedScrollAxes of recording nested sliding direction flag is changed.
After familiarizing yourself with the above four classes, I suggest that you look at the implementation of NestedScrollingChild interface in RecycleView and the timing of calling NestedScrollingChild interface. If you look at the implementation of NestedScrollingParent interface in NestedScrollView, you can get a general grasp of the nested sliding mechanism.
In the next article, I will help you understand the nested sliding mechanism more deeply through a simple analysis of RecycleView and NestedScrollView source code, as well as the implementation of simple demo!
To sum up, the workflow of nested sliding is as follows:
(1) After ChildView receives TouchEvent (down type), it first looks for a ParentView that matches nested sliding:
NestedScrollingChild#startNestedScroll(int axes) -> NestedScrollingChildHelper#startNestedScroll(int axes) -> Recursive search for ParentView that meets nested sliding requirements (that is, NestedScrolling Parent# startNestedScroll (int axes) returns the parent control of true)
(2) When the ChildView receives a sliding request, it notifies ParentView (implements the NestedScrollingParent interface) of the direction in which it intends to slide, nestedScrollAxes and distance DX & dy:
// consumed as the output parameter to obtain the sliding distance of ParentView NestedScrollingChild#dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
(3) ParentView can calculate the sliding distance int[] consumed according to custom rules, and then notify ChildView.
//consumed is used as output parameter to record the consumption sliding distance of ParentView NestedScrollingParent#onNestedPreScroll(View target, int dx, int dy, int[] consumed)
(4) ChildView recalculates the distance dxConsumed & dyConsumed and the unused distance dxUnconsumed & dyUnconsumed according to the data int[] consumed returned by ParentView.
(5) After ChildView calculates the actual sliding distance according to step 4, it first calls scroll function to slide, and then notifies ParentView about its sliding condition.
NestedScrollingChild#dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
(6) When ParentView receives the actual sliding condition of ChildView, it can determine the sliding of ParentView itself according to dxUnconsumed & dyUnconsumed.
NestedScrollingParent#onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
Reference link:
Source code parsing of NestedScrolling event mechanism