CoordinatorLayout 测量、布局及 Behavior 的调用机制

几年前 CoordinatorLayout 刚出来时,创建Activity会自动生成xml布局,默认的就是 CoordinatorLayout 为根节点,并且还有 AppBarLayout 、CollapsingToolbarLayout 等容器,当时看着头大,开始只是按照Demo知道怎么用,但没有深入原理去解读过,当时始终觉得 CoordinatorLayout 容器比较特殊,现在准备做个总结,简化对它的认识。如果不配置 Behavior 的情况下,可以认为 CoordinatorLayout 是个与 FrameLayout 相似的容器,我们先看看它的测量方法 onMeasure()

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        ensurePreDrawListener();
        ...
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            ...
            final Behavior b = lp.getBehavior();
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }
            ...
        }
        final int width = ViewCompat.resolveSizeAndState(widthUsed, widthMeasureSpec,
                childState & ViewCompat.MEASURED_STATE_MASK);
        final int height = ViewCompat.resolveSizeAndState(heightUsed, heightMeasureSpec,
                childState << ViewCompat.MEASURED_HEIGHT_STATE_SHIFT);
        setMeasuredDimension(width, height);
    }

这里面是简化后的代码,注意看前面的两个方法

    private void prepareChildren() {
        mDependencySortedChildren.clear();
        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = getResolvedLayoutParams(child);
            lp.findAnchorView(this, child);
            mDependencySortedChildren.add(child);
        }
        selectionSort(mDependencySortedChildren, mLayoutDependencyComparator);
    }

getResolvedLayoutParams() 方法是如果xml布局中没有设置 Behavior 路径,在此用注解方式生成,设置到 LayoutParams 中;selectionSort() 中完成 CoordinatorLayout 中子view按照依赖关系的排列,被依赖的 view 在前面,并把view保存在 mDependencySortedChildren 中。 ensurePreDrawListener() 方法牵涉到 Behavior 中的方法,一会统一讲解。我们再看看
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);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior behavior = lp.getBehavior();

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

注意看for循环中的代码,获取到view的 Behavior,如果为null,或者 Behavior 的 onLayoutChild() 返回为 false,也就是说不需要依赖view的位置决定自己的布局,此时执行 onLayoutChild(child, layoutDirection) 方法,这个方法中会对自己的位置进行布局。我们可以看得出,Behavior 的优先级似乎比 CoordinatorLayout 的高,此时看看 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;
            }
        }
        // 如果有依赖 
        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();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
        mNeedsPreDrawListener = true;
    }

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

会调用 dispatchOnDependentViewChanged() 方法,这个方法中,最关键的还是下面缩减的代码

    void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            ...
            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();
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    ...
                    final boolean handled = b.onDependentViewChanged(this, checkChild, child);
                    ...
                }
            }
        }
    }

for循环中,根据依赖关系,通过 Behavior 的 layoutDependsOn() 方法判断是否依赖,然后通过 onDependentViewChanged() 方法来控制依赖view的位置变化,最常见的场景是控件 A 和 B,需要 B 随着 A 的位移而位移,这时候,就可以自定义 Behavior,在 layoutDependsOn() 中判断要要依赖的view 是否是A,然后在 onDependentViewChanged() 方法中获取A的位置,计算出B的位置,进行位移。

Behavior 的 onMeasureChild() 和 onLayoutChild() 方法,是在 CoordinatorLayout 的 onMeasure() 和 onLayout() 中调用,可见,CoordinatorLayout 也是先把子view的测量和布局先让子view对应的 Behavior 来判断是否执行,如果不执行,CoordinatorLayout 才会走自己的逻辑。


Behavior 的方法比较多,上面简单的介绍了几个,现在看看 onDependentViewRemoved() 方法,看它的名字是被依赖的view从 CoordinatorLayout 中移除时的回调。ViewGroup 本身有移除view的监听,CoordinatorLayout 在构造方法中会去注册 setOnHierarchyChangeListener(new HierarchyChangeListener()),

    private class HierarchyChangeListener implements OnHierarchyChangeListener {
        @Override
        public void onChildViewAdded(View parent, View child) {
            if (mOnHierarchyChangeListener != null) {
                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
            }
        }

        @Override
        public void onChildViewRemoved(View parent, View child) {
            dispatchDependentViewRemoved(child);
            
            if (mOnHierarchyChangeListener != null) {
                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
            }
        }
    }

    void dispatchDependentViewRemoved(View view) {
        final int childCount = mDependencySortedChildren.size();
        boolean viewSeen = false;
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            if (child == view) {
                viewSeen = true;
                continue;
            }
            if (viewSeen) {
                CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)
                        child.getLayoutParams();
                CoordinatorLayout.Behavior b = lp.getBehavior();
                if (b != null && lp.dependsOn(this, child, view)) {
                    b.onDependentViewRemoved(this, child, view);
                }
            }
        }
    }

在这里可以看到,一旦触发回调,会执行 dispatchDependentViewRemoved() 方法,Behavior 中也是根据 dependsOn() 方法返回值,才会执行 onDependentViewRemoved()方法。

CoordinatorLayout 是个 ViewGroup 容器,则自然有 onInterceptTouchEvent() 和 onTouchEvent() 方法,简单的看看

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ...
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
        ...
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ...
        if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }
        if (mBehaviorTouchView == null) {
            handled |= super.onTouchEvent(ev);
        } else if (cancelSuper) {
            ...
            super.onTouchEvent(cancelEvent);
        }
        ...
        return handled;
    }

这两个方法中都调用了 performIntercept() 方法,区别是传进去的参数不一样,

    private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        ...
        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();
            // 步骤一
            if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
                if (b != null) {
                    ...
                    switch (type) {
                        case TYPE_ON_INTERCEPT:
                            b.onInterceptTouchEvent(this, child, cancelEvent);
                            break;
                        case TYPE_ON_TOUCH:
                            b.onTouchEvent(this, child, cancelEvent);
                            break;
                    }
                }
                continue;
            }
            // 步骤二
            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) {
                    mBehaviorTouchView = child;
                }
            }
            ...
        }
        topmostChildList.clear();
        return intercepted;
    }

根据意思,根据传进去的type值,Behavior 会调用它自身的 onInterceptTouchEvent() 或 onTouchEvent() 方法, ACTION_DOWN 时会执行 步骤二,其他的情况会执行 步骤一。CoordinatorLayout 中的 onTouchEvent() 方法也表明,事件分发机制还是优先交给 Behavior 执行,然后才是自身的逻辑。

至于 Behavior 中剩下几个滑动事件的方法,以 onStartNestedScroll() 为例 ,发现 CoordinatorLayout 中也有 onStartNestedScroll() 方法,关系如下

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

发现,都是 CoordinatorLayout 调用 Behavior 中的方法,这更像是个代理,这里简单的说一下自定义 Behavior 滑动事件的几个方法:

 onStartNestedScroll(): 嵌套滑动开始(ACTION_DOWN),确定 Behavior 是否要监听此次事件
 onStopNestedScroll(): 嵌套滑动结束(ACTION_UP 或 ACTION_CANCEL)

 onNestedPreScroll(): 嵌套滑动进行中,要监听的子 View 将要滑动,滑动事件即将被消费(最终被谁消费,可以通过代码控制)
 onNestedScroll(): 嵌套滑动进行中,要监听的子 View 的滑动事件已经被消费

 onNestedPreFling(): 要监听的子 View 即将快速滑动
 onNestedFling(): 要监听的子 View 在快速滑动中

至于 CoordinatorLayout 中对应的这几个方法是如何触发调用的,下一章再介绍。

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/100146474