CoordinatorLayout三部曲學習之一:Nest接口的實現

NestScrollChild和NestedScrollingParent

吐槽一下開源中國居然標題字數有限制java

因爲項目中使用了CoordinatorLayout來解決聯動以及實現炫酷的UI效果,那麼必須就要研究一波源碼了,畢竟知其然知其因此然。因此這篇文章是CoordinatorLayout 學習的三部曲的開篇。android

在學習CoordinatorLayout以前,首先先來學習一下CoordinatorLayout中實現聯動的關鍵類:NestScrollChild和NestedScrollingParent。spring

簡單查看一下兩個接口的定義,首先來看下NestedScrollingParent的:app

public interface NestedScrollingParent {
    //當前方法會在嵌套的子View調用ViewCompat.startNestedScroll(View, int)方法的時候會觸發,告訴父View我要滑動了,返回值爲真說明父View接收當前滑動操做
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
    //當上面的方法返回true後,此方法會回調到
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
	//當子View滑動中止後,該方法會回調到
    void onStopNestedScroll(@NonNull View target);
	//當onStartNestedScroll返回true後,子View在滑動時候會回調這個方法,能夠理解爲ProgressBar的onProgress方法
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);
	//該方法在onNestedScroll回調以前調用,給個父View一個消費滑動的機會
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
     //子View在進行fling時候的回調,與onNestedScroll相似,行爲不一樣而已
    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
	//該方法在onNestedFling回調以前調用
    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
    //當前的滑動方向
    //ViewCompat#SCROLL_AXIS_HORIZONTAL
	//ViewCompat#SCROLL_AXIS_VERTICAL
	//ViewCompat#SCROLL_AXIS_NONE
    @ScrollAxis
    int getNestedScrollAxes();
}

NestedScrollingParent須要由實現滾動聯動的父類實現,當實現NestedScrollingParent接口的時候,在此父類內部須要維護一個NestedScrollingParentHelper來進行觸摸處理以及分發。上述方法的做用在註釋中解釋。ide

上述註釋看起來可能仍是有點抽象,等會跟着源碼(NestedScrollView)過一遍就很清晰了。post

接下來再看下NestedScrollingChild的定義:學習

public interface NestedScrollingChild {
	//是否啓動聯動
    void setNestedScrollingEnabled(boolean enabled);
	//返回是否聯動
    boolean isNestedScrollingEnabled();
	//開始滑動
    boolean startNestedScroll(@ScrollAxis int axes);
	//中止滑動滑動
    void stopNestedScroll();
	//是否擁有滑動嵌套的父View
    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);
	//分發Fling
    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
	//分發Fling前的回調
    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

咱們以NestedScrollView爲聯動的父View,NestedScrollView爲子View來進行代碼邏輯的分析。this

分析完後才以爲不應選擇上述狀況分析,容易混淆,畢竟在一個NestedScrollView中回調來,回調去的spa

xml以下:code

<com.lin.aloha.photo.FatherScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_height="match_parent"
    tools:context=".photo.TestActivity">
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="vertical"
        android:layout_height="match_parent">

        <View
            android:layout_width="match_parent"
            android:background="#dd2288"
            android:layout_margin="20dp"
            android:layout_height="200dp">

        </View>
        <com.lin.aloha.photo.ChildScrollView
            android:layout_width="match_parent"
            android:background="#1d9d29"
            android:layout_height="400dp">
            <LinearLayout
                android:layout_width="match_parent"
                android:orientation="vertical"
                android:layout_height="wrap_content">
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>

                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
                <View
                    android:layout_width="match_parent"
                    android:background="#2277dd"
                    android:layout_margin="20dp"
                    android:layout_height="200dp">

                </View>
            </LinearLayout>
        </com.lin.aloha.photo.ChildScrollView>
        <View
            android:layout_width="match_parent"
            android:background="#dd2288"
            android:layout_margin="20dp"
            android:layout_height="200dp">

        </View>
    </LinearLayout>
</com.lin.aloha.photo.FatherScrollView>

咱們使用這個就能直接實現聯動的效果,效果圖以下:

NestedScrollView實現了NestedScrollingParent以及NestedScrollingChild2接口,NestedScrollingChild2就是NestedScrollingChild一些方法的拓展,核心是不變的。因爲實現了這兩個接口,能夠說明聯動的時候其能當作父View也能當作子View來處理。這裏咱們首先把它當作父View來學習,既然是父View的話,天然要從dispatchTouchEvent()開始,可是一查發現並無複寫該方法,因此咱們從onInterceptTouchEvent()看起:

public boolean onInterceptTouchEvent(MotionEvent ev) {
 	//mIsBeingDragged標識當前View是否在移動,包括咱們觸摸移動以及正在進行fling操做的時候。
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }

        switch (action & MotionEvent.ACTION_MASK) {
				case MotionEvent.ACTION_DOWN: {
                final int y = (int) ev.getY();
				...
					//記錄觸摸的y點
                mLastMotionY = y;
                mActivePointerId = ev.getPointerId(0);
				//初始化或者重置VelocityTracker
                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                mScroller.computeScrollOffset();
                mIsBeingDragged = !mScroller.isFinished();//若是還在滑動,則爲true
				//調用mChildHelper.startNestedScroll()
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                final int activePointerId = mActivePointerId;
                ...
                final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (yDiff > mTouchSlop
                        && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
                    mIsBeingDragged = true;//攔截事件
                    mLastMotionY = y;
                    initVelocityTrackerIfNotExists();
                    mVelocityTracker.addMovement(ev);
                    mNestedYOffset = 0;
                    final ViewParent parent = getParent();
                    if (parent != null) {
					//主動告訴parent不要調用parent的interceptTouchEvent,意思是我要來消費觸摸事件
                     parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            }

           

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                /* Release the drag */
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                recycleVelocityTracker();
                if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
				//中止滑動
                stopNestedScroll(ViewCompat.TYPE_TOUCH);
                break;
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }

        /*
        * The only time we want to intercept motion events is if we are in the
        * drag mode.
        */
        return mIsBeingDragged;
    }

onInterceptTouchEvent()事件處理比較簡單:

  • 首先在ACTION_DOWN中會去調用startNestedScroll()方法,然而該NestedScrollView因爲沒父View,因此該方法沒意義。
  • 在ACTION_MOVE將mIsBeingDragged設置爲true,說明要把事件攔截並傳遞給自身的onTouch()來處理事件。
  • 在ACTION_UP或者ACTION_CANCEL中調用stopNestedScroll(),這個方法也沒意義(緣由同第一條),而後把mIsBeingDragged從新設爲false。

咱們再看下onTouchEvent()事件,

public boolean onTouchEvent(MotionEvent ev) {
       ...
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
               ...
              	//獲取座標
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                //2.調用的NestedScrollingChild的startNestedScroll()方法
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
            }
            case MotionEvent.ACTION_MOVE:
                ...
                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
				//父類View不執行dispatchNestedPreScroll()方法,下面分析子View的NestedScrollView會用到
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                        ViewCompat.TYPE_TOUCH)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;//標識爲true
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = getScrollY();
                    final int range = getScrollRange();
                    final int overscrollMode = getOverScrollMode();//默認爲OVER_SCROLL_ALWAYS
                    boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
                            || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
					//最終調用scrollTo方法進行滑動
                    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                            0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    final int scrolledDeltaY = getScrollY() - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;
					//一樣這個方法對於父View不起效
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    } else if (canOverscroll) {//執行當前方法
                        ensureGlows();
                        final int pulledToY = oldY + deltaY;
						//執行邊緣效果,即到頂部或者底部出現的圓弧遮罩
                        if (pulledToY < 0) {
                            EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(),
                                    ev.getX(activePointerIndex) / getWidth());
                            if (!mEdgeGlowBottom.isFinished()) {
                                mEdgeGlowBottom.onRelease();
                            }
                        } else if (pulledToY > range) {
                            EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(),
                                    1.f - ev.getX(activePointerIndex)
                                            / getWidth());
                            if (!mEdgeGlowTop.isFinished()) {
                                mEdgeGlowTop.onRelease();
                            }
                        }
                        if (mEdgeGlowTop != null
                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                            ViewCompat.postInvalidateOnAnimation(this);
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
			//計算鬆開手後須要滑動的距離
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                    flingWithNestedDispatch(-initialVelocity);
                } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                        getScrollRange())) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
                mActivePointerId = INVALID_POINTER;
                endDrag();
                break;
          
          ...
        return true;//返回值爲true
    }

從上面的步驟來看,父View徹底沒有涉及到聯動的操做,那麼點擊第一個紫紅色的View,這個View在父NestedScrollView中,而且沒任何觸摸事件的處理,那麼最終會調到父View的onTouchEvent()中,能夠看下面的log,也正是這種狀況。

拋出個問題:在log中看到調用了兩次的startNestedScroll()方法,這個會不會有什麼影響呢,答案是不會的,具體的在子NestedScrollView分析

那麼接下來分析一會兒NestedScrollView的行爲,在父NestedScrollView中分析過,onInterceptEvent(...)方法會調用到NestedScrollingChildHelper.startNestedScroll(...)方法,對於父NestedScrollView來講,是沒多大意義的,但對於子NestedScrollView來講那就有意義了,NestedScrollingChildHelper.startNestedScroll(...)實現以下所示:

public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
        if (hasNestedScrollingParent(type)) {
            // Already in progress
            return true;
        }
    	//是否支持嵌套滑動
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            //從子View向外查詢第一個接收滑動的父View,在當前例子中就是父NestedScrollView
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                    setNestedScrollingParentForType(type, p);
                    //調用onNestedScrollAccepted()通知父View
                    ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                    return true;
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }

首先看下hasNestedScrollingParent()方法:

public boolean hasNestedScrollingParent(@NestedScrollType int type) {
        return getNestedScrollingParentForType(type) != null;
    }
 private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
        switch (type) {
            case TYPE_TOUCH:
                return mNestedScrollingParentTouch;
            case TYPE_NON_TOUCH:
                return mNestedScrollingParentNonTouch;
        }
        return null;
    }

第一次調用時候,因爲mNestedScrollingParentTouch爲null,因此會接着往下走,而後在setNestedScrollingParentForType()綁定對應的mNestedScrollingParentTouch,第二次調用時候,mNestedScrollingParentTouch不爲null,天然就不會往下走,(並且在咱們中止滑動的時候,mNestedScrollingParentTouch會設置爲Null的)也就不會調用到ViewParentCompat.onStartNestedScroll()方法了。所以上面提的問題就解決了,雖然調用了兩次startNestedScroll()方法,對於代碼邏輯是沒有大影響的。

回到startNestedScroll()方法中,接下來的主要有三部分組成:

  • 調用isNestedScrollingEnabled()判斷是否支持嵌套滑動,支持則往下走,不然返回false
  • 調用ViewParentCompat.onStartNestedScroll()方法通知父View開始滑動了
  • 調用 ViewParentCompat.onNestedScrollAccepted()方法

上述的onStartNestedScroll(...)以及onNestedScrollAccepted()最終都會回調到實現NestedScrollingParent接口的相同方法名的回到當中,在這裏就是回調到父NestedScrollView的對應方法中:

//NestedScrollView
    @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);
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);//三級聯動時會有用,這裏Demo中不考慮
    }
		//NestedScrollingParentHelper
	    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
            @ScrollAxis int axes) {
        onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
    }

上述能夠看到,咱們在觸摸子NestedScrollView的時候,經過NestScrollChild2.startNestScroll(),會調用到父NestedScrollView實現NestedScrollingParent的onNestedScrollAccepted()和onStartNestedScroll()。因爲子NestedScrollView的子View們不涉及觸摸事件的處理,因此正當會回調到子View的onTouchEvent()中,從新看下ACTION_DOWN方法:

case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    final ViewParent parent = getParent();
                    if (parent != null) {//這裏能夠看到直接通知父NestedScrollView不攔截觸摸事件
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }

                /*
                 * If being flinged and user touches, stop the fling. isFinished
                 * will be false if being flinged.
                 */
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }

                // Remember where the motion event started
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                break;
            }

在子NestedScrollView中的onTouchEvent()中調用了requestDisallowInterceptTouchEvent(true)通知父NestedScrollView不攔截觸摸事件,且子NestedScrollView的onTouchEvent()永遠返回true,則父NestedScrollView在當前情境下的onTouchEvent()是永遠不會被調用到的。

接着繼續往下看,因爲onInterceptTouchEvent()中沒有涉及到對應聯動的操做,這裏就不看了,直接看onTouchEvent()的ACTION_MOVE操做吧,從新貼下代碼:

case MotionEvent.ACTION_MOVE:
                ...
                final int y = (int) ev.getY(activePointerIndex);
                int deltaY = mLastMotionY - y;
				//1
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset,
                        ViewCompat.TYPE_TOUCH)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
               ...
			   //mIsBeingDragged爲true
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = getScrollY();
                    final int range = getScrollRange();
                    final int overscrollMode = getOverScrollMode();
                    boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS
                            || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
							//滑動該NestedScrollView
					if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                            0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }

                    final int scrolledDeltaY = getScrollY() - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;
					//2
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    } else if (canOverscroll) {
                       ...//處理邊緣事件
                    }
                }
                break;

首先在1中, 調用mChildHelper.dispatchNestedPreScroll()方法:

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            ...
            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(parent, mView, dx, dy, consumed, type);
				...
				return consumed[0] != 0 || consumed[1] != 0;
        }
        return false;
    }
	
	 @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        dispatchNestedPreScroll(dx, dy, consumed, null);//三層聯動會調用到,可忽略
    }

