CoordinatorLayout用法解析

概述

Google官方對(duì)它的概述如下:

CoordinatorLayout is a super-powered {@link android.widget.FrameLayout FrameLayout}.

CoordinatorLayout is intended for two primary use cases:
    As a top-level application decor or chrome layout
    As a container for a specific interaction with one or more child views

By specifying {@link CoordinatorLayout.Behavior Behaviors} for child views of a
CoordinatorLayout you can provide many different interactions within a single parent and those
views can also interact with one another. View classes can specify a default behavior when
used as a child of a CoordinatorLayout using the
{@link CoordinatorLayout.DefaultBehavior DefaultBehavior} annotation.

Behaviors may be used to implement a variety of interactions and additional layout
modifications ranging from sliding drawers and panels to swipe-dismissable elements and buttons
that stick to other elements as they move and animate.

Children of a CoordinatorLayout may have an
{@link CoordinatorLayout.LayoutParams#setAnchorId(int) anchor}. This view id must correspond
to an arbitrary descendant of the CoordinatorLayout, but it may not be the anchored child itself
or a descendant of the anchored child. This can be used to place floating views relative to
other arbitrary content panes.

Children can specify {@link CoordinatorLayout.LayoutParams#insetEdge} to describe how the
view insets the CoordinatorLayout. Any child views which are set to dodge the same inset edges by
{@link CoordinatorLayout.LayoutParams#dodgeInsetEdges} will be moved appropriately so that the
views do not overlap.

大概的意思也就是說:
CoordinatorLayout 是一個(gè)增強(qiáng)版的FrameLayout。(繼承自ViewGroup)

主要有兩個(gè)用途:
1雌隅、作為應(yīng)用的頂層視圖
2溪掀、作為一個(gè)可以指定子views之間相互作用的容器睹簇,通過給CoordinatorLayout的子View指定CoordinatorLayout.Behavior來提供子view之間不同的相互作用疆虚,也就是說可以通過自定義CoordinatorLayout.Behavior來定義子views之間的相互作用朗若。

CoordinatorLayout核心就在于協(xié)調(diào)子View之間的相互作用,而子View之間的相互作用是通過CoordinatorLayout.Behavior來定義的蓝角,Google實(shí)現(xiàn)了幾個(gè)繼承自CoordinatorLayout.Behavior的類:



注意:上面白底的是我自己自定義的Behavior阱穗,大家不必關(guān)心這兩個(gè)。

CoordinatorLayout處理子View的layout_behavior屬性的源碼分析

通過layout_behavior的名稱就可以知道使鹅,這個(gè)屬性肯定是CoordinatorLayout.LayoutParams解析的揪阶,那我們就看一下CoordinatorLayout.LayoutParams構(gòu)造方法:

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

從上面的代碼中可以得知,會(huì)先判斷CoordinatorLayout的子View是否設(shè)置CoordinatorLayout_Layout_layout_behavior(即在布局文件中設(shè)置的layout_behavior屬性)屬性患朱,如果在子View的布局中設(shè)置了layout_behavior屬性鲁僚,就會(huì)調(diào)用CoordinatorLayout類的parseBehavior方法,在該方法中會(huì)通過反射技術(shù)實(shí)例化Behavior:

static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
        Context.class,
        AttributeSet.class
};

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

    final String fullName;
    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 {
        // Assume stock behavior in this package (if we have one)
        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);
        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);
    }
}

由上面的代碼可知在實(shí)例化Behavior時(shí)裁厅,會(huì)調(diào)用Behavior的參數(shù)類型為Context和AttributeSet的構(gòu)造函數(shù)冰沙,這也是為什么在自定義Behavior是必現(xiàn)要實(shí)現(xiàn)這個(gè)構(gòu)造函數(shù)的原因。

CoordinatorLayout如何管理自己的子View

由于CoordinatorLayout中的子View之間是具有依賴關(guān)系的执虹,所以將CoordinatorLayout中的子View按照依賴關(guān)系保存到Directed Acyclic Graph(定向無環(huán)圖)中拓挥,首先通過下圖直觀的看一下DAG:



