CoordinatorLayout用法解析

概述

Google官方对它的概述如下:

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 是一个增强版的FrameLayout。(继承自ViewGroup)

主要有两个用途:
1、作为应用的顶层视图
2、作为一个可以指定子views之间相互作用的容器,通过给CoordinatorLayout的子View指定CoordinatorLayout.Behavior来提供子view之间不同的相互作用,也就是说可以通过自定义CoordinatorLayout.Behavior来定义子views之间的相互作用。

CoordinatorLayout核心就在于协调子View之间的相互作用,而子View之间的相互作用是通过CoordinatorLayout.Behavior来定义的,Google实现了几个继承自CoordinatorLayout.Behavior的类:



注意:上面白底的是我自己自定义的Behavior,大家不必关心这两个。

CoordinatorLayout处理子View的layout_behavior属性的源码分析

通过layout_behavior的名称就可以知道,这个属性肯定是CoordinatorLayout.LayoutParams解析的,那我们就看一下CoordinatorLayout.LayoutParams构造方法:

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

从上面的代码中可以得知,会先判断CoordinatorLayout的子View是否设置CoordinatorLayout_Layout_layout_behavior(即在布局文件中设置的layout_behavior属性)属性,如果在子View的布局中设置了layout_behavior属性,就会调用CoordinatorLayout类的parseBehavior方法,在该方法中会通过反射技术实例化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);
    }
}

由上面的代码可知在实例化Behavior时,会调用Behavior的参数类型为Context和AttributeSet的构造函数,这也是为什么在自定义Behavior是必现要实现这个构造函数的原因。

CoordinatorLayout如何管理自己的子View

由于CoordinatorLayout中的子View之间是具有依赖关系的,所以将CoordinatorLayout中的子View按照依赖关系保存到Directed Acyclic Graph(定向无环图)中,首先通过下图直观的看一下DAG:



DAG 沒有环,不走回头路、永远不回头、不断向前进。 DAG 可以重新绘制,让所有边朝着同一个方向延伸拓展、让所有点有着先后次序。

CoordinatorLayout的onMeasure方法中会调用CoordinatorLayout的prepareChildren方法,prepareChildren就是用来将CoordinatorLayout中的子View按照依赖关系保存到DAG 中并且进行拓扑排序:

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按照依赖关系保存到DAG中,具体的算法细节有兴趣的同学可以自己研究一下,这里就不在讲解了,最终DAG的结果可以通过上图进行理解,将上图的数字原点想象成View,那箭头的方向就是被依赖的关系。
然后通过基于dfs算法的拓扑排序对CoordinatorLayout子View的DAG图进行排序,下面通过一张图直观的看一下拓扑排序:


最终得到拓扑排序后的列表中View之间的依赖顺序一定和列表中的View的顺序相同,例如上图中的依赖9节点的节点一定在节点9的后面。

自定义CoordinatorLayout.Behavior

View之间的相互作用就是一个View监听另一个View的变化从而做出响应,View的变化可以概括的分为两类:

  1. View的大小、在父布局中位置、显示状态等发生改变
  2. View自身的内容发生改变(比如内容发生移动)

注意:通过下面的源码分析可知,CoordinatorLayout是通过监听视图树的绘制来监听子View的第一类变化,如果子View发生了第一类的变化并且绘制区域发生了改变就会通知依赖该子View的兄弟View。因此子View显示状态的变化只有从VISIBLE --> GONE变化时才会导致依赖该子View的兄弟View得到通知,而INVISIBLE --> GONE和VISIBLE --> INVISIBLE 不会导致依赖该子View的兄弟View得到通知。

1. View的第一类变化

CoordinatorLayout的子View的第一类变化肯定会导致CoordinatorLayout的子View的绘图区域发生改变,从而被重绘,因此在CoordinatorLayout的onAttachedToWindow方法中监听了视图树的绘制,代码如下:

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    resetTouchBehaviors();
    if (mNeedsPreDrawListener) {
        if (mOnPreDrawListener == null) {
            mOnPreDrawListener = new OnPreDrawListener();
        }
        final ViewTreeObserver vto = getViewTreeObserver();
        //添加视图树即将被绘制的监听器
        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;
}

当视图树即将被重绘时,OnPreDrawListener类的onPreDraw方法会被调用,代码如下:

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

接着CoordinatorLayout类的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();
        ......
        // 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设置了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首先会按照拓扑排序后的顺序遍历CoordinatorLayout子View,然后判断子View的绘制区域是否发生了改变,如果发生了改变,接着会通过该子View的兄弟View的Behavior实例的layoutDependsOn方法判断兄弟View是否依赖于该子View,如果依赖,则会调用兄弟View的Behavior实例的onDependentViewChanged方法。

到此CoordinatorLayout 处理子View第一类变化的过程的源码分析完毕,举例如下:
这里我们实现一个简单的效果,让一个View根据另一个View上下左右移动。
1> 首先我们来自定义一个继承自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) {
        //当dependency发生移动时,计算出child应该偏移的距离,然后让child进行偏移
        int offsetX = (dependency.getLeft() - child.getLeft()) - initDisX;
        int offsetY = dependency.getTop() - child.getTop();
        child.offsetLeftAndRight(offsetX);
        child.offsetTopAndBottom(offsetY);
        return true;
    }
}

2> 下面是应用的布局文件:

<?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> 下面是上面布局文件对应的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:
                        // 记录触摸点坐标
                        mLastX = x;
                        mLastY = y;
                        break;
                    case MotionEvent.ACTION_MOVE:
                        // 计算偏移量
                        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);
    }
}