上述代碼主要調用到了ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type)方法通知父ScrollView經過consumed參數決定是否消費滑動事件,最終仍是回調會NestedScrollView.onNestedPreScroll()回調中,因爲父NestedScrollView沒有parent,因此dispatchNestedPreScroll()邏輯不會調用,至關於父NestedScrollView什麼都不作。

而後在2中,調用dispatchNestedScroll()方法,方法中的uncomsued表明父NestedScrollView消費後剩下的須要子NestedScrollView滑動的距離。

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type) {
        if (isNestedScrollingEnabled()) {
            final ViewParent parent = getNestedScrollingParentForType(type);
            if (parent == null) {
                return false;
            }

            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                ...
                ViewParentCompat.onNestedScroll(parent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed, type);

               ...
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }

最終回調會父NestedScrollView的onNestedScroll()中,調用scrollBy()方法進行滑動:

@Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed) {
        final int oldScrollY = getScrollY();
        scrollBy(0, dyUnconsumed);
      ...
    }

接下來看下最後的ACTION_UP方法:

case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                    flingWithNestedDispatch(-initialVelocity);
                } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                        getScrollRange())) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
                mActivePointerId = INVALID_POINTER;
                endDrag();
                break;
    private void endDrag() {
        mIsBeingDragged = false;

        recycleVelocityTracker();
        stopNestedScroll(ViewCompat.TYPE_TOUCH);//調用stopNestedScroll中止滑動

        if (mEdgeGlowTop != null) {
            mEdgeGlowTop.onRelease();
            mEdgeGlowBottom.onRelease();
        }
    }

這個ACTION_UP中相關的就只有stopNestedScroll()方法,經過調用該方法通知父NestedScrollView的onStopNestScroll()方法中:

@Override
    public void onStopNestedScroll(View target) {
        mParentHelper.onStopNestedScroll(target);
       ...
    }
	
	//重置方向參數
	public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
        mNestedScrollAxes = 0;
    }

當咱們手指離開屏幕時候,就是fling滑動開始的時候了,也就是說在ACTION_UP方法中進行,再看下ACTION_UP方法:

case MotionEvent.ACTION_UP:
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
				//速度大於最小速度,則執行Nest下發fling操做
                if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                    flingWithNestedDispatch(-initialVelocity);
                } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                        getScrollRange())) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
                mActivePointerId = INVALID_POINTER;
                endDrag();
                break;
				
    private void flingWithNestedDispatch(int velocityY) {
        final int scrollY = getScrollY();
        final boolean canFling = (scrollY > 0 || velocityY > 0)
                && (scrollY < getScrollRange() || velocityY < 0);
        if (!dispatchNestedPreFling(0, velocityY)) {//若是父NestScrollView不消費fling,則由子NestedScrollView進行消費,通知回調onNestedFling()方法給父NestScrollView
           dispatchNestedFling(0, velocityY, canFling);
            fling(velocityY);
        }
    }

fling操做比較簡單,這裏就不敘述了。


總體的流程算是分析完了一遍,下面整理一張對應的流程圖表示一下:

源碼走一遍後發現,子View賦予了父View處理滑動的機會,這個觸摸傳遞的模式徹底不一樣,觸摸事件傳遞是從上到下的,而上述的兩個接口則提供了一種從下到上的方式,使得處理觸摸事件更加的靈活。舉個例子體驗一下:

當子NestedScollView還沒滑動到頂部或者底部時候,因此觸摸事件由子NestedScrollView處理,當到頂部或者底部的時候,則藉由NestedScrollChild.dispatchNestedScroll()傳遞給NestedScrollParent.onNestedScroll()方法處理,也就解釋爲何子NestedScrollView滑動到頂部時,手指再往下滑,就會滑動父NestedScrollView。

能夠直接看onTouchEvent()的ACTION_MOVE方法驗證正確與否:

...
 final int oldY = getScrollY();
...
 final int scrolledDeltaY = getScrollY() - oldY;
 final int unconsumedY = deltaY - scrolledDeltaY;
 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                            ViewCompat.TYPE_TOUCH)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
 } else if (canOverscroll) {
					
					...

當咱們滑動子NestedScrollView時候因爲子NestedScrollView沒有到頭,scrolledDeltaY計算獲取子NestedScrollView滑動距離,這時候是與deltaY的值,相等的,因此unconsumedY=0,也就是說父NestedScrollView沒有消費的距離,當子NestedScrollView到頭時候,scrolledDeltaY就等於0了,滑動的距離就由unconsumedY記錄下來傳遞給父NestedScrollView,父NestedScrollView最終調用scrollTo()方法完成了滑動。

有時間的話再寫一個實現Nest的例子

相關文章
相關標籤/搜索