天天看點

Android NestedScrolling 嵌套滾動原了解析一.原有問題二.解決方案三.源碼分析四.NestedScrollView+RecyclerView解析

Android NestedScrolling 嵌套滾動原了解析

  • 一.原有問題
  • 二.解決方案
    • 1.實作原理
    • 2.方案設計
      • (1)android的support-v4包提供了兩個接口來實作NestedScroll架構
      • (2)android的support-v4包也提供了兩個相應的Helper類來實作通用功能
      • (3)相容性
      • (4)流程
  • 三.源碼分析
    • NestedScrollView/ScrollView
  • 四.NestedScrollView+RecyclerView解析

一.原有問題

衆所周知,android的觸摸事件傳遞有局限性,當比較複雜的可滾動控件嵌套在一起的時候,總會有各種各樣的滑動問題,這與android的觸摸事件傳遞機制(View觸摸事件機制)密不可分:

android的觸摸事件傳遞是從上至下的遞歸傳遞,如果某次DOWN事件,有子view消費了,則之後的所有事件都隻可能交由該子view處理,其父view沒有機會再去處理(隻能攔截);

并且很多ViewGroup,比如ViewPager、ScrollView,直接在onInterceptTouchEvent方法中,将move事件攔截,為的是交由自己處理,而沒有兼顧到其子view可能的滑動;

是以原有的觸摸傳遞問題的局限性就是:一旦子view處理事件後,父view就不能處理了,并且是以導緻許多ViewGroup為了自己處理,直接攔截事件并沒有交由子view處理事件,導緻滑動效果的局限性很大

二.解決方案

1.實作原理

android在5.0後,對這塊的問題做了很大的改善,其根本解決辦法就是:既然原有問題是子view處理事件後父view就處理不了,那麼就想辦法在子view處理前,給父view一個處理的機會就好了。

這樣有三個好處:

  1. 不影響核心的觸摸事件傳遞機制,還是從上至下遞歸執行;一個view處理後不會交由其他view處理(onTouchEvent)
  2. 父view不用為了自己處理而武斷的攔截事件,因為自己也會有一個處理事件的機會
  3. 可以讓一次觸摸事件在多層view中都應用上去,即可以實作滾動view内部嵌套滾動view的效果

2.方案設計

(1)android的support-v4包提供了兩個接口來實作NestedScroll架構

NestedScrollingChild:提供了作為内部嵌套類應該實作的方法

public interface NestedScrollingChild {
    //是否可以嵌套滾動
    public void setNestedScrollingEnabled(boolean enabled);
	
	//自身是否支援嵌套滾動
	public boolean isNestedScrollingEnabled();

    //開始内部嵌套滾動,傳回值為是否可以内部嵌套滾動,參數為滾動方向
    public boolean startNestedScroll(int axes);

    //停止内部嵌套滾動
    public void stopNestedScroll();

    //是否已經有支援其内部嵌套滾動的父view
    public boolean hasNestedScrollingParent();

    //在内部嵌套滾動時,派發給支援其嵌套滾動的parent,使其有機會做一些滾動的處理
	//參數為x/y軸已消費的和未消費的距離
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

    //在内部嵌套滾動前,派發給支援其嵌套滾動的parent,使其有機會做一些滾動的預處理
	//dx,dy為可以消耗的距離,consumed為已經消耗的距離
	//傳回值為支援其嵌套滾動的parent是否消費了部分距離
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);

    //在内部嵌套自由滑動時,派發給支援其嵌套滾動的parent,使其有機會做一些自由滑動的處理
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    //在内部嵌套自由滑動前,派發給支援其嵌套滾動的parent,使其有機會做一些自由滑動的預處理
	//傳回值為支援其嵌套滾動的parent是否消費了fling事件
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
           

NestedScrollingParent:提供了作為支援内部嵌套滾動的view的方法

public interface NestedScrollingParent {
    //是否接受此次的内部嵌套滾動
	//target是想要内部滾動的view,child是包含target的parent的直接子view
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    //接受内部滾動後,做一些預處理工作
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

