事件分發(3)-全面的CoordinateLayout和Behavior源碼分析

主目錄見:Android高級進階知識(這是總目錄索引)
本來以為這個知識點大家已經說得很多了,直接鏈接別人文章就可以了,可是后面看看講的感覺不是很清楚,所以決定這邊多寫這一篇文章,同時也是因為自定義behavior的例子寫完還需要完善,所以這篇就提前來說了。

我不是隨意的人

一.目標

自定義behavior其實是非常重要的知識,他決定了幾個控件怎么協調運動,做出合理且酷炫的效果,我們這篇文章的目標有以下幾點:
?1.了解layout_behavior屬性怎么加載的。
?2.了解layoutDependsOn和onDependentViewChanged調用過程。
?3.深入嵌套滑動的機制。
?4.為下一篇自定義behavior例子打基礎。

二.源碼分析

1.layout_behavior屬性加載

我們都知道我們在使用自定義behavior的時候,我們都要使用app:layout_behavior來引用。但是我們這個屬性是怎么被識別的呢?其實這個知識點我們已經講過了,我們知道攔截View的創建有自定義LayoutInflater(換膚框架(一)之Support v7庫解析)和重寫ViewGroup下面的LayoutParams(自定義ViewGroup(一)之卡牌)。其實這里用的方法是第二種。我們看下CoordinateLayout重寫的LayoutParams:

 public static class LayoutParams extends ViewGroup.MarginLayoutParams {
}

我們看到這里繼承MarginLayoutParams自定義了一個LayoutParams ,我們看這個方法里面是怎么獲取layout_behavior的:

   LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);

            final TypedArray a = context.obtainStyledAttributes(attrs,
                    R.styleable.CoordinatorLayout_Layout);

            this.gravity = a.getInteger(
                    R.styleable.CoordinatorLayout_Layout_android_layout_gravity,
                    Gravity.NO_GRAVITY);
            mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_Layout_layout_anchor,
                    View.NO_ID);
            this.anchorGravity = a.getInteger(
                    R.styleable.CoordinatorLayout_Layout_layout_anchorGravity,
                    Gravity.NO_GRAVITY);

            this.keyline = a.getInteger(R.styleable.CoordinatorLayout_Layout_layout_keyline,
                    -1);

            insetEdge = a.getInt(R.styleable.CoordinatorLayout_Layout_layout_insetEdge, 0);
            dodgeInsetEdges = a.getInt(
                    R.styleable.CoordinatorLayout_Layout_layout_dodgeInsetEdges, 0);
            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior);
            if (mBehaviorResolved) {
//判斷如果有自定義behavior的話就調用這個方法解析
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));
            }
            a.recycle();

            if (mBehavior != null) {
                // If we have a Behavior, dispatch that it has been attached
                mBehavior.onAttachedToLayoutParams(this);
            }
        }

我們看到這個方法其實很簡單,其實就是獲取自定義屬性,一共獲取的layout_gravity,layout_anchor,layout_anchorGravity,layout_keyline,layout_insetEdge,layout_dodgeInsetEdges,layout_behavior等自定義屬性。這些屬性大家應該都非常熟悉,這個就是使用CoordinateLayout會使用到的。我們看到最后調用了方法parseBehavior()來解析behavior:

 static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
//判斷這個behavior是否以.開頭,如果是以.開頭我們需要拼湊上我們應用的包名
        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
//如果我們包含.但是不是以.開頭,說明我們填寫的是全類名,直接反射即可
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
//不然我們判斷widget的包名是否為空,不為空就添加上widget的包名即可
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : name;
        }

        try {
//這個map是用來保存bahavior的構造函數的,獲取到通過反射構造函數來初始化自定義behavior
            Map<String, Constructor<Behavior>> constructors = sConstructors.get();
            if (constructors == null) {
                constructors = new HashMap<>();
                sConstructors.set(constructors);
            }
            Constructor<Behavior> c = constructors.get(fullName);
            if (c == null) {
                final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                        context.getClassLoader());
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
            return c.newInstance(context, attrs);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

上面的注釋已經說得非常清楚了,這個方法很簡單,其實就是根據我們傳進layout_behavior屬性的類名反射實例化我們的自定義behavior。到這里我們的behavior就被加載進來了。

2.layoutDependsOn和onDependentViewChanged調用過程

這個過程其實也不難,我們想想,一個控件運動,另外一個控件也會跟著運動,這個我們自己來實現要怎么實現呢?首先我們肯定會先確立好依賴關系,layoutDependsOn要做的事情就是判斷這個依賴關系是否成立的,然后我們肯定要監聽我們控件運動,我們知道我們平常會用到ViewTreeObserver來監聽視圖的狀態變化,然后我們監聽到變化后悔通知依賴的控件進行相應的運動,過程就完成了。這里CoordinateLayout也是這么干的。

 final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);

