Android CoordinatorLayout之源碼解析

源碼基於com.android.support:design:26.1.0,不同版本可能有所差異。

一、開始

上一篇Android CoordinatorLayout之自定義Behavior中,我們簡單介紹了CoordinatorLayout以及如何自定義Behavior。所以這次我們從源碼的角度分析CoordinatorLayout的內部實現機制,以便它更好的服務我們!

本文內容主要圍繞Behavior展開,還不瞭解Behavior的建議先看上一篇

二、Behavior的初始化

通常使用Behavior是通過給View設置layout_behavior屬性,屬性值是Behavior的路徑,很顯然,這個屬性值最終是綁定在了LayoutParams上。可以猜測,和FrameLayout等自帶ViewGroup類似,CoordinatorLayout內部也有一個LayoutParams類!嗯,找到了如下代碼段:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
 
    LayoutParams(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 是否設置了layout_behavior屬性
        mBehaviorResolved = a.hasValue(
                R.styleable.CoordinatorLayout_Layout_layout_behavior);
        if (mBehaviorResolved) {
            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);
        }
    }

首先判斷是否給View設置了layout_behavior屬性,如果設置了則先得到Behavior路徑再去解析Behavior,我們重點看下parseBehavior()方法:

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
    if (TextUtils.isEmpty(name)) {
        return null;
    }
    // behavior的全包名路徑
    final String fullName;
    if (name.startsWith(".")) {
        // 如果設置behavior路徑不包含包名,則需要拼接包名
        fullName = context.getPackageName() + name;
    } else if (name.indexOf('.') >= 0) {
        // 設置了behavior的全包名路徑
        fullName = name;
    } else {
        // 系統內部實現,WIDGET_PACKAGE_NAME代表android.support.design.widget
        fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                ? (WIDGET_PACKAGE_NAME + '.' + name)
                : name;
    }

    try {
        Map<String, Constructor<Behavior>> constructors = sConstructors.get();
        if (constructors == null) {
            constructors = new HashMap<>();
            sConstructors.set(constructors);
        }
        Constructor<Behavior> c = constructors.get(fullName);
        // 通過反射實例化behavior
        if (c == null) {
            final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                    context.getClassLoader());
            // 指定behavior的構造函數有兩個參數
            // CONSTRUCTOR_PARAMS = new Class<?>[] {Context.class, AttributeSet.class}
            // 這也是我們自定義behavior需要兩個參數的構造函數的原因
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        // 返回behavior的實例
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

parseBehavior()方法就是使用設置的Behavior的路徑進一步通過反射得到Behavior實例。如果要使用該Behavior實例,可通過CoordinatorLayout.LayoutParamsgetBehavior()方法得到:

public Behavior getBehavior() {
    return mBehavior;
}

除此之外,還可以在代碼中通過LayoutParamsView設置Behavior,例如修改上一篇demo-1的behavior設置方式:

TextView title = findViewById(R.id.title);
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) title.getLayoutParams();
params.setBehavior(new SampleTitleBehavior());

就是通過CoordinatorLayout.LayoutParamssetBehavior()方法完成的。

最後還有一種就是通過註解,系統的AppBarLayout就是用@CoordinatorLayout.DefaultBehavior()註解來設置behavior的:

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
}

如果某個View需要固定設置某個Behavior,註解是個不錯的選擇,至於註解的使用時機和原理後邊會提到的。

到此,給View設置Behavior的方式和原理就基本結束了!

三、CoordinatorLayout的測量、佈局

前邊已經分析了Behavior的初始化過程,初始化好了,總要用吧,在哪裏用呢?莫急,先看CoordinatorLayout在代碼層面是什麼:

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2 {
}

嗯,一個自定義ViewGroup,同時實現了NestedScrollingParent2接口,既然這樣,CoordinatorLayout必然遵循oMeasure()onLayout()的執行流程。