    //停止了内部滾動
    public void onStopNestedScroll(View target);

    //内部嵌套滾動開始,根據已消費和未消費的距離參數進行應用
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    //内部嵌套滾動開始前做一些預處理,主要是根據dx,dy,将自己要消費的距離計算出來,告知target(通過consumed一層層記錄實作)
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    //内部嵌套滑動開始,consumed參數為target是否消費了fling事件,parent可以根據此來做出自己的選擇
	//傳回值為parent自己是否消費了fling事件
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    //内部嵌套滑動開始前做一些預處理
	//傳回值為parent自己是否消費此次fling事件
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);
} 
           

(2)android的support-v4包也提供了兩個相應的Helper類來實作通用功能

NestedScrollingChildHelper:内部嵌套滾動view實作NestedScrollingChild的一些通用實作

public class NestedScrollingChildHelper {
    private final View mView;
    private ViewParent mNestedScrollingParent;
    private boolean mIsNestedScrollingEnabled;

    //view作為内部嵌套滾動的view
    public NestedScrollingChildHelper(View view) {
        mView = view;
    }

    //開始嵌套滾動的方法
    public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            //之前已經找到了支援嵌套滾動的parent,說明正在進行嵌套滾動,則傳回true即可
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
				//向上找到支援内部嵌套滾動的parent,并記錄下來
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                    mNestedScrollingParent = p;
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

    //派發滾動事件
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
			//已經消費部分距離或者還有未消費距離時,需要派發給parent進行處理
            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];
                }
				//派發,即調用到NestedScrollingParent的onNestedScroll中進行處理
                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;
    }

    //派發預滾動事件
    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;
				//派發,即調用到NestedScrollingParent的onNestedPreScroll中進行處理
                ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);

                if (offsetInWindow != null) {
                    mView.getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
				//parent有消費距離則傳回true
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
	...
} 
           

NestedScrollingParentHelper:支援内部嵌套滾動的parent實作NestedScrollingParent的一些通用實作

public class NestedScrollingParentHelper {
    private final ViewGroup mViewGroup;
    private int mNestedScrollAxes;

    //支援内部嵌套滾動的view
    public NestedScrollingParentHelper(ViewGroup viewGroup) {
        mViewGroup = viewGroup;
    }

    //接受内部滾動時,記錄下内部滾動方向
    public void onNestedScrollAccepted(View child, View target, int axes) {
        mNestedScrollAxes = axes;
    }
	...
}
           

(3)相容性

我們可以注意到,在Helper類裡面的方法調用,使用了ViewParentCompat類,顧名思義是用來做相容的,即不同版本系統支援的功能不一樣,需要用不同的方式去實作一樣的效果,在Compat類裡做處理,那麼這塊功能為什麼需要相容呢?

  1. 因為NestedScrolling這一套接口雖然是support包裡的,但是原有的控件并沒有實作這些接口(support包裡的控件以及自定義的可以實作這些接口)
  2. android5.0開始,framework在ViewParent裡面實作了這些接口的同名方法,使得可以直接調用ViewParent的這些方法;但是5.0之前沒有這些方法

綜上所述,Compat包裡需要做的就是,5.0之後的系統,可以直接調用ViewParent的相關方法;5.0之前隻得調用實作了這些接口的類的相關方法

//ViewParentCompat.onStartNestedScroll(p, child, mView, axes)
 
//Compat調用方法
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}
 
//IMPL通過系統版本生成不同的相容類
static final ViewParentCompatImpl IMPL;
static {
    final int version = Build.VERSION.SDK_INT;
    if (version >= 21) {
        IMPL = new ViewParentCompatLollipopImpl();
    } else if (version >= 19) {
        IMPL = new ViewParentCompatKitKatImpl();
    } else if (version >= 14) {
        IMPL = new ViewParentCompatICSImpl();
    } else {
        IMPL = new ViewParentCompatStubImpl();
    }
}
 
