ITKeyword,专注技术干货聚合推荐

注册 | 登录

CoordinatorLayout源码解析之初识Behavior

chengkun_123 分享于 2017-08-01

2018阿里云全部产品优惠券(新购或升级都可以使用,强烈推荐)
领取地址https://promotion.aliyun.com/ntms/yunparter/invite.html

说在前面

CoordinatorLayout(下面简称CL)是Material Design中的明星控件了,学习它的源码不仅可以更好掌握这个控件的使用,而且可以更好地理解其他诸如AppbarLayout、FloatingActionButton等控件,还可以学习到一些有趣的思想。

可是当我像看其他ViewGroup(下面简称VG)的源码一样去看CL的源码的时候,我却发现了一个CL和其他VG最大的不同:几乎所有地方都有Behavior的参与,而CL本身并没有做什么事情。在大致了解了Behavior的作用之后,我决定从Behavior入手,聊一聊CL。

Behavior是什么

Behavior(下面简称Bh)是什么?它是CL中的一个静态内部类。挑一些重要的接口来看一看,注释在下面。从整体上来看,对CL来说,每一个子View的Bh像是一个代理,CL的几乎所有工作都会先去询问子View的Bh如何决策。先记住这一点,在下面我们会逐步详细说明。

public static abstract class Behavior<V extends View> {
        //事件分发和拦截相关
        public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {}
        public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {}
        //View之间互动相关
        public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {}
        public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) { }
        public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
        }

      //测量和布局相关
        public boolean onMeasureChild(CoordinatorLayout parent, V child,
                int parentWidthMeasureSpec, int widthUsed,
                int parentHeightMeasureSpec, int heightUsed) { }
        public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { }

      //嵌套滑动相关
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                V child, View directTargetChild, View target, int nestedScrollAxes) { }
        public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
                View directTargetChild, View target, int nestedScrollAxes) {}
        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {}
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {}
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dx, int dy, int[] consumed) {}
        public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
                float velocityX, float velocityY, boolean consumed) {}
        public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
                float velocityX, float velocityY) {}

        ...
    }

Behavior的绑定

通过为CL的直接子View绑定一个Bh,你可以拦截CL的一切触摸事件,测量,布局和嵌套滑动等。Bh本身并不能发挥什么功能,它要被绑定到相应的子View上才能在合适的时机被调用。绑定Bh有三种方式:

  • 在xml中指定为属性

    <android.support.v7.widget.RecyclerView
      android:layout_height=”wrap_content”
      android:layout_width=”match_parent”
      app:layout_behavior=”.MyBehavior” />

    这个属性中的Bh我们是自定义,或者使用系统的资源,假设我们是自定义的。

    public class MyBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
    
      public MyBehavior() {
      }
    
      public MyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    
      }
    }

    我们知道LayoutInflater在inflate整棵View树的时候,某个View的LayoutParams是通过root.generateLayoutParams(attrs)来获取的(也就是调用其父View的generateLayoutParams)。在这个时候它的LayoutParams就被初始化了,那么CL的直接子View就会初始化一个CoordinatorLayout.LayoutParams,这个Params是CL独有的,最大的特色就是包含了一个Bh,并在其初始化的时候会初始化这个Bh。

     LayoutParams(Context context, AttributeSet attrs) {
                super(context, attrs);
    
                final TypedArray a = context.obtainStyledAttributes(attrs,
                        R.styleable.CoordinatorLayout_Layout);
              ...
                mBehaviorResolved = a.hasValue(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior);
                if (mBehaviorResolved) {
                  //这里通过反射调用对应Bh的2参构造器初始化一个Bh。
                    mBehavior = parseBehavior(context, attrs, a.getString(
                            R.styleable.CoordinatorLayout_Layout_layout_behavior));
                }
                a.recycle();
                ...
            }

    通过xml指定的Bh是在setContentView()中被初始化且被CL的直接子View持有的(存在LayoutParams中)。

  • 程序中动态绑定

    //可以使用无参构造器
    MyBehavior myBehavior = new MyBehavior();
    CoordinatorLayout.LayoutParams params =
        (CoordinatorLayout.LayoutParams) myView.getLayoutParams();
    params.setBehavior(myBehavior);
  • 为你的View添加注解

    @CoordinatorLayout.DefaultBehavior(MyBehavior.class)
    public class MyView extends FrameLayout {
    
    }

    这样的View在xml文件中app:layout_behavior=”.MyBehavior”为空,只有在运行时去赋值,而CL选择在onMeasure中。

    //CoordinatorLayout.java
    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            prepareChildren();
            ....
        }
    
    private void prepareChildren() {
           //检查每一个子View
            for (int i = 0, count = getChildCount(); i < count; i++) {
                final View view = getChildAt(i);
                //“解决LayoutParams中的mBehavior”
                final LayoutParams lp = getResolvedLayoutParams(view);
                ...
            }
        }
    
        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 {
                        //调用无参构造器创建一个Bh
                        result.setBehavior(defaultBehavior.value().newInstance());
                    } 
                }
            }
            return result;
        }

    通过对View类进行注解为View赋值Bh发生在CL的onMeasure中。