DAG 沒有環(huán),不走回頭路袋励、永遠(yuǎn)不回頭侥啤、不斷向前進(jìn)当叭。 DAG 可以重新繪制,讓所有邊朝著同一個(gè)方向延伸拓展愿棋、讓所有點(diǎn)有著先后次序科展。

CoordinatorLayout的onMeasure方法中會(huì)調(diào)用CoordinatorLayout的prepareChildren方法,prepareChildren就是用來將CoordinatorLayout中的子View按照依賴關(guān)系保存到DAG 中并且進(jìn)行拓?fù)渑判颍?/p>

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

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

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

    // Finally add the sorted graph list to our list
    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    // We also need to reverse the result since we want the start of the list to contain
    // Views which have no dependencies, then dependent views after that
    Collections.reverse(mDependencySortedChildren);
}

上面的代碼將CoordinatorLayout的子View按照依賴關(guān)系保存到DAG中糠雨,具體的算法細(xì)節(jié)有興趣的同學(xué)可以自己研究一下才睹,這里就不在講解了,最終DAG的結(jié)果可以通過上圖進(jìn)行理解甘邀,將上圖的數(shù)字原點(diǎn)想象成View琅攘,那箭頭的方向就是被依賴的關(guān)系。
然后通過基于dfs算法的拓?fù)渑判?/a>對(duì)CoordinatorLayout子View的DAG圖進(jìn)行排序松邪,下面通過一張圖直觀的看一下拓?fù)渑判颍?br>


最終得到拓?fù)渑判蚝蟮牧斜碇蠽iew之間的依賴順序一定和列表中的View的順序相同坞琴,例如上圖中的依賴9節(jié)點(diǎn)的節(jié)點(diǎn)一定在節(jié)點(diǎn)9的后面。

自定義CoordinatorLayout.Behavior

View之間的相互作用就是一個(gè)View監(jiān)聽另一個(gè)View的變化從而做出響應(yīng)逗抑,View的變化可以概括的分為兩類:

  1. View的大小剧辐、在父布局中位置、顯示狀態(tài)等發(fā)生改變
  2. View自身的內(nèi)容發(fā)生改變(比如內(nèi)容發(fā)生移動(dòng))

注意:通過下面的源碼分析可知邮府,CoordinatorLayout是通過監(jiān)聽視圖樹的繪制來監(jiān)聽子View的第一類變化荧关,如果子View發(fā)生了第一類的變化并且繪制區(qū)域發(fā)生了改變就會(huì)通知依賴該子View的兄弟View。因此子View顯示狀態(tài)的變化只有從VISIBLE --> GONE變化時(shí)才會(huì)導(dǎo)致依賴該子View的兄弟View得到通知褂傀,而INVISIBLE --> GONE和VISIBLE --> INVISIBLE 不會(huì)導(dǎo)致依賴該子View的兄弟View得到通知忍啤。

1. View的第一類變化

CoordinatorLayout的子View的第一類變化肯定會(huì)導(dǎo)致CoordinatorLayout的子View的繪圖區(qū)域發(fā)生改變,從而被重繪仙辟,因此在CoordinatorLayout的onAttachedToWindow方法中監(jiān)聽了視圖樹的繪制同波,代碼如下:

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    resetTouchBehaviors();
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        //添加視圖樹即將被繪制的監(jiān)聽器
        vto.addOnPreDrawListener(mOnPreDrawListener);
    }
    if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
        // We're set to fitSystemWindows but we haven't had any insets yet...
        // We should request a new dispatch of window insets
        ViewCompat.requestApplyInsets(this);
    }
    mIsAttachedToWindow = true;
}

當(dāng)視圖樹即將被重繪時(shí),OnPreDrawListener類的onPreDraw方法會(huì)被調(diào)用叠国,代碼如下:

@Override
public boolean onPreDraw() {
    onChildViewsChanged(EVENT_PRE_DRAW);
    return true;
}