//ViewParentCompatStubImpl(5.0之前的處理方式)
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
	//需要ViewParent實作NestedScrollingParent接口才能調用
    if (parent instanceof NestedScrollingParent) {
        return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
                nestedScrollAxes);
    }
    return false;
}
 
//ViewParentCompatLollipopImpl(5.0之後的處理方式)
public boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    return ViewParentCompatLollipop.onStartNestedScroll(parent, child, target,
            nestedScrollAxes);
}
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    try {
		//5.0後ViewParent實作了該方法,可以直接調用
        return parent.onStartNestedScroll(child, target, nestedScrollAxes);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onStartNestedScroll", e);
        return false;
    }
} 
           

(4)流程

上面就是整個支援内部嵌套滾動的架構,通過這些方法和Helper的排程邏輯,可以大緻看出應有的實作流程

Android NestedScrolling 嵌套滾動原了解析一.原有問題二.解決方案三.源碼分析四.NestedScrollView+RecyclerView解析

三.源碼分析

通過上面的叙述,應該已經對内部嵌套滾動的實作有了大概的輪廓,也對實作的流程有了大緻的邏輯,現在就來通過具體的例子來看看,已經實作了内部嵌套滾動的View具體是如何來把這些流程實作的

NestedScrollView/ScrollView

這裡我們先通過NestedScrollView的實作,并與原有的5.0之前的ScrollView作對比,看看是有何差别的吧!

(1)首先來看看5.0之前的ScrollView

public boolean onInterceptTouchEvent(MotionEvent ev) {
	...
    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {//移動時
            final int pointerIndex = ev.findPointerIndex(activePointerId);
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop) {
                mIsBeingDragged = true;//直接進行ScrollView的拖拽,即攔截了事件傳遞
                ...
            }
            break;
        }
		...
    }
    return mIsBeingDragged;
}
           

可見,5.0之前,ScrollView(包括大多數可滑動的控件)都在onInterceptTouchEvent裡将事件直接攔下,沒有交由child處理。

(2)再來看看5.0之後的ScrollView,因為support包提供了和ScrollView類似的NestedScrollView,且不用考慮版本相容問題,是以我們直接看NestedScrollView就好

首先NestedScrollView實作了Child和Parent兩個接口,說明其既可以作為内部嵌套滾動的View,也可以作為支援内部嵌套滾動的容器

//1.攔截與否
 public boolean onInterceptTouchEvent(MotionEvent ev) {
    ...
    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {//移動時
            ...
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop
                    && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {//如果目前沒有内部嵌套滾動,才會進行攔截,這樣就會有交由child處理的機會
                mIsBeingDragged = true;
                ...
            }
            break;
        }
        ...
    }
    return mIsBeingDragged;
}
           

上面這段代碼說明了NestedScrollView不會武斷的攔截事件,而是對于可嵌套滑動的child,将事件優先傳給他做處理,自己作為parent也有機會處理,這樣互不影響,是正确的思路

//1.自己處理
public boolean onTouchEvent(MotionEvent ev) {
    ...
    switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {//按下時開始嘗試内部嵌套滾動(找到接受内部嵌套滾動的parent并記錄狀态)
            ...
            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
            break;
        }
        case MotionEvent.ACTION_MOVE:
            ...
            final int y = (int) ev.getY(activePointerIndex);
            int deltaY = mLastMotionY - y;
            if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {//計算出總共的deltaY,開始派發内部滾動預處理事件給parent,統計parent總共消費的距離
				//如果parent有所消費,則計算自己目前還能消費的距離
                deltaY -= mScrollConsumed[1];
                vtev.offsetLocation(0, mScrollOffset[1]);
                mNestedYOffset += mScrollOffset[1];
            }
            ...
            if (mIsBeingDragged) {
                ...//應用變化
				//計算消費掉的距離和未消費的距離
                final int scrolledDeltaY = getScrollY() - oldY;
                final int unconsumedY = deltaY - scrolledDeltaY;
				//派發給parent内部滾動事件,使parent可以有機會處理剩餘的未消費距離
                if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                    mLastMotionY -= mScrollOffset[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }...
            }
            break;
        case MotionEvent.ACTION_UP:
			...
            flingWithNestedDispatch(-initialVelocity);//是否觸發fling事件以及如何派發fling的統一方法
            endDrag();//endDrag()内部嘗試停止内部嵌套滾動stopNestedScroll()
            break;
        case MotionEvent.ACTION_CANCEL:
            ...
            endDrag();//endDrag()内部嘗試停止内部嵌套滾動stopNestedScroll()
            break;
        ...
    }
    return true;
}