Behavior与“依赖”效果

解决了”是什么”,”从哪来”两大哲学问题,接下来我们就可以看一下”到哪去”这个问题了。这个问题也就是Behavior到底有什么用?我们先来看它的第一个作用:形成View之间的依赖效果,即形成View之间的互动。

这个功能是唯一涉及到两个View功能。

首先我们依旧来看一下如何产生这样的效果,两种方式:

  • 在xml布局中指定属性
    假设一个FAB放在AppbarLayout的右下方,通过layout_anchor和layout_anchorGravity两个属性即可达到。

    <!-- 最外层是CL-->
    <android.support.design.widget.FloatingActionButton  android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_anchor="@id/appBarLayout" app:layout_anchorGravity="bottom|right" />

    在其LayoutParams初始化的时候获得了Anchor的id和gravity

    LayoutParams(Context context, AttributeSet attrs) {
                ....
                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);
                ...
            }

    然后我们查找mAnchorId和与之相关的方法,发现还是在prepareChildren()这个方法中确定了mAnchorView,即当前View所依赖的View。

    private void prepareChildren() {
    
            for (int i = 0, count = getChildCount(); i < count; i++) {
                ...
                final LayoutParams lp = getResolvedLayoutParams(view);
                //通过findViewById找到mAnchorView,并且找到mAnchorView的父辈中属于CL的直接子View的那一个(不清楚是干嘛的)
                lp.findAnchorView(this, view);
                ...
            }
                ...
        }
  • 在Bh的layoutDependsOn()方法中返回true
    我们寻找layoutDependsOn()方法,在CL中发现了为数不多的几处,一个地方是在onChildViewsChanged()这个方法中,而onChildViewsChanged()又分别在Scroll和Fling以及preDraw(OnPreDrawListener中)、onChildViewRemoved(HierarchyChangeListener)这些场景中被调用。所以我们猜想只要View发生了变化,这些地方都是两个View进行互动的地方! 看一下onChildViewsChanged(),也基本印证了我们的猜想。

    final void onChildViewsChanged(@DispatchChangeEvent final int type) {
            ....
            for (int i = 0; i < childCount; i++) {
                final View child = mDependencySortedChildren.get(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();
                //如果mDependencySortedChildren.get(j)是依赖于mDependencySortedChildren.get(i)。
                    if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                        ...
                        switch (type) {
                            case EVENT_VIEW_REMOVED:
                                //执行如果mDependencySortedChildren.get(j)的Bh的onDependentViewRemoved方法。
                                b.onDependentViewRemoved(this, checkChild, child);
                                handled = true;
                                break;
                            default:
                                handled = b.onDependentViewChanged(this, checkChild, child);
                                break;
                        }
                        ...
                    }
                }
            }
        }

    这里我们也找到了上面通过xml方式确定一个mAnchorView后两个View是如何联动的答案,也是在这个方法中,也就是说View产生的一系列变动是两个View联动的触因。

    final void onChildViewsChanged(@DispatchChangeEvent final int type) {
    
            for (int i = 0; i < childCount; i++) {
                final View child = mDependencySortedChildren.get(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
                for (int j = 0; j < i; j++) {
                    final View checkChild = mDependencySortedChildren.get(j);
                //如果mDependencySortedChildren.get(i)的lp.mAnchorDirectChild和mDependencySortedChildren.get(j)相等
                    if (lp.mAnchorDirectChild == checkChild) {
                        offsetChildToAnchor(child, layoutDirection);
                    }
                }
            .....
            }
        }
    
      void offsetChildToAnchor(View child, int layoutDirection) {
          final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //如果mAnchorView不空
          if (lp.mAnchorView != null) {
              ....
              if (changed) {
    
                  final Behavior b = lp.getBehavior();
                  //根据mAnchorView进行移动
                    if (b != null) {
                      b.onDependentViewChanged(this, child, lp.mAnchorView);
                  }
              }
          }
      }

看一个实例,一般最简单的布局是外层一个CL,里面一个AppbarLayout一个RecyclerView,此时我们会给RecyclerView添加这样的属性:

app:layout_behavior="@string/appbar_scrolling_view_behavior"

它对应的就是AppBarLayout的一个静态内部类ScrollingViewBehavior,而它在“依赖”上所做的事情就是让当前View处于AppBarLayout的下面:

//ScrollingViewBehavior.java
        @Override
        public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
            // We depend on any AppBarLayouts
            return dependency instanceof AppBarLayout;
        }

        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
                View dependency) {
            offsetChildAsNeeded(parent, child, dependency);
            return false;
        }
        private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
            final CoordinatorLayout.Behavior behavior =
                    ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
            if (behavior instanceof Behavior) {
                // Offset the child, pinning it to the bottom the header-dependency, maintaining
                // any vertical gap and overlap
                //让View处于AppBarLayout下面
                final Behavior ablBehavior = (Behavior) behavior;
                ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
                        + ablBehavior.mOffsetDelta
                        + getVerticalLayoutGap()
                        - getOverlapPixelsForOffset(dependency));
            }
        }