我們看到coordinateLayout里面的ViewTreeObserver里面設置了監聽mOnPreDrawListener,這個監聽器是干什么的呢?我們的邏輯肯定是在這個監聽器里面的,我們來看看這個監聽器:

    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

我們看了這個監聽器里面就調用了onChildViewsChanged方法,我們直接跟進去:

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
//得到依賴的視圖數量
        final int childCount = mDependencySortedChildren.size();
        final Rect inset = mTempRect4;
        inset.setEmpty();
        for (int i = 0; i < childCount; i++) {
//遍歷依賴的視圖
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
//根據anchor的值來停靠
                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }

//省略一些未說明代碼   
..........

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//得到視圖里面的behavior屬性對應的自定義behavior對象
                final Behavior b = checkLp.getBehavior();
//調用behavior的layoutDependsOn來判斷依賴關系是否成立
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        // If this is from a pre-draw and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
//最后我們會調用behavior的onDependentViewChanged來通知依賴對象說我們的視圖狀態已經改變了
                            // Otherwise we dispatch onDependentViewChanged()
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
    }

這段代碼略微有點長了,但是我已經是盡力把代碼省略了,留下來都是要講的關鍵代碼了。首先我們來看看mDependencySortedChildren是什么?這個就是記錄依賴對象的數據結構,那么我們這個是怎么確定這個依賴關系的呢?我們來看下這個mDependencySortedChildren是怎么構建的,我們在CoordinateLayout的prepareChildren()方法里面看到:

        // Finally add the sorted graph list to our list
        mDependencySortedChildren.addAll(mChildDag.getSortedList());

那么我們這里又要來看看mChildDag是個啥。首先我們看下他的聲明:

 private final DirectedAcyclicGraph<View> mChildDag = new DirectedAcyclicGraph<>();

我先告訴你這個是啥,這個其實是有向非循環圖,我們的依賴視圖其實就是添加進這個圖里面,然后程序會利用深度優先搜索(dfs)來檢索排序添加進去的視圖節點,現在我們來一步一步來看,首先我們看視圖是怎么添加進這個圖的,還是在prepareChildren()方法里面:

  for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);

            final LayoutParams lp = getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);

            mChildDag.addNode(view);

            // Now iterate again over the other children, adding any dependencies to the graph
            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                final LayoutParams otherLp = getResolvedLayoutParams(other);
                if (otherLp.dependsOn(this, other, view)) {
                    if (!mChildDag.contains(other)) {
                        // Make sure that the other node is added
                        mChildDag.addNode(other);
                    }
                    // Now add the dependency to the graph
                    mChildDag.addEdge(view, other);
                }
            }
        }

我們看到首先是遍歷CoordinateLayout的所有的一級子類,添加進去,所以我們要注意,我們自定義behavior只能使用在CoordinateLayout的一級子類上面,不然不會被監聽到,就是說如果是用在嵌套幾層層級關系的視圖上面是不會有效果的。然后添加完一級子類后會重新遍歷一級子類,把依賴的子類(用dependsOn判斷)的視圖用addEdge方法添加進來這個圖。添加進來之后我們看下后面調用了mChildDag.getSortedList()方法來進行排序:

   @NonNull
    ArrayList<T> getSortedList() {
        mSortResult.clear();
        mSortTmpMarked.clear();

        // Start a DFS from each node in the graph
        for (int i = 0, size = mGraph.size(); i < size; i++) {
            dfs(mGraph.keyAt(i), mSortResult, mSortTmpMarked);
        }

        return mSortResult;
    }

我們看到這個方法很簡單,就是遍歷圖中每個元素即添加進去的一級子類,然后用深度優先搜索(dfs)來分別遍歷每個一級子類的依賴視圖,我們來看下這個算法做了啥:

    private void dfs(final T node, final ArrayList<T> result, final HashSet<T> tmpMarked) {
        if (result.contains(node)) {
            // We've already seen and added the node to the result list, skip...
            return;
        }
        if (tmpMarked.contains(node)) {
            throw new RuntimeException("This graph contains cyclic dependencies");
        }
        // Temporarily mark the node
        tmpMarked.add(node);
        // Recursively dfs all of the node's edges
        final ArrayList<T> edges = mGraph.get(node);
        if (edges != null) {
            for (int i = 0, size = edges.size(); i < size; i++) {
                dfs(edges.get(i), result, tmpMarked);
            }
        }
        // Unmark the node from the temporary list
        tmpMarked.remove(node);
        // Finally add it to the result list
        result.add(node);
    }