接著CoordinatorLayout類的onChildViewsChanged方法被調(diào)用未檩,部分代碼如下:

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();
        ......
        // Get the current draw rect of the view
        final Rect drawRect = mTempRect1;
        getChildRect(child, true, drawRect);
        ......
        if (type == EVENT_PRE_DRAW) {
            // Did it change? if not continue
            final Rect lastDrawRect = mTempRect2;
            getLastChildRect(child, lastDrawRect);
            if (lastDrawRect.equals(drawRect)) {
                continue;
            }
            recordLastChildRect(child, drawRect);
        }

        // 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();
            final Behavior b = checkLp.getBehavior();
            //如果checkChild設(shè)置了layout_ behavior屬性且checkChild依賴于child
            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:
                        // 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);
                }
            }
        }
    }
}

onChildViewsChanged首先會(huì)按照拓?fù)渑判蚝蟮捻樞虮闅vCoordinatorLayout子View,然后判斷子View的繪制區(qū)域是否發(fā)生了改變粟焊,如果發(fā)生了改變讹挎,接著會(huì)通過該子View的兄弟View的Behavior實(shí)例的layoutDependsOn方法判斷兄弟View是否依賴于該子View,如果依賴吆玖,則會(huì)調(diào)用兄弟View的Behavior實(shí)例的onDependentViewChanged方法筒溃。

到此CoordinatorLayout 處理子View第一類變化的過程的源碼分析完畢,舉例如下:
這里我們實(shí)現(xiàn)一個(gè)簡(jiǎn)單的效果沾乘,讓一個(gè)View根據(jù)另一個(gè)View上下左右移動(dòng)怜奖。
1> 首先我們來自定義一個(gè)繼承自CoordinatorLayout.Behavior的類DependentBehavior,如下所示:

public class DependentBehavior extends CoordinatorLayout.Behavior<View> {

    private int initDisX = 0;

    public DependentBehavior() {
    }

    public DependentBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setInitDisX(int initDisX) {
        this.initDisX = initDisX;
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        //如果dependency的類型是ImageView翅阵,則就可以被child依賴
        return dependency instanceof ImageView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        //當(dāng)dependency發(fā)生移動(dòng)時(shí)歪玲,計(jì)算出child應(yīng)該偏移的距離迁央,然后讓child進(jìn)行偏移
        int offsetX = (dependency.getLeft() - child.getLeft()) - initDisX;
        int offsetY = dependency.getTop() - child.getTop();
        child.offsetLeftAndRight(offsetX);
        child.offsetTopAndBottom(offsetY);
        return true;
    }
}

2> 下面是應(yīng)用的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:custom="http://schemas.android.com/apk/res-auto">

    <ImageView
        android:id="@+id/iv_dependency"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:src="@drawable/second_pic"
        android:scaleType="centerCrop"
        android:layout_gravity="left|top"/>

    <ImageView
        android:id="@+id/iv_child"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:src="@drawable/third_pic"
        android:scaleType="centerCrop"
        android:layout_gravity="top|right"
        android:layout_marginRight="64dp"
        custom:layout_behavior="com.cytmxk.test.testmaterialdesign.DependentBehavior"/>

</android.support.design.widget.CoordinatorLayout>

3> 下面是上面布局文件對(duì)應(yīng)的fragment的源碼:

public class BehaviorFragment extends BaseFragment {

    private View root;
    private ImageView ivDependency;
    private ImageView ivChild;

    @Override
    protected int getLayoutResId() {
        return R.layout.fragment_behavior;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        root = super.onCreateView(inflater, container, savedInstanceState);
        initView();
        return root;
    }

    private int mLastX;
    private int mLastY;

    private void initView() {
        ivDependency = (ImageView) root.findViewById(R.id.iv_dependency);
        ivDependency.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                int x = (int) motionEvent.getX();
                int y = (int) motionEvent.getY();
                switch (motionEvent.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        // 記錄觸摸點(diǎn)坐標(biāo)
                        mLastX = x;
                        mLastY = y;
                        break;
                    case MotionEvent.ACTION_MOVE:
                        // 計(jì)算偏移量
                        int offsetX = x - mLastX;
                        int offsetY = y - mLastY;
                        ivDependency.offsetLeftAndRight(offsetX);
                        ivDependency.offsetTopAndBottom(offsetY);
                        break;

                    default:
                        break;
                }
                return true;
            }
        });
        ivChild = (ImageView) root.findViewById(R.id.iv_child);
        ivChild.postDelayed(new Runnable() {
            @Override
            public void run() {
                CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams)ivChild.getLayoutParams();
                final DependentBehavior dependentBehavior = (DependentBehavior) layoutParams.getBehavior();
                dependentBehavior.setInitDisX(ivDependency.getLeft() - ivChild.getLeft());
            }
        }, 100);
    }
}

