從NestedScrollingChild、NestedScrollingParent源碼分析嵌套滑動機制

前言

1、類概述:
NestedScrollingChild接口
定義了“與父view交互”的方法,需要嵌套滑動的子view就實現它;

NestedScrollingChildHelper幫助類
協助子view(如RecyclerView)實現“與父view交互”的方法。在實現這些方法時,發現有很多邏輯是可以共用的,所以抽了一個Helper類,專門來實現這些邏輯,讓其它需要嵌套滑動的子view可以複用,也讓RecyclerView的代碼更簡介、易懂;

NestedScrollingParent接口
定義了“與子view交互”的方法,需要嵌套滑動的父view就實現它;

NestedScrollingParentHelper幫助類
功能跟上面的xxxChildHelper一樣,但是由於不同父view與子view交互差別很大,所以這個幫助類裏並沒有多少功能,具體實現還是在父view裏自己實現;

2、嵌套滑動流程概述:
子view接收touch事件,開始滑動前,通過ViewParentCompat這個類通知父view,父view做相應處理;
子view滑動後,通知父view自己的滑動情況,父view做相應處理;


滑動機制源碼分析

NestedScrollingChild和NestedScrollingParent中的方法都是一一對應的,子view通過某個方法通知父view,父view通過某個方法接收到通知,對應關係如下:

啓動嵌套滑動:startNestedScroll() —— onStartNestedScroll();
滑動前:dispatchNestedPreScroll() —— onNestedPreScroll();
滑動:dispatchNestedScroll() —— onNestedScroll();
fling滑動前:dispatchNestedPreFling() —— onNestedPreFling();
fling滑動:dispatchNestedFling() —— onNestedFling();
停止滑動:stopNestedScroll() —— onStopNestedScroll();

下面就用RecyclerView、NestedScrollView的源碼來分析(由於NestedScrollingParentHelper非常簡單,也沒做什麼實際實現,下面的源碼分析中就不貼出了)。

假設子view是RecyclerView,父view是NestedScrollView,父父view是也是NestedScrollView(後面簡稱爺view),總共三層。

啓動嵌套滑動

RecyclerView源碼:

// 在dispatchTouchEvent、onTouchEvent方法中,都會調用startNestedScroll()
case MotionEvent.ACTION_DOWN: {
    mScrollPointerId = e.getPointerId(0);
    mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
    mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

    int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
    if (canScrollHorizontally) {
        nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
    }
    if (canScrollVertically) {
        nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
    }
    // 如果canScroll,就啓動嵌套滑動,通知父view。至於如何通知,見下面源碼
    startNestedScroll(nestedScrollAxis);
} break;


NestedScrollingChildHelper源碼:

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            // 通知父view,如果父view接受嵌套滑動,就調用其onNestedScrollAccepted()方法
            // 如果父view不接受嵌套滑動,就繼續通知爺view......直到找到一個接受的
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                // 如果找到一個接受嵌套滑動的父view,就直接返回了,不再繼續通知爺view了
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}


NestedScrollView源碼:

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    // 如果是縱向滑動,就接受,返回true;否則就返回false
    return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

 @Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    // 記錄嵌套滑動的方向
    mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    // 父view接受了嵌套滑動之後,子view就直接返回,不再繼續通知爺view了,所以通知爺view就由父view來完成;
    // 父view也繼承了NestedScrollingChild接口,並和子view一樣使用同樣的helper,通知爺view的方式也是一樣
}


滑動前

RecyclerView源碼:

case MotionEvent.ACTION_MOVE: {
    final int index = e.findPointerIndex(mScrollPointerId);
    if (index < 0) {
        Log.e(TAG, "Error processing scroll; pointer index for id " +
                            mScrollPointerId + " not found. Did any MotionEvents get skipped?");
        return false;
    }

    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;

    // 通知父view,把滑動情況通知給父view。如果父view處理了,就減去父view消費的距離,剩下的就是子view需要滑動的距離
    // 把實參mScrollConsumed傳過去,父、爺view消費的距離會記錄在上面
    if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
        dx -= mScrollConsumed[0];
        dy -= mScrollConsumed[1];
        vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        // Updated the nested offsets
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    }
......
......
......


NestedScrollingChildHelper源碼:

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;
            // 通知父view執行onNestedPreScroll()方法
            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;
}


NestedScrollView源碼:

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        // 跟子view一樣,把滑動情況通知給爺view
        dispatchNestedPreScroll(dx, dy, consumed, null);
    }


滑動後

RecyclerView源碼:

// 該方法在dispatchTouchEvent、onTouchEvent中調用,調用時已完成滑動。參數x、y分別是手指滑動的距離
boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int unconsumedX = 0, unconsumedY = 0;
    int consumedX = 0, consumedY = 0;

    consumePendingUpdateOperations();
    if (mAdapter != null) {
        eatRequestLayout();
        onEnterLayoutOrScroll();
        TraceCompat.beginSection(TRACE_SCROLL_TAG);
        if (x != 0) {
            // 消費的距離就是子view真實滑動的距離
            consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
            // 手指滑動的距離減去已消費的距離,得到未消費的距離
            unconsumedX = x - consumedX;
        }
        if (y != 0) {
            consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
            unconsumedY = y - consumedY;
        }
        TraceCompat.endSection();
        repositionShadowingViews();
        onExitLayoutOrScroll();
        resumeRequestLayout(false);
    }
    if (!mItemDecorations.isEmpty()) {
        invalidate();
    }

    // 調用dispatchNestedScroll()通知父view本次滑動情況
    if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
        // Update the last touch co-ords, taking any scroll offset into account
        // mScrollOffset記錄的是,因爲父view的滑動,導致子view的偏移。通過這個偏移量,重新計算當前手指的位置,保證下次滑動的準確性
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        if (ev != null) {
            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
        }
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
    } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
        if (ev != null) {
            pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
        }
        considerReleasingGlowsOnScroll(x, y);
    }
    if (consumedX != 0 || consumedY != 0) {
        dispatchOnScrolled(consumedX, consumedY);
    }
    if (!awakenScrollBars()) {
        invalidate();
    }
    return consumedX != 0 || consumedY != 0;
}


NestedScrollingChildHelper源碼:

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) {
                // 父view滑動前,子view的位置
                mView.getLocationInWindow(offsetInWindow);
                startX = offsetInWindow[0];
                startY = offsetInWindow[1];
            }

            // 通知父view調用onNestedScroll()方法
            ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed,
                        dyConsumed, dxUnconsumed, dyUnconsumed);

            if (offsetInWindow != null) {
                // 父view滑動後,子view的位置
                mView.getLocationInWindow(offsetInWindow);
                // 父view滑動後的位置減去之前的位置,得到偏移量
                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;
}


NestedScrollView源碼:

@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed) {
    final int oldScrollY = getScrollY();
    // 滑動dyUnconsumed
    scrollBy(0, dyUnconsumed);
    final int myConsumed = getScrollY() - oldScrollY;
    final int myUnconsumed = dyUnconsumed - myConsumed;
    // 通知爺view滑動情況。由於把子view未消費的dy全部消費掉了,所以爺view在dy上不會有滑動了,也就自然不需要傳實參偏移量了
    dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}


fling滑動前

RecyclerView源碼:

// 在onTouchEvent方法的Action.UP中,會根據當前手指離開屏幕的方向、速度等,計算出最終可以滑動的距離,然後調用fling()方法,下面的代碼是fling()方法中的一段

// fling滑動前,通知父view,如果父view接受fling滑動,子view將不再處理fling滑動;否則就自己fling滑動
if (!dispatchNestedPreFling(velocityX, velocityY)) {
    final boolean canScroll = canScrollHorizontal || canScrollVertical;
    dispatchNestedFling(velocityX, velocityY, canScroll);

    if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
        return true;
    }

    if (canScroll) {
        velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
        velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
        mViewFlinger.fling(velocityX, velocityY);
        return true;
    }
}


NestedScrollingChildHelper源碼:

public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        // 通知父view調用onNestedPreFling()方法
        return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX,
                    velocityY);
    }
    return false;
}


NestedScrollView源碼:

@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
    // 繼續通知爺view
    return dispatchNestedPreFling(velocityX, velocityY);
}


fling滑動後

RecyclerView源碼:

// 跟fling滑動前是同一段代碼,就不做太多解釋

// 如果父view、爺view都沒有攔截,就通知父view,並執行fling滑動
if (!dispatchNestedPreFling(velocityX, velocityY)) {
    final boolean canScroll = canScrollHorizontal || canScrollVertical;
    // 如果自己能滑動,就自己fling,並通知父view
    dispatchNestedFling(velocityX, velocityY, canScroll);

    // 如果mOnFlingListener爲null,或正在fling中,就返回
    if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
        return true;
    }

    if (canScroll) {
        velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
        velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
        // 開始fling滑動
        mViewFlinger.fling(velocityX, velocityY);
        return true;
    }
}


NestedScrollingChildHelper源碼:

public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
    if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
        // 通知父view調用onNestedFling()方法
        return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX,
                    velocityY, consumed);
    }
    return false;
}


NestedScrollView源碼:

    @Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
    // 如果子view沒有滑動,就自己滑動
    if (!consumed) {
        flingWithNestedDispatch((int) velocityY);
        return true;
    }
    return false;
}
// 這裏有2個值得注意的點:
// 1、fling傳遞的參數不再是消費多少、未消費多少,而是隻有一個速度,說明google其實並不推薦兩個控件同時fling;
// 2、父view執行完fling後,也沒有向爺view通知,可能也是不希望讓爺view再對fling進行處理;


停止滑動

RecyclerView源碼:

// dispatchTouchEvent中,在ACTION_UP和ACTION_CANCEL中調用了stopNestedScroll()方法
case MotionEvent.ACTION_UP: {
            mVelocityTracker.clear();
            stopNestedScroll();
        } break;

        case MotionEvent.ACTION_CANCEL: {
            cancelTouch();
        }


NestedScrollingChildHelper源碼:

public void stopNestedScroll() {
    if (mNestedScrollingParent != null) {
        // 通知父view調用onStopNestedScroll()方法
        ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
        mNestedScrollingParent = null;
    }
}


NestedScrollView源碼:

@Override
public void onStopNestedScroll(View target) {
    // 停止滑動,並通知爺view
    mParentHelper.onStopNestedScroll(target);
    stopNestedScroll();
}
發佈了35 篇原創文章 · 獲贊 52 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章