说一下mDependencySortedChildren

上面频繁出现了mDependencySortedChildren这个成员,我说一下我的理解。在CL中搜索,第一次赋值出现的位置是onMeasure()的prepareChildren()中。

//CoordinatorLayout#prepareChildren()
mDependencySortedChildren.addAll(mChildDag.getSortedList());

从名字上来看好像是和依赖有关系,而且和mChildDag这个成员有关系,继续搜索mChildDag,它是一个DirectedAcyclicGraph,其注释是:代表了一个简单的不可循环的graph(…..没看懂)。

/** * A class which represents a simple directed acyclic graph. */
final class DirectedAcyclicGraph<T>

不过没关系,我们继续搜索它被赋值的地方。mChildDag的addNode()和addEdge()都是操作的名为 mGraph的SimpleArrayMap,该SimpleArrayMap存放的是(T,ArrayList< T >)的键值对,也就是说存进mChildDag的每一个View都对应一个ArrayList< View > ,这个ArrayList< View > 保存的是依附于它的View的集合。

private void prepareChildren() {

        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);
            ...
            //先把getChildAt(i)添加到mChildDag中
            mChildDag.addNode(view);
            for (int j = 0; j < count; j++) {
                //只考虑i以外的View
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                final LayoutParams otherLp = getResolvedLayoutParams(other);
                //如果getChildAt(j)依赖于getChildAt(i)
                if (otherLp.dependsOn(this, other, view)) {
                    if (!mChildDag.contains(other)) {
                        //添加getChildAt(j)到mChildDag中
                        mChildDag.addNode(other);
                    }
                    //添加getChildAt(j)到getChildAt(i)对应的ArrayList中
                    mChildDag.addEdge(view, other);
                }
            }
        }
        // 获取一个List添加到mDependencySortedChildren中
        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);
    }

然后调用了mChildDag.getSortedList()获取一个List并添加到mDependencySortedChildren中,并且最后还要把mDependencySortedChildren倒序一下,注释的意思是说我们开始的时候是想从没有dependency的View开始(从后面的方法实现可以看出,没有dependency的view后添加进List,也就是说产生变化的View放在前面,被动联动的View放在后面)。

mChildDag.getSortedList()这个List很有意思,获取的过程概括的说就是用 dfs+回溯 来返回一个基于依赖关系的列表。重要的都注释在下面了。