//2.處理fling事件的統一方法
private void flingWithNestedDispatch(int velocityY) {
    final int scrollY = getScrollY();
    final boolean canFling = (scrollY > 0 || velocityY > 0)
            && (scrollY < getScrollRange() || velocityY < 0);//自身是否可以滑動
    if (!dispatchNestedPreFling(0, velocityY)) {//向parent派發預滑動事件,給parent一個處理機會,看parent是否會處理fling事件
		//parent沒有處理的話,再去派發fling事件,自己也許處理也許不處理,也通知給parent,讓parent有機會根據child是否消費而決定如何處理
        dispatchNestedFling(0, velocityY, canFling);
        if (canFling) {//執行自己的fling
            fling(velocityY);
        }
    }
}
           

上面的onTouchEvent的處理,展示了NestedScrollView裡面是如何在不同的時機調用内部嵌套滾動的方法(相應接口的方法),開始整個流程,使事件從源頭處,讓處理者和parent都有機會去處理一次觸摸事件

上面所說都是作為Child角度來說的,那麼再來看看NestedScrollView作為Parent,相應的嵌套滾動方法是做了什麼

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//接受垂直方向的内部嵌套滾動
}

@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);//調用helper做基礎處理
    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);//作為Child也會去通知parent進行内部滾動
}

@Override
public void onStopNestedScroll(View target) {
    mParentHelper.onStopNestedScroll(target);//調用helper做基礎處理
    stopNestedScroll();//作為Child也會去通知parent進行停止内部滾動
}

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed) {
    final int oldScrollY = getScrollY();
    scrollBy(0, dyUnconsumed);//自己應用Child未消費的距離
    final int myConsumed = getScrollY() - oldScrollY;
    final int myUnconsumed = dyUnconsumed - myConsumed;
    dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);//作為Child将剩餘距離派發給Parent
}

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    dispatchNestedPreScroll(dx, dy, consumed, null);//自己并不主動消耗,隻是作為Child派發給Parent
}

@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
    if (!consumed) {//如果target沒有消費掉
        flingWithNestedDispatch((int) velocityY);//則交由自己處理
        return true;//自己處理
    }
    return false;//自己未處理
}

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    return dispatchNestedPreFling(velocityX, velocityY);//自己不會主動消費fling,直接派發給Parent
}
           

由代碼可知,NestedScrollView作為Parent,不會主動消費距離,但是會消費掉Child未消費的距離

(3)綜上所述,NestedScrollView既支援内部嵌套滾動,也支援可以内部嵌套滾動的View作為child,那麼我們是不是可以實作這種效果:兩個(甚至多個)NestedScrollView套在一起,滑動内部的NestedScrollView,内部的NestedScrollView會正常滾動,滾動結束後,外部的NestedScrollView會繼續滾動?

答案是可以的,這就是内部嵌套滾動要解決的問題,包括其他實作了這些功能的控件,比如NestedScrollView+RecyclerView,SwipeRefreshLayout+RecyclerView,CoordinatorLayout等等都可以的

下面我們通過看一個比較簡單使用的組合來看下具體實作流程,NestedScrollView+RecyclerView

四.NestedScrollView+RecyclerView解析

(1)xml:一個NestedScrollView裡面有一個RecyclerView,上下還有兩個Button用作可滾動範圍