所以我們從CoordinatorLayoutonMeasure()方法開始分析(只保留了核心代碼):

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    prepareChildren();
    ensurePreDrawListener();

    final int childCount = mDependencySortedChildren.size();
    // 遍歷子View,並測量大小
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        if (child.getVisibility() == GONE) {
            // If the child is GONE, skip...
            continue;
        }

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        // 得到給View設置的Behavior
        final Behavior b = lp.getBehavior();
        if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                childHeightMeasureSpec, 0)) {
            onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0);
        }
    }
    setMeasuredDimension(width, height);
}

先看prepareChildren()方法:

private void prepareChildren() {
    mDependencySortedChildren.clear();
    mChildDag.clear();

    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);

        // 按照View之間的依賴關係,存儲View
        for (int j = 0; j < count; j++) {
            if (j == i) {
                continue;
            }
            final View other = getChildAt(j);
            if (lp.dependsOn(this, view, other)) {
                if (!mChildDag.contains(other)) {
                    // Make sure that the other node is added
                    mChildDag.addNode(other);
                }
                // Now add the dependency to the graph
                mChildDag.addEdge(other, view);
            }
        }
    }

    // mChildDag.getSortedList()會返回一個按照依賴關係排序後的View集合
    // 被依賴的View排在前邊,沒有被依賴的在後邊
    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    Collections.reverse(mDependencySortedChildren);
}

很明顯prepareChildren()就是完成CoordinatorLayout中子View按照依賴關係的排列,被依賴的View排在前面,並將結果保存在mDependencySortedChildren中,在每次測量前都會重新排序。

還記得我們前邊遺留了一個問題嗎?就是註解形式的Behavior初始化,答案就在getResolvedLayoutParams()中:

LayoutParams getResolvedLayoutParams(View child) {
    final LayoutParams result = (LayoutParams) child.getLayoutParams();
    if (!result.mBehaviorResolved) {
        Class<?> childClass = child.getClass();
        DefaultBehavior defaultBehavior = null;
        while (childClass != null &&
                (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
            childClass = childClass.getSuperclass();
        }
        if (defaultBehavior != null) {
            try {
                result.setBehavior(
                        defaultBehavior.value().getDeclaredConstructor().newInstance());
            } catch (Exception e) {
                Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +
                        " could not be instantiated. Did you forget a default constructor?", e);
            }
        }
        result.mBehaviorResolved = true;
    }
    return result;
}

其實很簡單,如果沒有通過layout_behavior或者java代碼給View設置Behavior,則LayoutParams的成員變量mBehaviorResolvedfalse,此時如果通註解設置了Behavior則會在此完成Behavior初始化操作。可見,通過註解設置的Behavior被處理的優先級最低。

先跳過ensurePreDrawListener()方法,繼續看onMeasure()方法剩下的代碼,即遍歷CoordinatorLayout的子View,注意這裏的子View從已排序的mDependencySortedChildren列表裏得到,先拿到ViewLayoutParams再從中取出Behavior,如果Behavior非空,並且重寫了onMeasureChild()方法,則按照重寫的規則測量該子View,否則執行系統的默認測量。可以發現,如果有需要我們可以重寫BehavioronMeasureChild()方法,攔截系統的默認onLayoutChild()方法。

繼續分析onLayout()方法:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        if (child.getVisibility() == GONE) {
            // If the child is GONE, skip...
            continue;
        }

        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior behavior = lp.getBehavior();

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

其實和onMeasure()方法類似,遍歷子View,確定其位置。同樣,如果有需要我們可以重寫BehavioronLayoutChild()方法,攔截系統的默認的onLayoutChild()方法。

初見端倪,我Behavior就是能爲所欲爲,想攔截就攔截。類似的Behavior攔截操作後邊還會繼續講到!

四、CoordinatorLayout中的依賴、監聽