//用于存放结果
    private final ArrayList<T> mSortResult = new ArrayList<>();
    //用于保护现场
    private final HashSet<T> mSortTmpMarked = new HashSet<>();


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

        // 开始dfs
        for (int i = 0, size = mGraph.size(); i < size; i++) {
          //三个参数分别是:一个View,一个用于存放结果的ArrayList,一个用于记录的HashSet
            dfs(mGraph.keyAt(i), mSortResult, mSortTmpMarked);
        }
        return mSortResult;
    }
    //开始dfs收集List
    private void dfs(final T node, final ArrayList<T> result, final HashSet<T> tmpMarked) {
        //如果已经将这个View添加在了结果中
        if (result.contains(node)) {
            //直接返回
            return;
        }
        //如果这个View已经在tmpMarked中了,代表循环依赖了,抛出异常(因为是在每个主View对应的ArrayList中进行dfs,而ArrayList中的View代表依赖了主View,如果两者相等就是循环依赖)
        if (tmpMarked.contains(node)) {
            throw new RuntimeException("This graph contains cyclic dependencies");
        }
        // 标记这个View为已经检查
        tmpMarked.add(node);
        // 取出这个View对应的ArrayList(代表依赖这个View的所有View)
        final ArrayList<T> edges = mGraph.get(node);
        if (edges != null) {
            //从这个View对应的ArrayList中继续递归
            for (int i = 0, size = edges.size(); i < size; i++) {
                dfs(edges.get(i), result, tmpMarked);
            }
        }
        // 还原现场
        tmpMarked.remove(node);
        //很关键
        // 先递归后添加,所以最先添加的是依赖其他View的View
        result.add(node);
    }

Behavior与嵌套滑动

我们把比较复杂的两个View的依赖讲完了,Bh的作用还有很多,这里讲一下它作为嵌套滑动机制里CL的代理作用。嵌套滑动机制在我上一篇文章中已经讲过了,还不清楚的童鞋我强烈建议去看一下。

我们知道嵌套滑动里的child在onTouchEvent()的DOWN中会起调startNestedScroll(),假设parent是CL,那么会回调它的onStartNestedScroll(),这个方法里会做什么呢?它把回调交给感兴趣的直接子View去处理了!

@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) {
                //交给某个View的Bh去处理
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                        nestedScrollAxes);
                //只要有一个感兴趣就表示对接成功
                handled |= accepted;
                //在该Bh中记录是否接受了嵌套滑动
                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

同理在onNestedPreScroll()、onNestedScroll()、onNestedPreFling()、onNestedFling()也都是交给直接子View去处理了。记住这么几点:你不必为你的RV或其他ScrollingView添加依赖,因为CL的每一个直接子View都有机会去处理嵌套滑动;滑动的起调者不一定是CL的直接子View,嵌套滑动事件会传到CL的直接子View罢了。

还有,从源码来看,一次嵌套滑动可以有多个parent也就是多个CL的直接子View响应,但是在嵌套滑动开始之后这个关系是不能改变的。

我们还是举出一个具体的栗子,看一看直接子View究竟是怎样帮助CL去处理嵌套滑动的。比如RecyclerView、CoordinatorLayout 、AppBarLayout的组合,RV是作为嵌套机制里面的child,CL是作为parent,AppBarLayout是用注解的方式获取了一个Bh的CL的直接子View,那么CL作为parent嵌套滑动是要交给AppBarLayout处理。

通常是要给RV的布局中加上app:layout_behavior=”@string/appbar_scrolling_view_behavior 这样一个属性,这样RV就具有了Bh,这个Bh让RV依赖于AppBarLayout(一直让RV在其下方),但这里依赖关系不是重点!依赖关系和嵌套滑动机制是相互独立的!

嵌套滑动的流程我就不再重述了。首先是调用startNestedScroll()启动嵌套滑动时,贴一张图:

图片来源于网络

既然都说了AppBarLayout会帮助CL处理,那么我们有理由相信在其Bh的onStartNestedScroll()中会做相应判断:

//AppBarLayout$Behavior.java 
    @Override
        public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                View directTargetChild, View target, int nestedScrollAxes) {

            //如果是 纵向 + 有可滑动的子View + 还有空间滑动 则承诺作为嵌套滑动中的parent
            //(这里的directTargetChild是指嵌套滑动中的child向上寻找parent的倒数第二个parent...也可能是child本身)
            final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
                    && child.hasScrollableChildren()
                    && parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
            ...
            return started;
        }

然后是RV在onTouchEvent()的MOVE中调用dispatchNestedPreScroll()的时候以及AppBarLayout处理完RV也处理完之后继续调用ispatchNestedScroll()的时候,也贴一张图:
图片来源于网络

AppBarLayout也做了相应的处理,都和滚动相关,我就不展开了:

 //AppBarLayout$Behavior.java 
        @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dx, int dy, int[] consumed) {
            if (dy != 0 && !mSkipNestedPreScroll) {
                int min, max;
                if (dy < 0) {
                    // We're scrolling down
                    min = -child.getTotalScrollRange();
                    max = min + child.getDownNestedPreScrollRange();
                } else {
                    // We're scrolling up
                    min = -child.getUpNestedPreScrollRange();
                    max = 0;
                }
                consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
            }
        }

        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed) {
            if (dyUnconsumed < 0) {
                // If the scrolling view is scrolling down but not consuming, it's probably be at
                // the top of it's content
                scroll(coordinatorLayout, child, dyUnconsumed,
                        -child.getDownNestedScrollRange(), 0);
                // Set the expanding flag so that onNestedPreScroll doesn't handle any events
                mSkipNestedPreScroll = true;
            } else {
                // As we're no longer handling nested scrolls, reset the skip flag
                mSkipNestedPreScroll = false;
            }
        }

Behavior与测量和布局

CL作为一个VG,测量和布局肯定是它的主要作用,这关系到系统如何绘制这个VG。

先来看一看onMeasure(),大致的逻辑就是先交给每个直接子View的Bh的onMeasureChild()去测量,如果返回false,调用CL的onMeasureChild(),最终调用的是VG的measureChildWithMargins。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //上面解析过,不仅让View获得Bh,anchorView,而且把View按照dependency的由少到多形成一个List
        //即没有dependency的View在前面
        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();

            //keyLine相关,不懂什么意思
                ...

            int childWidthMeasureSpec = widthMeasureSpec;
            int childHeightMeasureSpec = heightMeasureSpec;
            //适配insets,不懂什么意思,看到网上说是statusbar
            ....

            final Behavior b = lp.getBehavior();
            //如果Bh为空或者测量失败
            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);
    }

再看一下onLayout(),也是类似的逻辑。先交给Bh处理再由自身处理。

@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();
            //Bh测量
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                //
                onLayoutChild(child, layoutDirection);
            }
        }
    }

可是就这么粗略看一下似乎什么也体会不到,这里还是举一个栗子更直观地感受一下。比如这样一个布局CL中有一个AppBarLayout,一个RV,一个FAB。

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/topCoordinatorLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:background="@color/colorPrimary"
        android:layout_height="320dp"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="@color/colorPrimary"
                android:minHeight="?attr/actionBarSize"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />

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

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:background="@color/skin_bg_color"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="500dp"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <!--这个Behavior的一个作用是把自身放在appbar下面-->
    </android.support.v7.widget.RecyclerView>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        android:src="@drawable/ic_pets_dark"

        app:borderWidth="0dp"
        app:layout_anchor="@id/appBarLayout"
        app:layout_anchorGravity="bottom|right" />

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

每个View都有自己对应的Bh,对应着不同的测量和布局逻辑。

view behavior AppBarLayout AppBarLayout.Behavior RecyclerView AppBarLayout.ScrollingViewBehavior FloatingActionButton AppBarLayout.Behavior

这些Bh的继承图是这样的,可以看到都和一个叫ViewOffsetBehavior的类有关,从名字上看肯定和偏移有关。
图片来源于网络
那么我们就从测量开始,从上述对CL的onMeasure的整体分析,以及mDependencySortedChildren的顺序,我们知道测量肯定从AppBarLayout开始。