最后的運(yùn)行結(jié)果如下:



上圖中左邊ImageView的運(yùn)動(dòng)是通過監(jiān)聽觸摸事件實(shí)現(xiàn)的,由于左邊ImageView的運(yùn)動(dòng)會(huì)導(dǎo)致視圖樹重繪并且左邊ImageView的繪制區(qū)域發(fā)生了改變滥崩,因此根據(jù)源碼分析自定義的DependentBehavior 類的layoutDependsOn會(huì)被調(diào)用岖圈,由于左邊ImageView的類型是ImageView類型,所以layoutDependsOn會(huì)返回true钙皮,layoutDependsOn返回true導(dǎo)致右邊ImageView會(huì)依賴于左邊ImageView蜂科,接著onDependentViewChanged方法會(huì)被調(diào)用,就可以在onDependentViewChanged方法中讓右邊ImageView對(duì)左邊ImageView的運(yùn)動(dòng)做出響應(yīng)短条,這樣就會(huì)實(shí)現(xiàn)左右兩張圖片的聯(lián)動(dòng)效果导匣。
layoutDependsOn和onDependentViewChanged方法的參數(shù)相同,第一個(gè)參數(shù)是CoordinatorLayout實(shí)例茸时,第二個(gè)參數(shù)是我們?cè)O(shè)置了Behavior的View贡定,第三個(gè)參數(shù)就是第二個(gè)參數(shù)依賴的View。

2. View的第二類變化

第二類的變化不會(huì)導(dǎo)致CoordinatorLayout子View的繪制區(qū)域發(fā)生改變可都,而是會(huì)導(dǎo)致CoordinatorLayout子View的內(nèi)容發(fā)生移動(dòng)缓待,由于繪制區(qū)域沒有發(fā)生改變,所以View的第一類變化的監(jiān)聽方法無法監(jiān)聽View的第二類變化渠牲。那么我們就已RecyclerView為例旋炒,因?yàn)镽ecyclerView作為CoordinatorLayout的子View時(shí)可以實(shí)現(xiàn)聯(lián)動(dòng)效果并且滑動(dòng)的是其內(nèi)容,我們首先通過一張時(shí)序圖直觀的理解RecyclerView實(shí)現(xiàn)嵌套滑動(dòng)的這個(gè)過程:

RecyclerView的嵌套滑動(dòng)流程

如上圖所示嘱兼,1、9贤徒、17步的操作都會(huì)觸發(fā)RecyclerView的onTouchEvent方法的執(zhí)行,
RecyclerView的部分相關(guān)源碼如下:

@Override
public boolean onTouchEvent(MotionEvent e) {
    ......
    switch (action) {
        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;

        ......

        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;

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

            ......
        } break;

        ......

        case MotionEvent.ACTION_UP: {
            ......
            resetTouch();
        } break;

        case MotionEvent.ACTION_CANCEL: {
            cancelTouch();
        } break;
    }

    if (!eventAddedToVelocityTracker) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();

    return true;
}

private void resetTouch() {
    if (mVelocityTracker != null) {
        mVelocityTracker.clear();
    }
    stopNestedScroll();
    releaseGlows();
}

private void cancelTouch() {
    resetTouch();
    setScrollState(SCROLL_STATE_IDLE);
}