<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

       <android.support.v7.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    </LinearLayout>

</android.support.v4.widget.NestedScrollView>
           

(2)DOWN事件觸摸RecyclerView

//1.NestedScrollView不會攔截DOWN事件,交由child(RecyclerView)處理
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ...
    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
    		...
    		mIsBeingDragged = !mScroller.isFinished();
    		break;
		}
        ...
    }
    return mIsBeingDragged;
}
 
//2.RecyclerView嘗試開啟内部嵌套滾動
public boolean onInterceptTouchEvent(MotionEvent e) {
    ...
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            ...
            if (mScrollState == SCROLL_STATE_SETTLING) {
                getParent().requestDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);//交由自己onTouchEvent處理事件
            }
			...
            startNestedScroll(nestedScrollAxis);//嘗試開啟内部嵌套滾動
            break;

        ...
    }
    return mScrollState == SCROLL_STATE_DRAGGING;
}
 
//3.找到(ChildHelper)NestedScrollingParent(NestedScrollView)通知其開啟内部嵌套滾動
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);//記錄内部滾動資訊(ParentHelper)
    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}
           

至此,RecyclerView已經處理了DOWN事件,NestedScrollView也記錄了内部嵌套滾動的開始

(3)MOVE事件滑動RecyclerView

//1.NestedScrollView不會攔截,直接交由RecyclerView繼續處理
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ...
    switch (action & MotionEventCompat.ACTION_MASK) {
        case MotionEvent.ACTION_MOVE: {
            final int y = (int) ev.getY(pointerIndex);
            final int yDiff = Math.abs(y - mLastMotionY);
            if (yDiff > mTouchSlop
                    && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {//記錄過内部嵌套滾動的開始後,getNestedScrollAxes為VERTICAL了,就不會攔截了
                mIsBeingDragged = true;
                ...
            }
            break;
        }
        ...
    }
    return mIsBeingDragged;
}
 
//2.RecyclerView派發和處理滾動事件
public boolean onTouchEvent(MotionEvent e) {
    ...
    switch (action) {
        ...
        case MotionEvent.ACTION_MOVE: {
            ...
			//計算消費距離
            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;
			//派發預滾動事件給NestedParent
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
				//如若parent消費部分距離,則此view少消費相應距離
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // Updated the nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }
            ...
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];
				//執行滾動事件,并派發内部嵌套滾動事件給parent
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        } break;
		...
    }
	...
    return true;
}
boolean scrollByInternal(int x, int y, MotionEvent ev) {
    ...
	//派發内部滾動事件,告知parent消費的距離和未消費的距離
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
        // Update the last touch co-ords, taking any scroll offset into account
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        if (ev != null) {
            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        }
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    }...
    return consumedX != 0 || consumedY != 0;
}
 
//3.NestedScrollView處理未消費距離
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
        int dyUnconsumed) {
    final int oldScrollY = getScrollY();
	//消費未消費的距離
    scrollBy(0, dyUnconsumed);
    final int myConsumed = getScrollY() - oldScrollY;
    final int myUnconsumed = dyUnconsumed - myConsumed;
	//将剩餘距離繼續派發通知其parent
    dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
	//預滾動事件不做處理,繼續往上派發(NestedScrollView本身也是NestedChild)
    dispatchNestedPreScroll(dx, dy, consumed, null);
}
           

(4)UP事件處理Fling或者stop事件類似,就不做贅述

以上就是NestedScrollView+RecyclerView的嵌套滾動使用,效果就是可以在NestedScrollView裡正常滑動RecyclerView,并且在RecyclerView滑動到邊界後繼續正常滑動NestedScrollView

除此之外,還有很多support包提供的新控件實作了内部嵌套滾動的邏輯,其中最為複雜且實用的是CoordinatorLayout,其通過對各個Child分發NestedScroll相關事件,将各個child的滾動可以依賴起來,用以實作複雜的組合控件滾動效果。