上一篇我們講到自定義Behavior的第一種情況是某個View要監聽另一個View的位置、尺寸等狀態的變化,需要重寫layoutDependsOn()onDependentViewChanged()兩個方法,接下來剖析其中的原理。View狀態的變化必然導致重繪操作,想必有一個監聽狀態變化的接口吧。上邊我們將onMeasure()方法時有一個ensurePreDrawListener()沒說,答案就在裏邊,現在來看:

void ensurePreDrawListener() {
    boolean hasDependencies = false;
    final int childCount = getChildCount();
    // 判斷是否存在依賴關係
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        if (hasDependencies(child)) {
            hasDependencies = true;
            break;
        }
    }
    // mNeedsPreDrawListener默認爲false
    // 如果存在依賴關係
    if (hasDependencies != mNeedsPreDrawListener) {
        if (hasDependencies) {
            addPreDrawListener();
        } else {
            removePreDrawListener();
        }
    }
}

大致的作用是判斷CoordinatorLayout的子View之間是否存在依賴關係,如果存在則註冊監聽繪製的接口:

void addPreDrawListener() {
    if (mIsAttachedToWindow) {
        // Add the listener
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        // 給ViewTreeObserver註冊一個監聽繪製的OnPreDrawListener接口
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }

    // Record that we need the listener regardless of whether or not we're attached.
    // We'll add the real listener when we become attached.
    mNeedsPreDrawListener = true;
}

所以第一次註冊OnPreDrawListener接口是在onMeasure()裏,並不是在onAttachedToWindow()裏邊。看一下OnPreDrawListener具體實現:

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

顯然核心就在onChildViewsChanged(type)裏邊了,這裏typeEVENT_PRE_DRAW代表將要繪製,另外還有兩個type:EVENT_NESTED_SCROLL代表嵌套滾動、EVENT_VIEW_REMOVED代表View被移除。我們來分析onChildViewsChanged()方法(只保留了核心代碼):

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
    final int layoutDirection = ViewCompat.getLayoutDirection(this);
    final int childCount = mDependencySortedChildren.size();
    final Rect inset = acquireTempRect();
    final Rect drawRect = acquireTempRect();
    final Rect lastDrawRect = acquireTempRect();

    for (int i = 0; i < childCount; i++) {
        final View child = mDependencySortedChildren.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
            // Do not try to update GONE child views in pre draw updates.
            continue;
        }

        if (type != EVENT_VIEW_REMOVED) {
            // 檢查子View狀態是否改變
            getLastChildRect(child, lastDrawRect);
            if (lastDrawRect.equals(drawRect)) {
                continue;
            }
            // 記錄最後一次View的狀態
            recordLastChildRect(child, drawRect);
        }

        // 根據依賴關係更新View
        for (int j = i + 1; j < childCount; j++) {
            final View checkChild = mDependencySortedChildren.get(j);
            final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
            final Behavior b = checkLp.getBehavior();
            // 如果Behavior不爲空,並且checkChild依賴child,即重寫了layoutDependsOn
            // 則繼續執行if塊,否則當前循環到此結束!
            if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                // 如果type是EVENT_PRE_DRAW,並且checkChild在嵌套滑動後已經更新
                // 則重置標誌,進行下一次循環
                if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                    checkLp.resetChangedAfterNestedScroll();
                    continue;
                }
                
                final boolean handled;
                switch (type) {
                    case EVENT_VIEW_REMOVED:
                        // 如果type是EVENT_VIEW_REMOVED,即被依賴的view被移除
                        // 則需要執行Behavior的onDependentViewRemoved()
                        b.onDependentViewRemoved(this, checkChild, child);
                        handled = true;
                        break;
                    default:
                        // 如果type是EVENT_PRE_DRAW或者EVENT_NESTED_SCROLL
                        // 並且我們重寫了Behavior的onDependentViewChanged則執行該方法
                        handled = b.onDependentViewChanged(this, checkChild, child);
                        break;
                }

                if (type == EVENT_NESTED_SCROLL) {
                    // 記錄在嵌套滑動後是否已經更新了View
                    checkLp.setChangedAfterNestedScroll(handled);
                }
            }
        }
    }
}