在第1步中芹壕,當(dāng)用戶觸碰到RecyclerView所在的屏幕區(qū)域時(shí)會(huì)觸發(fā)MotionEvent.ACTION_DOWN事件,此時(shí)會(huì)調(diào)用startNestedScroll方法(第2步)接奈,傳給startNestedScroll方法的參數(shù)與
RecyclerView的滑動(dòng)方向有關(guān)(通常在為RecyclerView設(shè)置LayoutManager時(shí)設(shè)置RecyclerView的滑動(dòng)方向)踢涌;在第9步中,當(dāng)用戶在RecyclerView所在的屏幕區(qū)域上滑動(dòng)時(shí)會(huì)觸發(fā)MotionEvent.ACTION_MOVE事件序宦,此時(shí)會(huì)調(diào)用dispatchNestedPreScroll方法(第10步)睁壁,傳給dispatchNestedPreScroll方法的前兩個(gè)參數(shù)是RecyclerView的內(nèi)容在水平和垂直方向上偏移的距離;在第17步中互捌,當(dāng)用戶手指離開RecyclerView所在的屏幕區(qū)域時(shí)會(huì)觸發(fā)MotionEvent.ACTION_UP事件潘明,此時(shí)會(huì)調(diào)用resetTouch方法(第18步),resetTouch方法中會(huì)調(diào)用stopNestedScroll方法(第19步)秕噪。
下面我們來看一下上面提到的RecyclerView類中的startNestedScroll钳降、dispatchNestedPreScroll和stopNestedScroll方法的源碼:

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

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

@Override
public void stopNestedScroll() {
    getScrollingChildHelper().stopNestedScroll();
}

上面的三個(gè)方法會(huì)調(diào)用NestedScrollingChildHelper類的startNestedScroll(第3步)、dispatchNestedPreScroll(第11步)腌巾、stopNestedScroll(第20步)方法:

public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

public boolean 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;
            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;
}

public void stopNestedScroll() {
    if (mNestedScrollingParent != null) {
        ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView);
        mNestedScrollingParent = null;
    }
}

上面的三個(gè)方法會(huì)調(diào)用ViewParentCompat的onStartNestedScroll(第4步)遂填、onNestedPreScroll(第12步)铲觉、和onStopNestedScroll方法(第21步):

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes);
}

public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
        int[] consumed) {
    IMPL.onNestedPreScroll(parent, target, dx, dy, consumed);
}

public static void onNestedScroll(ViewParent parent, View target, int dxConsumed,
        int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    IMPL.onNestedScroll(parent, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}

下面我們看一下ViewParentCompat中的如下一段代碼:

static final ViewParentCompatImpl IMPL;
static {
    final int version = Build.VERSION.SDK_INT;
    if (version >= 21) {
        IMPL = new ViewParentCompatLollipopImpl();
    } else if (version >= 19) {
        IMPL = new ViewParentCompatKitKatImpl();
    } else if (version >= 14) {
        IMPL = new ViewParentCompatICSImpl();
    } else {
        IMPL = new ViewParentCompatStubImpl();
    }
}

由于我的手機(jī)是L的手機(jī),所以上面的三個(gè)方法會(huì)調(diào)用ViewParentCompatLollipopImpl的onStartNestedScroll(第5步)吓坚、onNestedPreScroll(第13步)撵幽、和onStopNestedScroll方法(第22步):

public boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    return ViewParentCompatLollipop.onStartNestedScroll(parent, child, target,
            nestedScrollAxes);
}

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

@Override
public void onStopNestedScroll(ViewParent parent, View target) {
    ViewParentCompatLollipop.onStopNestedScroll(parent, target);
}

上面的3個(gè)方法會(huì)調(diào)用ViewParentCompatLollipop的onStartNestedScroll(第6步)、onNestedPreScroll(第14步)和onStopNestedScroll方法(第23步):

public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
        int nestedScrollAxes) {
    try {
        return parent.onStartNestedScroll(child, target, nestedScrollAxes);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onStartNestedScroll", e);
        return false;
    }
}

public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy,
        int[] consumed) {
    try {
        parent.onNestedPreScroll(target, dx, dy, consumed);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onNestedPreScroll", e);
    }
}

public static void onStopNestedScroll(ViewParent parent, View target) {
    try {
        parent.onStopNestedScroll(target);
    } catch (AbstractMethodError e) {
        Log.e(TAG, "ViewParent " + parent + " does not implement interface " +
                "method onStopNestedScroll", e);
    }
}