這個方法做法很明確,遞歸搜索所有的依賴對象,然后加到result中,這樣我們的依賴關系就確定完畢了。那么我們視圖狀態變化就可以通知了,我們來看下后面是怎么通知,我們繼續看上面代碼:

 final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
//得到視圖里面的behavior屬性對應的自定義behavior對象
                final Behavior b = checkLp.getBehavior();
//調用behavior的layoutDependsOn來判斷依賴關系是否成立
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        // If this is from a pre-draw and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

這句代碼就是獲取節點后面的后一個節點,同樣這是從圖里面獲取的,我們取出后一個節點的behavior,然后調用behavior的layoutDependsOn方法來判斷這個節點是否依賴前面那個節點。如果有依賴關系則最后會調用:

    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

然后我們看到在default里面會調用behavior里面的onDependentViewChanged方法。
總結:到這里我們的layoutDependsOn和onDependentViewChanged就說完了,總過程就是先把依賴關系添加進圖里然后搜索添加進ArrayList里面,最后取出依賴視圖的behavior然后調用layoutDependsOn判斷是否有依賴關系,如果有則調用behavior的onDependentViewChanged通知。

大boss已經在路上

3.嵌套滑動的機制

這個機制應該說是CoordinateLayout的精髓了,非常精彩的源碼,看了熱血沸騰,你也有充分地理由來了解它!!!說這個之前我們先來明確下下面幾個類的作用:
?1.interface NestedScrollingChild
?2.class NestedScrollingChildHelper
?3.interface NestedScrollingParent
?4.class NestedScrollingParentHelper

為了能夠看得懂下面的源碼我先來串下整個過程:首先CoordinateLayout實現NestedScrollingParent接口,并且里面的滾動處理由NestedScrollingParentHelper來轉發調用。那么嵌套滑動到底是什么呢?其實就是實現了NestedScrollingChild接口的子類(RecyclerView,NestedScrollerView,SwipeRrefreshLayout所以CoordinateLayout必須包含這幾個控件之一,不然滑動事件不會被監聽到)等先接收到滑動事件,然后通過調用NestedScrollingChildHelper類中的相應方法,傳給CoordinateLayout(實現了NestedScrollingParent的子類)處理滑動事件,然后coordinateLayout調用相應的自定義behavior處理,完成后會把剩余的未消費的事件傳回實現了NestedScrollingChild接口的子類(RecyclerViewNestedScrollerView,SwipeRrefreshLayout)等。

整個過程講解的已經很明白了,所以嵌套滑動可以達到攔截RecyclerView等控件的滑動事件,決定消費掉多少滑動值,然后再交給RecyclerView自己來處理,這個功能非常管用,可以做好多效果。接下來我們舉RecyclerView為例子來走下代碼,我們從RecyclerView的onTouchEvent方法開始首先看下ACTION_DOWN:

   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;
                }
                startNestedScroll(nestedScrollAxis);
            } 
break;

我們看到在ACTION_DOWN事件中我們首先判斷滑動是垂直的還是水平的,然后調用startNestedScroll()方法:

 @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

我們看到這個地方調用了NestedScrollerChildHelper中的的startNestedScroll方法:

 public boolean startNestedScroll(int axes) {
        if (hasNestedScrollingParent()) {
            // Already in progress
            return true;
        }
        if (isNestedScrollingEnabled()) {
            ViewParent p = mView.getParent();
            View child = mView;
            while (p != null) {
//這個方法最終會調用到NestedScrollingParent中的onStartNestedScroll方法
                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;
    }

我們看到這個方法是循環向上查找實現了NestedScrollingParent接口的父類,找到了就調用onStartNestedScroll方法,我們這里coordinateLayout實現了NestedScrollingParent,所以這個地方我們調用了CoordinateLayout的onStartNestedScroll方法:

  @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                        nestedScrollAxes);
                handled |= accepted;

                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

我們看到這個地方CoordinateLayout會遍歷所以的子類,然后獲取他們的behavior,然后調用behavior的onStartNestedScroll()方法。所以onStartNestedScroll()是在事件DOWN中調用的,說明我要開始滑動了。接著我們開始看RecyclerView的onTouchEvent中的事件ACTION_MOVE做了啥:

  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;
//在這個地方調用dispatchNestedPreScroll最終會調用到NestedScrollingParent的onNestedPreScroll方法
                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
//這個地方mScrollConsumed記錄了子視圖behavior消費了多少的x軸方向距離和y軸方向距離
                    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) {
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        if (dx > 0) {
                            dx -= mTouchSlop;
                        } else {
                            dx += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        if (dy > 0) {
                            dy -= mTouchSlop;
                        } else {
                            dy += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
//然后把剩余的x軸方向滑動距離和y軸方向滑動距離傳給這個方法,這個方法最終會調用到NestedScrollingParent的onNestedScroll方法
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    if (ALLOW_PREFETCHING) {
                        mViewPrefetcher.postFromTraversal(dx, dy);
                    }
                }
            } break;

我們看到在MOVE事件中我們首先是調用到了dispatchNestedPreScroll方法,這個方法首先會調用NestedScrollingChildHelper的dispatchNestedPreScroll方法:

   @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

這樣的話我們直接就跟進NestedScrollingChildHelper的dispatchNestedPreScroll方法:

    public boolean
    dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {
........
                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;
    }

我們直接看到 consumed[0] = 0;consumed[1] = 0;這個數組待會用來保存x軸和y軸消費掉的滑動值,然后調用ViewParentCompat的onNestedPreScroll,我們繼續跟進這個方法:

      @Override
        public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
                int[] consumed) {
            if (parent instanceof NestedScrollingParent) {
                ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed);
            }
        }