//AppBarLayout&Behavior 
    @Override
        public boolean onMeasureChild(CoordinatorLayout parent, AppBarLayout child,
                int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
                int heightUsed) {
            final CoordinatorLayout.LayoutParams lp =
                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
                // If the view is set to wrap on it's height, CoordinatorLayout by default will
                // cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
                // what we actually want, so we measure it ourselves with an unspecified spec to
                // allow the child to be larger than it's parent

              //如果高度是WRAP_CONTENT,传入MeasureSpec.UNSPECIFIED作为Mode进行测量
                parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,
                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed);
                return true;
            }

            // 默认返回false
            return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
                    parentHeightMeasureSpec, heightUsed);
        }

看一下RV的测量,它是使用的ScrollingViewBehavior的父类HeaderScrollingViewBehavior的onMeasureChild方法,可以看到,RV的高度是减去了AppBarLayout的高度的。

 //HeaderScrollingViewBehavior
    @Override
    public boolean onMeasureChild(CoordinatorLayout parent, View child,
            int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
            int heightUsed) {
        final int childLpHeight = child.getLayoutParams().height;
       //如果是MATCH_PARENT或者是WRAP_CONTENT
        if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {

            final List<View> dependencies = parent.getDependencies(child);
            //获取它所依赖的View
            final View header = findFirstDependency(dependencies);
            if (header != null) {
                //和fitsWindow相关
                ....
                int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
                if (availableHeight == 0) {
                    // If the measure spec doesn't specify a size, use the current height
                    availableHeight = parent.getHeight();
                }
               //减去了header的高度
                final int height = availableHeight - header.getMeasuredHeight()
                        + getScrollRange(header);
                final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
                        childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
                                ? View.MeasureSpec.EXACTLY
                                : View.MeasureSpec.AT_MOST);

                // Now measure the scrolling view with the correct height
                //测量
                parent.onMeasureChild(child, parentWidthMeasureSpec,
                        widthUsed, heightMeasureSpec, heightUsed);

                return true;
            }
        }
        return false;
    }

FAB的测量略。

接下来看一下布局的过程。首先还是AppBarLayout的布局,几经辗转最终还是调用到了CL的layoutChild,计算了一下statusbar的高度,并偏移到这个AppBarLayout的布局上。

//AppBarLayout&Behavior .java
        @Override
        public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl,
                int layoutDirection) {
            boolean handled = super.onLayoutChild(parent, abl, layoutDirection);

            //下面一大段和PendingIntentAction有关
            ....

            return handled;
        }
//ViewOffsetBehavior.java
   @Override
    public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
        // First let lay the child out
        layoutChild(parent, child, layoutDirection);
        ...
        return true;
    }
    //ViewOffsetBehavior.java
    protected void layoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
        // Let the parent lay it out by default
        parent.onLayoutChild(child, layoutDirection);
    }
    //CoordinatorLayout.java
    public void onLayoutChild(View child, int layoutDirection) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        ...
        ...
          else {
            layoutChild(child, layoutDirection);
        }
    }
//CoordinatorLayout.java
    private void layoutChild(View child, int layoutDirection) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //适配statusbar的高度

        final Rect out = mTempRect2;
        GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
                child.getMeasuredHeight(), parent, out, layoutDirection);
        child.layout(out.left, out.top, out.right, out.bottom);
    }

再看RV的布局过程,注意最终调用的是ViewOffsetBehavior的onLayoutChild,但是注意看上面我给的Bh的继承图,HeaderScrollingViewBehavior是重写了layoutChild的方法的,所以最终调用的是HeaderScrollingViewBehavior的layoutChild(一开始我没注意,心想那最后AppBarLayout和RV同样的布局不就重叠了~)。最后RV的布局考虑了AppBarLayout的存在。