因?yàn)镃oordinatorLayout實(shí)現(xiàn)了ViewParent 接口礁击,所以
上面的三個(gè)方法會(huì)調(diào)用父布局(即CoordinatorLayout類)的startNestedScroll(第7步)盐杂、onNestedPreScroll(第15步)和onStopNestedScroll方法(第24步):

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

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

@Override
public void onStopNestedScroll(View target) {
    mNestedScrollingParentHelper.onStopNestedScroll(target);

    final int childCount = getChildCount();
    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) {
            viewBehavior.onStopNestedScroll(this, view, target);
        }
        lp.resetNestedScroll();
        lp.resetChangedAfterNestedScroll();
    }

    mNestedScrollingDirectChild = null;
    mNestedScrollingTarget = null;
}

接著上面的三個(gè)方法會(huì)就會(huì)調(diào)用實(shí)現(xiàn)CoordinatorLayout.Behavior子類的onStartNestedScroll(第8步)、onNestedPreScroll(第16步)和onStopNestedScroll方法(第25步)客税。

注意:

  1. 在第3步到第8步的過程中况褪,會(huì)遍歷RecyclerView的所有祖先View,尋找第一個(gè)實(shí)現(xiàn)了ViewParent接口的onStartNestedScroll方法的祖先View并且該祖先View至少有一個(gè)設(shè)置了layout_behavior和behavior onStartNestedScroll方法的返回值為true的子View更耻,如果找到符合條件的祖先View测垛,第3步的方法就會(huì)返回true并且將該祖先View保存到NestedScrollingChildHelper實(shí)例的mNestedScrollingParent變量中,否者返回false秧均。

  2. 在1中食侮,判斷祖先View至少有一個(gè)設(shè)置了layout_behavior和behavior onStartNestedScroll方法的返回值為true的子View的過程是在第8步中完成的,當(dāng)某個(gè)子View的behavior onStartNestedScroll方法的返回值為true時(shí)目胡,就會(huì)調(diào)用該子View的LayoutParams acceptNestedScroll方法(參數(shù)為true)锯七,將該子View的LayoutParams中的mDidAcceptNestedScroll屬性設(shè)置為true。

  3. 在第11步中誉己,會(huì)直接將使用1中保存的mNestedScrollingParent當(dāng)做參數(shù)傳遞眉尸;在第15步中就會(huì)通過mNestedScrollingParent調(diào)用onNestedPreScroll方法。

  4. 在第16步中巨双,會(huì)遍歷mNestedScrollingParent的所有子View噪猾,如果子View的LayoutParams的mDidAcceptNestedScroll屬性為true并且設(shè)置了layout_behavior屬性,就會(huì)執(zhí)行該子View的behavior的onNestedPreScroll方法筑累。這也就證明了只有當(dāng)子View的behavior的onStartNestedScroll方法返回了true袱蜡,子View的behavior的onNestedPreScroll方法才有可能被執(zhí)行。

到此CoordinatorLayout 處理子View第二類變化的過程的源碼分析完畢慢宗,舉例如下:
這里我們實(shí)現(xiàn)一個(gè)簡(jiǎn)單的效果坪蚁,讓一個(gè)RecyclerView根據(jù)另一個(gè)RecyclerView上下滑動(dòng)而上下滑動(dòng)。
1> 首先我們來自定義一個(gè)繼承自CoordinatorLayout.Behavior的類ScrollBehavior镜沽,如下所示:

public class ScrollBehavior extends CoordinatorLayout.Behavior<View> {

    public ScrollBehavior() {
    }

    public ScrollBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    // 第一個(gè)參數(shù)就是CoordinatorLayout 實(shí)例敏晤,也就是當(dāng)前ScrollBehavior 實(shí)例對(duì)應(yīng)的View的祖先View
    // 第二個(gè)參數(shù)就是當(dāng)前ScrollBehavior 實(shí)例對(duì)應(yīng)的View
    // 第三個(gè)參數(shù)就是直接目標(biāo)View,比如第一個(gè)參數(shù)CoordinatorLayout 實(shí)例包含嵌套兩層的RecyclerView缅茉,那這個(gè)參數(shù)就是最外層的RecyclerView茵典。
    // 第四個(gè)參數(shù)就是目標(biāo)View,比如第一個(gè)參數(shù)CoordinatorLayout 實(shí)例包含嵌套兩層的RecyclerView宾舅,那這個(gè)參數(shù)就是手指觸屏區(qū)域?qū)?yīng)的最內(nèi)層的RecyclerView统阿。
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    // 前三個(gè)參數(shù)與上面的相同彩倚,第4和第5個(gè)參數(shù)代表在水平和垂直方向上的偏移量
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        child.scrollBy(dx, dy);
    }
}