最后的运行结果如下:



上图中左边ImageView的运动是通过监听触摸事件实现的,由于左边ImageView的运动会导致视图树重绘并且左边ImageView的绘制区域发生了改变,因此根据源码分析自定义的DependentBehavior 类的layoutDependsOn会被调用,由于左边ImageView的类型是ImageView类型,所以layoutDependsOn会返回true,layoutDependsOn返回true导致右边ImageView会依赖于左边ImageView,接着onDependentViewChanged方法会被调用,就可以在onDependentViewChanged方法中让右边ImageView对左边ImageView的运动做出响应,这样就会实现左右两张图片的联动效果。
layoutDependsOn和onDependentViewChanged方法的参数相同,第一个参数是CoordinatorLayout实例,第二个参数是我们设置了Behavior的View,第三个参数就是第二个参数依赖的View。

2. View的第二类变化

第二类的变化不会导致CoordinatorLayout子View的绘制区域发生改变,而是会导致CoordinatorLayout子View的内容发生移动,由于绘制区域没有发生改变,所以View的第一类变化的监听方法无法监听View的第二类变化。那么我们就已RecyclerView为例,因为RecyclerView作为CoordinatorLayout的子View时可以实现联动效果并且滑动的是其内容,我们首先通过一张时序图直观的理解RecyclerView实现嵌套滑动的这个过程:

RecyclerView的嵌套滑动流程

如上图所示,1、9、17步的操作都会触发RecyclerView的onTouchEvent方法的执行,
RecyclerView的部分相关源码如下:

@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步中,当用户触碰到RecyclerView所在的屏幕区域时会触发MotionEvent.ACTION_DOWN事件,此时会调用startNestedScroll方法(第2步),传给startNestedScroll方法的参数与
RecyclerView的滑动方向有关(通常在为RecyclerView设置LayoutManager时设置RecyclerView的滑动方向);在第9步中,当用户在RecyclerView所在的屏幕区域上滑动时会触发MotionEvent.ACTION_MOVE事件,此时会调用dispatchNestedPreScroll方法(第10步),传给dispatchNestedPreScroll方法的前两个参数是RecyclerView的内容在水平和垂直方向上偏移的距离;在第17步中,当用户手指离开RecyclerView所在的屏幕区域时会触发MotionEvent.ACTION_UP事件,此时会调用resetTouch方法(第18步),resetTouch方法中会调用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();
}

上面的三个方法会调用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;
    }
}

上面的三个方法会调用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();
    }
}

由于我的手机是L的手机,所以上面的三个方法会调用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个方法会调用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);
    }
}

因为CoordinatorLayout实现了ViewParent 接口,所以
上面的三个方法会调用父布局(即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;
}

接着上面的三个方法会就会调用实现CoordinatorLayout.Behavior子类的onStartNestedScroll(第8步)、onNestedPreScroll(第16步)和onStopNestedScroll方法(第25步)。

注意:

  1. 在第3步到第8步的过程中,会遍历RecyclerView的所有祖先View,寻找第一个实现了ViewParent接口的onStartNestedScroll方法的祖先View并且该祖先View至少有一个设置了layout_behavior和behavior onStartNestedScroll方法的返回值为true的子View,如果找到符合条件的祖先View,第3步的方法就会返回true并且将该祖先View保存到NestedScrollingChildHelper实例的mNestedScrollingParent变量中,否者返回false。

  2. 在1中,判断祖先View至少有一个设置了layout_behavior和behavior onStartNestedScroll方法的返回值为true的子View的过程是在第8步中完成的,当某个子View的behavior onStartNestedScroll方法的返回值为true时,就会调用该子View的LayoutParams acceptNestedScroll方法(参数为true),将该子View的LayoutParams中的mDidAcceptNestedScroll属性设置为true。

  3. 在第11步中,会直接将使用1中保存的mNestedScrollingParent当做参数传递;在第15步中就会通过mNestedScrollingParent调用onNestedPreScroll方法。

  4. 在第16步中,会遍历mNestedScrollingParent的所有子View,如果子View的LayoutParams的mDidAcceptNestedScroll属性为true并且设置了layout_behavior属性,就会执行该子View的behavior的onNestedPreScroll方法。这也就证明了只有当子View的behavior的onStartNestedScroll方法返回了true,子View的behavior的onNestedPreScroll方法才有可能被执行。

到此CoordinatorLayout 处理子View第二类变化的过程的源码分析完毕,举例如下:
这里我们实现一个简单的效果,让一个RecyclerView根据另一个RecyclerView上下滑动而上下滑动。
1> 首先我们来自定义一个继承自CoordinatorLayout.Behavior的类ScrollBehavior,如下所示:

public class ScrollBehavior extends CoordinatorLayout.Behavior<View> {

    public ScrollBehavior() {
    }

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

    @Override
    // 第一个参数就是CoordinatorLayout 实例,也就是当前ScrollBehavior 实例对应的View的祖先View
    // 第二个参数就是当前ScrollBehavior 实例对应的View
    // 第三个参数就是直接目标View,比如第一个参数CoordinatorLayout 实例包含嵌套两层的RecyclerView,那这个参数就是最外层的RecyclerView。
    // 第四个参数就是目标View,比如第一个参数CoordinatorLayout 实例包含嵌套两层的RecyclerView,那这个参数就是手指触屏区域对应的最内层的RecyclerView。
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    // 前三个参数与上面的相同,第4和第5个参数代表在水平和垂直方向上的偏移量
    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>下面是应用的布局文件:

<?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> 下面是上面布局文件对应的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);
        }
    }
}

最后的运行结果如下:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容