我們看到這里調用了NestedScrollingParent子類的onNestedPreScroll方法,也就是調用到了CoordinateLayout的onNestedPreScroll()方法:

 @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
//同樣是遍歷所有子視圖,獲取到所有子視圖的behavior,然后調用他的onNestedPreScroll然后記錄下來消耗的x,y方向滑動值
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);

                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }

        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

到這里我們知道在ACTION_MOVE事件的時候會先調用behavior的onNestedPreScroll方法,然后看用戶在自定義的behavior里面消耗多少x,y方向的滑動值,然后保存起來。然后ACTION_MOVE事件我們繼續往下看:

 if (scrollByInternal(canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
  }

我們看到我們會把未消費掉的dx和dy傳進這個scrollByInternal()方法,我們看下這個方法做了些什么:

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0, unconsumedY = 0;
        int consumedX = 0, consumedY = 0;
.......
        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];
        } 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;
    }

我們看到這個方法又調用了dispatchNestedScroll()方法,我們跟進這個方法看做了啥:

  @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
            int dyUnconsumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

同樣的套路,這個方法最后調用了CoordinateLayout的onNestedScroll方法,也就是遍歷調用了子視圖的behavior的onNestedScroll方法。最后我們又會判斷有沒有消耗完如果還有剩余則會調用我們自己的事件來處理:

  if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }

到這里我們的RecyclerView的onTouchEvent方法的MOVE也就執行完成了。也就是說在MOVE方法里面我們會觸發子視圖自定義behavior的onNestedPreScroll方法和onNestedScroll方法。如果還有剩余的交給自己處理。這樣我們就剩下ACTION_UP方法了,其實我們知道onTouchEvent方法前面會先調用onInterceptTouchEvent的,我們的stop方法其實是在這里的UP調用的:

 case MotionEvent.ACTION_UP: {
                mVelocityTracker.clear();
                stopNestedScroll();
            } break;

這個過程跟前面其他的方法調用過程是一樣的,也是會調用到CoordinateLayout中各個視圖behavior的onStopNestedScroll()方法。到這里我們的嵌套滑動已經講解完畢,其實到這里已經大部分知識點已經講完了。但是我們還有兩個我們可能會用到的方法:onLayoutChild和onMeasureChild,這兩個方法其實還是蠻簡單的,我們這里也來說明說明。

4.onMeasureChild和onLayoutChild

這兩個方法在自定義控件時候我們已經知道,我們一個自定義控件會先測量然后再布局,首先我們來看onMeasureChild方法,這個方法主要是在CoordinateLayout類里面的onMeasure,我們程序會遍歷所有的子視圖,然后獲取他的behavior,然后調用他的onMeasureChild:

  final Behavior b = lp.getBehavior();
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }

我們看到如果behavior存在的話則會調用他的onMeasureChild方法,不然就會調用自己的onMeasureChild方法。同樣的onLayoutChild方法也是一樣的。是在onLayout里面,程序會在CoordinateLayout里面會遍歷所有子視圖,然后獲取他們的behavior,然后調用他們的onLayoutChild:

     if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }

這個也是一樣,如果behavior存在則會調用他們的onLayoutChild。所以我們在這兩個方法里面可以來測量和布局我們的視圖,當然這兩個方法有可能被調用多次,所以請注意一下。
總結:到這里我們的講解就已經完畢了,其實從講解中可以看到這個機制可以協調各個控件的整體運動,可以做好多效果,所以我們一定要完全掌握了這個機制,希望大家享受谷歌這個優秀的代碼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容