//ViewOffsetBehavior.java 
  @Override
    public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
        //注意!这里是调用了HeaderScrollingViewBehavior#layoutChild
      //而不是ViewOffsetBehavior#layoutChild
        layoutChild(parent, child, layoutDirection);
        ...
        return true;
    }
    //HeaderScrollingViewBehavior.java
    @Override
    protected void layoutChild(final CoordinatorLayout parent, final View child,
            final int layoutDirection) {
        final List<View> dependencies = parent.getDependencies(child);
        final View header = findFirstDependency(dependencies);

        if (header != null) {
            final CoordinatorLayout.LayoutParams lp =
                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            final Rect available = mTempRect1;
          //计算了RV的可用空间
            available.set(parent.getPaddingLeft() + lp.leftMargin,
                    header.getBottom() + lp.topMargin,
                    parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
                    parent.getHeight() + header.getBottom()
                            - parent.getPaddingBottom() - lp.bottomMargin);

            final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
            //适配statusbar
            ...
              //实际布局区域
            final Rect out = mTempRect2;
            GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
                    child.getMeasuredHeight(), available, out, layoutDirection);
            //计算重叠
            final int overlap = getOverlapPixelsForOffset(header);
            //最终布局
            child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
            mVerticalLayoutGap = out.top - header.getBottom();
        } else {
            // If we don't have a dependency, let super handle it
            super.layoutChild(parent, child, layoutDirection);
            mVerticalLayoutGap = 0;
        }
    }

Behavior与Touch事件

作为一个VG,它把Touch事件的管理都交给了子View的Bh。注意,这里和嵌套滑动没有关系,CL在嵌套滑动中的身份是一个NestedScrollingParent,可以看成是一个listener响应child的调用,而在事件分发流程中,CL作为一个VG,有拦截事件流和处理事件流的主动权。

我们看到在onInterceptTouchEvent()中会调用performIntercept()这个方法

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        MotionEvent cancelEvent = null;

        ...
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
        ...
        return intercepted;
    }

这个方法利用View的Bh进行了拦截或消耗(这个函数可以用于onInterceptTouchEvent或onTouchEvent中)。

 private boolean performIntercept(MotionEvent ev, final int type) {
        ...

        final List<View> topmostChildList = mTempList1;
      //以z-order排序
        getTopSortedChildren(topmostChildList);
        //让最上层的View先被检查
        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();
            //如果 被拦截 且不是DOWN,发送CANCEL事件
            //也就是CL的某个直接子View决定拦截过后,要向其他View发送CANCEL事件
            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);
                    }
                    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) {
                    //如果是onInterceptTouchEvent函数中
                    case TYPE_ON_INTERCEPT:
                        //让该View决定是否拦截
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    //如果是onTouchEvent函数中(此时已经执行到了事件流向上传递的过程,CL依旧把事件的消耗交给某个View)
                    case TYPE_ON_TOUCH:
                        //让该View决定是否消耗
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                //不管是在下发过程拦截了,还是在上传过程中消耗了
                if (intercepted) {
                    //记录下这个CL的直接子View
                    mBehaviorTouchView = child;
                }
            }
            ...
        }

        topmostChildList.clear();

        return intercepted;
    }

接着在onTouchEvent中,能执行到这,说明CL本身(有直接子View)拦截了事件下发或者没有View消耗了事件,事件又回传到了CL。

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        ...
        //如果有拦截的View 或者 ...
        if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {
             //交给这个View去处理
            final LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();
            final Behavior b = lp.getBehavior();
            if (b != null) {
                handled = b.onTouchEvent(this, mBehaviorTouchView, ev);
            }
        }

      //调用super.onTouchEvent也就是View的
        if (mBehaviorTouchView == null) {
            handled |= super.onTouchEvent(ev);
        } else if (cancelSuper) {
            if (cancelEvent == null) {
                final long now = SystemClock.uptimeMillis();
                cancelEvent = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            }
            super.onTouchEvent(cancelEvent);
        }
        ...

        return handled;
    }

我们还是以上一节Behavior的的测量和布局中举的栗子,AppBarLayout和RV对触摸事件会有怎样的动作呢?肯定要看它们对应的Bh:AppBarLayout&Behavior和AppBarLayout&ScrollingViewBehavior。

经过一番搜索,RV的ScrollingViewBehavior以及其父类都没有重写onInterceptTouchEvent,只有最顶层的父类CoordinatorLayout&Behavior的onInterceptTouchEvent返回了false。也就是说RV不会帮助CL拦截触摸事件。

继续看AppBarLayout的AppBarLayout&Behavior,其父类HeaderBehavior重写了onInterceptTouchEvent和onTouchEvent方法.