onChildViewsChanged()方法中如果View之間有依賴關係,並重寫了相應BehaviorlayoutDependsOn()方法。則會執行BehavioronDependentViewChanged()onDependentViewRemoved()方法。這也就解釋了上一篇中第一種情況的原理,可見這裏起關鍵作用的還是Behavior

五、NestedScrolling機制

可能你沒聽過這個概念,但你可能已經使用過它了,例如CoordinatorLayout嵌套RecyclerView的佈局、NestedScrollView等,都有用到了這個原理。NestedScrolling提供了一套父View和子View嵌套滑動的交互機制,前提條件是父View需要實現NestedScrollingParent接口,子View需要實現NestedScrollingChild接口。按照NestedScrolling[Parent|Child]接口的要求(可查看接口的註釋),實現該接口的View需要創建一個NestedScrolling[Parent|Child]Helper幫助類實例來輔助子View和父View的交互。

先後構造一個符合NestedScrolling機制的場景,前邊我們已經提到了CoordinatorLayout實現了NestedScrollingParent2接口,NestedScrollingParent2又繼承自NestedScrollingParent,所以CoordinatorLayout做爲父View的條件是滿足的;其實RecyclerView實現了NestedScrollingChild2接口,NestedScrollingChild2又繼承自NestedScrollingChild接口也滿足作爲子View的條件。

NestedScrolling[Parent|Child]2接口可以看作是對NestedScrolling[Parent|Child]接口的擴展,本質和作用類似。

可以用上一篇自定義Behavior的第二種情況的例子來分析:在CoordinatorLayout裏邊嵌套一個RecyclerViewTextViewTextView跟隨RecyclerView的滑動來移動。根據上邊的分析,在CoordinatorLayout裏應該有一個NestedScrollingParentHelper的實例,在RecyclerView裏應該有一個NestedScrollingChildHelper的實例。

從哪裏開始分析呢?因爲例子的效果是從RecyclerView的滑動開始的,所以就從RecyclerView開始吧!滑動必然先進行事件的分發,所以先看它的onInterceptTouchEvent()方法(只保留核心代碼)是否有我們想要的東西:

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            break;
    }
}

發現了一個startNestedScroll()方法,和之前自定義Behavior重寫的onStartNestedScroll()方法有點像哦!目測找對地方了,繼續看startNestedScroll()方法:

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

原來是重寫了NestedScrollingParent2接口的方法,getScrollingChildHelper()做了什麼呢?

private NestedScrollingChildHelper getScrollingChildHelper() {
    if (mScrollingChildHelper == null) {
        mScrollingChildHelper = new NestedScrollingChildHelper(this);
    }
    return mScrollingChildHelper;
}

就是實例化上邊提到的NestedScrollingChildHelper,所以startNestedScroll(axes, type)就是該幫助類的方法:

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;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
                setNestedScrollingParentForType(type, p);
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

這裏插一段,isNestedScrollingEnabled()代表是否啓用了嵌套滑動,只有啓用了嵌套滑動,事件才能繼續分發。這個是在哪裏設置的呢?RecyclerView的構造函數中:

public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    // Re-set whether nested scrolling is enabled so that it is set on all API levels
    setNestedScrollingEnabled(nestedScrollingEnabled);
}

最終調用了NestedScrollingChildHelpersetNestedScrollingEnabled()方法:

@Override
public void setNestedScrollingEnabled(boolean enabled) {
    getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}

所以我們自定義的實現NestedScrollingChild接口的View,也需要設置setNestedScrollingEnabled(true),具體方法了RecyclerView中類似。好了插播結束,繼續往下看。

mView是什麼呢?還記得上邊創建mScrollingChildHelper的構造函數嗎?就是:

public NestedScrollingChildHelper(@NonNull View view) {
    mView = view;
}

所以mView就是RecyclerView,則p就應該是CoordinatorLayout,看下if條件ViewParentCompat.onStartNestedScroll()裏邊的實現:

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes, int type) {
    if (parent instanceof NestedScrollingParent2) {
        // First try the NestedScrollingParent2 API
        return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
                nestedScrollAxes, type);
    } else if (type == ViewCompat.TYPE_TOUCH) {
        // Else if the type is the default (touch), try the NestedScrollingParent API
        return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
    }
    return false;
}

看到了熟悉的NestedScrollingParent2,因爲CoordinatorLayout實現了該接口,這也更加確定了parent就是CoordinatorLayout,所以((NestedScrollingParent2) parent).onStartNestedScroll(child, target, nestedScrollAxes, type)就回到了CoordinatorLayout去執行:

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

    final int childCount = getChildCount();
    // 遍歷CoordinatorLayout的子View
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        if (view.getVisibility() == View.GONE) {
            // If it's GONE, don't dispatch
            continue;
        }
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        // 如果當前View有Behavior,則調用其onStartNestedScroll()方法。
        if (viewBehavior != null) {
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                    target, axes, type);
            handled |= accepted;
            lp.setNestedScrollAccepted(type, accepted);
        } else {
            lp.setNestedScrollAccepted(type, false);
        }
    }
    return handled;
}

該方法就是遍歷CoordinatorLayout的子View,如果有Behavior則調用它的onStartNestedScroll方法,如果返回true,則Behavior就攔截了這次事件,進一步可以更新對應的View狀態。

到這裏一個NestedScrolling機制交互的流程就走完了,先簡單總結一下,事件從RecyclerView開始到NestedScrollingChildHelper經過ViewParentCompat回到了CoordinatorLayout最後被Behavior處理掉了。

之前我們還重寫了BehavioronNestedPreScroll()方法,來處理RecyclerView的滑動事件,既然是滑動,那肯定逃不出onTouchEvent()ACTION_MOVE事件(只保留核心代碼):

@Override
public boolean onTouchEvent(MotionEvent e) {
    final int action = e.getActionMasked();
    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            final int index = e.findPointerIndex(mScrollPointerId);
            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;

            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                // Updated the nested offsets
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
            }
        } break;
    return true;
}

有一個dispatchNestedPreScroll()方法,繼續跟進,中間的流程和上的類似,可以自行打斷點跟一遍,我們重點看一下回到CoordinatorLayout中的onNestedPreScroll()方法:

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

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View view = getChildAt(i);
        if (view.getVisibility() == GONE) {
            // If the child is GONE, skip...
            continue;
        }

        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        if (!lp.isNestedScrollAccepted(type)) {
            continue;
        }

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

            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);
    }
}

同樣是遍歷子View,如果可以則執行View對應BehavioronNestedPreScroll()方法。當然這不是重點,這裏有個mTempIntPair數組,對應BehavioronNestedPreScroll()方法的consumed參數,所以之前我們寫consumed[1] = dy,實際是給mTempIntPair複製,最終讓父ViewCoordinatorLayout消費掉事件,也就是你滑動的是RecyclerView但實際上CoordinatorLayout在整體移動!

所以在NestedScrolling機制中,當實現了NestedScrollingChild 接口的子View滑動時,現將自己滑動的dxdy傳遞給實現了NestedScrollingParent接口的父View,讓View先決定是否要消耗相應的事件,父View可以消費全部事件,如果父View消耗了部分,則剩下的再由子View處理。

六、CoordinatorLayout的TouchEvent

CoordinatorLayout作爲一個自定義ViewGroup,必然會重寫onInterceptTouchEvent()onTouchEvent來進行事件的攔截和處理。結合之前多次的分析結果,可以猜測這兩個方法最終也由相應子ViewBehavior來處理!