2>下面是應(yīng)用的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview_dependency"
        android:layout_gravity="left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview_child"
        android:layout_gravity="right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_behavior="com.cytmxk.test.testmaterialdesign.ScrollBehavior" />

</android.support.design.widget.CoordinatorLayout>

3> 下面是上面布局文件對(duì)應(yīng)的fragment的源碼:

public class ScrollBehaviorFragment extends BaseFragment {

    private View root;
    private RecyclerView dependencyRV;
    private RecyclerView childRV;

    @Override
    protected int getLayoutResId() {
        return R.layout.fragment_scroll_behavior;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        root = super.onCreateView(inflater, container, savedInstanceState);
        initView();
        return root;
    }

    private void initView() {
        dependencyRV = (RecyclerView) root.findViewById(R.id.recyclerview_dependency);
        LinearLayoutManager layoutManager1 = new LinearLayoutManager(getActivity());
        MyAdapter adapter1 = new MyAdapter();
        dependencyRV.setLayoutManager(layoutManager1);
        dependencyRV.setAdapter(adapter1);
        childRV = (RecyclerView) root.findViewById(R.id.recyclerview_child);
        LinearLayoutManager layoutManager2 = new LinearLayoutManager(getActivity());
        MyAdapter adapter2 = new MyAdapter();
        childRV.setLayoutManager(layoutManager2);
        childRV.setAdapter(adapter2);
    }

    public class MyAdapter extends RecyclerView.Adapter {

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new MyViewHolder(LayoutInflater.from(getActivity()).inflate(R.layout.my_view_holder_item, parent, false));
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            ((MyViewHolder)holder).updateView("position : " + position);
        }

        @Override
        public int getItemCount() {
            return 100;
        }
    }

    public class MyViewHolder extends RecyclerView.ViewHolder {

        private TextView textView;

        MyViewHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.textview);
        }

        void updateView(String text) {
            textView.setText(text);
        }
    }
}

最后的運(yùn)行結(jié)果如下:


  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市扶平,隨后出現(xiàn)的幾起案子帆离,更是在濱河造成了極大的恐慌,老刑警劉巖结澄,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哥谷,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡麻献,警方通過查閱死者的電腦和手機(jī)们妥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來勉吻,“玉大人监婶,你說我怎么就攤上這事〕萏遥” “怎么了惑惶?”我有些...
    開封第一講書人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)短纵。 經(jīng)常有香客問我带污,道長(zhǎng),這世上最難降的妖魔是什么香到? 我笑而不...
    開封第一講書人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任鱼冀,我火速辦了婚禮,結(jié)果婚禮上悠就,老公的妹妹穿的比我還像新娘千绪。我一直安慰自己,他們只是感情好理卑,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開白布翘紊。 她就那樣靜靜地躺著蔽氨,像睡著了一般藐唠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鹉究,一...
    開封第一講書人閱讀 49,785評(píng)論 1 290
  • 那天宇立,我揣著相機(jī)與錄音,去河邊找鬼自赔。 笑死妈嘹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绍妨。 我是一名探鬼主播润脸,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼柬脸,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了毙驯?” 一聲冷哼從身側(cè)響起倒堕,我...
    開封第一講書人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎爆价,沒想到半個(gè)月后垦巴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铭段,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年骤宣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片序愚。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡憔披,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出展运,到底是詐尸還是另有隱情活逆,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布拗胜,位于F島的核電站蔗候,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏埂软。R本人自食惡果不足惜锈遥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望勘畔。 院中可真熱鬧所灸,春花似錦、人聲如沸炫七。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽万哪。三九已至侠驯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奕巍,已是汗流浹背吟策。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留的止,地道東北人檩坚。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親匾委。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拖叙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容