onInterceptTouchEvent()里面拦截的逻辑很简单啊,就是检查到是拖动就拦截。

@Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        ...
        //如果已经在拖动,拦截
        if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
            return true;
        }

        switch (MotionEventCompat.getActionMasked(ev)) {
            case MotionEvent.ACTION_DOWN: {
                //记录一些信息
                ...
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                ...
                final int y = (int) ev.getY(pointerIndex);
                final int yDiff = Math.abs(y - mLastMotionY);
                //如果大于临界值,拦截
                if (yDiff > mTouchSlop) {
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                }
                break;
            }
            ...
        }

        return mIsBeingDragged;
    }

onTouchEvent()中对ABL做了相应的处理。

 @Override
    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
        switch (MotionEventCompat.getActionMasked(ev)) {
            ...
            case MotionEvent.ACTION_MOVE: {

                final int y = (int) ev.getY(activePointerIndex);
                int dy = mLastMotionY - y;

                if (mIsBeingDragged) {
                    mLastMotionY = y;
                    // 拖动ABL
                    scroll(parent, child, dy, getMaxDragOffset(child), 0);
                }
                break;
            }

            case MotionEvent.ACTION_UP:
                if (mVelocityTracker != null) {
                    ...
                    //fling ABL
                    fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
                }
             ....
        }
        ....

        return true;
    }

看到这里其实我们明白了,当我们在滑动的时候ABL会首先拦截手势进行滚动,当它不拦截的时候或者说达到某些条件的时候(没仔细看源码ABL是何时放弃拦截的以及怎么放弃拦截的,清楚的朋友指点一下!!!),整个CL就不会拦截手势,事件会下发交给RV,进行嵌套滑动或者联动,那都是后话了。

总结

好了,现在我们差不多对CL的Behavior有了一个总体的认识,CL作为一个VG本身并没有做太多事情,他把一切交给Behavior去完成,包括拦截和处理触摸事件、测量和布局、嵌套滑动、形成两个View之间的依赖等。CL的直接子View的Behavior,看起来就像CL的管家们。

Behavior的创建有三种方式:一种在xml中作为一个属性赋值,会在View树实例化的时候初始化;一种在代码中动态指定;一种用注解为类指定一个Behavior,在绘制流程的测量过程中,CL会去实例化直接子View的Behavior。

CL中的两个View的依赖效果可以靠在xml中指定一个anchorView属性或者在Behavior的layoutDependsOn()中去完成,在一切回调onChildViewsChanged()的地方(Scroll和Fling以及preDraw、onChildViewRemoved这些地方)完成两个View的互动,具体来说是通过Bh的onDependentViewRemoved()方法。

CL本身作为一个NestedScrollingParent,它把接收child的回调进行响应的任务交给了直接子View,直接子View如果想进行嵌套滑动,就在Bh的onStartNestedScroll()返回true,这个Bh就会被标记,接着onNestedPreScroll()就会选出感兴趣的直接子View的Bh去执行其onNestedPreScroll()方法….

CL的测量和布局也和Behavior紧紧相关,不仅是先交由直接子View的onMeasure和onLayout经手,而且拥有不同Bh的View在测量和布局上会表现出不同的特点。

CL的Touch事件分发和嵌套滑动是独立的,直接子View的Bh有拦截事件向下分发的权利,也能在CL决定自身消耗事件的时候进行具体的处理。

现在我对CoordinatorLayout有了还算不错的理解,希望在后面的使用中能不断加深对它的理解,也希望通过此文抛砖引玉,多少给大家一些帮助,或者在某一点上解答了大家的疑惑。有什么不足或者见解,希望多多交流!!

说在前面 CoordinatorLayout(下面简称CL)是Material Design中的明星控件了,学习它的源码不仅可以更好掌握这个控件的使用,而且可以更好地理解其他诸如AppbarLayout、FloatingActionButton等

相关阅读排行


用户评论

游客

相关内容推荐

最新文章

×

×

请激活账号

为了能正常使用评论、编辑功能及以后陆续为用户提供的其他产品,请激活账号。

您的注册邮箱: 修改

重新发送激活邮件 进入我的邮箱

如果您没有收到激活邮件,请注意检查垃圾箱。