看一下這兩個方法:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    MotionEvent cancelEvent = null;
    final int action = ev.getActionMasked();
    // 重置Behavior的相關記錄,爲下次事件做準備
    if (action == MotionEvent.ACTION_DOWN) {
        resetTouchBehaviors();
    }
    // 是否攔截當前事件是由performIntercept()的返回值決定的
    final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
    if (cancelEvent != null) {
        cancelEvent.recycle();
    }
    // 同樣是重置Behavior
    if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        resetTouchBehaviors();
    }
    return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    boolean handled = false;
    boolean cancelSuper = false;
    MotionEvent cancelEvent = null;

    final int action = ev.getActionMasked();
    // mBehaviorTouchView不爲空,表示某個View的Behavior正在處理當前事件
    if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
        // 繼續由相應的Behavior處理當前事件
        final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
        final Behavior b = lp.getBehavior();
        if (b != null) {
            handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
        }
    }
    return handled;
}

它們都調用了一個共同的方法performIntercept()

private boolean performIntercept(MotionEvent ev, final int type) {
    boolean intercepted = false;
    boolean newBlock = false;
    MotionEvent cancelEvent = null;
    final int action = ev.getActionMasked();

    final List<View> topmostChildList = mTempList1;
    // 按照 z-order 排序,讓最頂部的View先被處理
    getTopSortedChildren(topmostChildList);
    final int childCount = topmostChildList.size();
    for (int i = 0; i < childCount; i++) {
        final View child = topmostChildList.get(i);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        final Behavior b = lp.getBehavior();
        // 如果已經有Behavior攔截了事件或者是新的攔截,並且不是ACTION_DOWN
        if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
            if (b != null) {
                if (cancelEvent == null) {
                    final long now = SystemClock.uptimeMillis();
                    cancelEvent = MotionEvent.obtain(now, now,
                            MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                }
                 // 發送cancelEvent給攔截了事件之後的其它子View的Behavior
                switch (type) {
                    case TYPE_ON_INTERCEPT:
                        b.onInterceptTouchEvent(this, child, cancelEvent);
                        break;
                    case TYPE_ON_TOUCH:
                        b.onTouchEvent(this, child, cancelEvent);
                        break;
                }
            }
            continue;
        }
        // 如果當前事件還沒被攔截,則先由當前遍歷到的子View的Behavior處理
        if (!intercepted && b != null) {
            switch (type) {
                case TYPE_ON_INTERCEPT:
                    intercepted = b.onInterceptTouchEvent(this, child, ev);
                    break;
                case TYPE_ON_TOUCH:
                    intercepted = b.onTouchEvent(this, child, ev);
                    break;
            }
            if (intercepted) {
                // 記錄要處理當前事件的View
                mBehaviorTouchView = child;
            }
        }

        // Don't keep going if we're not allowing interaction below this.
        // Setting newBlock will make sure we cancel the rest of the behaviors.
        final boolean wasBlocking = lp.didBlockInteraction();
        final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
        newBlock = isBlocking && !wasBlocking;
        if (isBlocking && !newBlock) {
            // Stop here since we don't have anything more to cancel - we already did
            // when the behavior first started blocking things below this point.
            break;
        }
    }

    topmostChildList.clear();

    return intercepted;
}

相關的說明都在註釋裏了,所以CoordinatorLayout的事件處理,還是優先的交給子ViewBehavior來完成。

七、小結

到此,CoordinatorLayout的幾個重要的點就分析完了,其實核心還是CoordinatorLayoutBehavior之間的關係,理清了這個也就明白爲什麼Behavior可以實現攔截一切的效果!對自定義Behavior也是有很大幫助的。可以發現CoordinatorLayout最終都是將各種處理優先交給了Behavior來完成,所以CoordinatorLayout更像是Behavior的代理!

如果有不合理的地方還望指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章