自定义View事件篇进阶篇(三)-CoordinatorLayout与Behavior

前言

在上篇文章中,咱们介绍了NestedScrolling(嵌套滑动)机制,介绍了子控件与父控件嵌套滑动的处理。如今咱们来了解谷歌大大为咱们提供的另外一个控件的交互布局CoordainatorLayout。CoordainatorLayout对于Android开发老司机来讲确定不会陌生,做为控制内部一个或多个的子控件协同交互的容器,开发者能够经过设置Behavior去控制多个控件的协同交互效果,测量尺寸、布局位置及触摸响应。做为谷歌推出的明星组件,分析CoordainatorLayout的文章已经是数不胜数。而分析整个CoordainatorLayout原理的相关资料在网上不多,所以本文会把重点放在分析其内部原理上。php

经过阅读该文,你能了解以下知识点:java

  • CoordainatorLayout中Behavior中的基础使用
  • CoordainatorLayout中多个控件协同交互的原理
  • CoordainatorLayout中Behavior的实例化过程
  • Behavior实现嵌套滑动的原理与过程
  • Behavior自定义布局的时机与过程
  • Behavior自定义测量的时机与过程

该博客中涉及到的示例,在NestedScrollingDemo项目中都有实现,你们能够按需自取。android

CoordainatorLayout简介

熟悉CoordinatorLayout的小伙伴,确定知道CoordinatorLayout主要实现如下四个功能:git

  • 处理子控件的依赖下的交互
  • 处理子控件的嵌套滑动
  • 处理子控件的测量与布局
  • 处理子控件的事件拦截与响应。

而上述四个功能,都依托于CoordainatorLayout中提供的一个叫作Behavior的“插件”。Behavior内部也提供了相应方法来对应这四个不一样的功能,具体以下所示:github

Behavior方法设置.jpg

在下面的文章中不会介绍Behavior嵌套滑动相关方法的做用,若是须要了解这些方法的做用,建议参看自定义View事件篇进阶篇(二)-自定义NestedScrolling实战文章下的方法介绍。数组

那如今咱们就一块儿来看看,谷歌是怎么围绕Behavior对上述四个功能进行设计的把。数据结构

子控件依赖下的交互设计

对于子控件的依赖交互,谷歌是这样设计的:app

依赖下的交互.jpg

当CoordainatorLayout中子控件(childView1)的位置、大小等发生改变的时候,那么在CoordainatorLayout内部会通知全部依赖childView1的控件,并调用对应声明的Behavior,告知其依赖的childView1发生改变。那么如何判断依赖,接受到通知后如何处理。这些都交由Behavior来处理。ide

子控件的嵌套滑动的设计

对于子控件的嵌套滑动,谷歌是这样设计的:函数

嵌套滑动设计.jpg

CoordinatorLayout实现了NestedScrollingParent2接口。那么当事件(scroll或fling)产生后,内部实现了NestedScrollingChild接口的子控件会将事件分发给CoordinatorLayout,CoordinatorLayout又会将事件传递给全部的Behavior。接着在Behavior中实现子控件的嵌套滑动。那么再结合上文提到的Behavior中嵌套滑动的相关方法,咱们能够获得以下流程:

嵌套滑动总体流程.jpg

观察谷歌的设计,咱们能够发现,相对于NestedScrolling机制(参与角色只有子控件和父控件),CoordainatorLayout中的交互角色更为丰富,在CoordainatorLayout下的子控件能够与多个兄弟控件进行交互

子控件的测量、布局、事件的设计

看了谷歌对子控件的嵌套滑动设计,咱们再来看看子控件的测量、布局、事件的设计:

布局与测量及事件的设计.jpg

由于CoordainatorLayout主要负责的是子控件之间的交互,内部控件的测量与布局,就简单的相似FrameLayout处理方式就行了。在特殊的状况下,如子控件须要处理宽高和布局的时候,那么交由Behavior内部的onMeasureChildonLayoutChild方法来进行处理。同理对于事件的拦截与处理,若是子控件须要拦截并消耗事件,那么交由给Behavior内部的onInterceptTouchEventonTouchEvent方法进行处理。

可能有的小伙伴会想,为何会将这四种功能对于的方法将这些功能都交由Behavior实现。其实缘由很是简单,由于将全部功能都对应在Behavior中,那么对于子控件来讲,这种插件化的方式就很是解耦了,咱们的子控件无需将效果写死在自身中,咱们只须要对应不一样的Behavior,就能够实现不一样的效果了。以下所示:

控件对应多个Behavior.jpg

CoordainatorLayout下的多个子控件的依赖交互

了解了CoordainatorLayout中四种功能的设计后,如今咱们经过一个例子来说解CoordainatorLayout下多个子控件的交互。在讲解具体的例子以前,咱们先回顾一下Behavior中对子控件依赖交互提供的方法。以下所示:

public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {return false; }
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}
复制代码

layoutDependsOn方法介绍:

肯定一个控件(childView1)依赖另一个控件(childView2)的时候,是经过layoutDependsOn这个方法。其中child是依赖对象(childView1),而dependency是被依赖对象(childView2),该方法的返回值是判断是否依赖对应view。若是返回true。那么表示依赖。反之不依赖。通常状况下,在咱们自定义Behavior时,咱们须要重写该方法。当layoutDependsOn方法返回true时,后面的onDependentViewChangedonDependentViewRemoved方法才会调用。

onDependentViewChanged方法介绍:

当一个控件(childView1)所依赖的另外一个控件(childView2)位置、大小发生改变的时候,该方法会调用。其中该方法的返回值,是由childView1来决定的,若是childView1在接受到childView2的改变通知后,childView1的位置或大小发生改变,那么就返回true,反之返回false。

onDependentViewRemoved方法介绍:

当一个控件(childView1)所依赖的另外一个控件(childView2)被删除的时候,该方法会调用。

Demo展现

下面咱们就看一种简单的例子,来说解在使用CoordainatorLayout下各个兄弟控件之间的依赖产生的交互效果。

效果展现.gif

在上述Demo中,咱们建立了一个随手势滑动的DependedView,并设定了另外两个依赖DependedView的TextView的Behavior,BrotherChameleonBehavior(变色小弟)与BrotherFollowBehavior(跟随小弟)。具体代码以下所示:

public class DependedView extends View {

    private float mLastX;
    private float mLastY;
    private final int mDragSlop;//最小的滑动距离


    public DependedView(Context context) {
        this(context, null);
    }

    public DependedView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DependedView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDragSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                int dx = (int) (event.getX() - mLastX);
                int dy = (int) (event.getY() - mLastY);
                if (Math.abs(dx) > mDragSlop || Math.abs(dy) > mDragSlop) {
                    ViewCompat.offsetTopAndBottom(this, dy);
                    ViewCompat.offsetLeftAndRight(this, dx);
                }
                mLastX = event.getX();
                mLastY = event.getY();
                break;

            default:
                break;

        }
        return true;
    }
}
复制代码

DependedView逻辑很是简单,就是重写了onTouchEvent,监听滑动,并设置DependedView的位置。咱们继续查看另外两个TextView的Behavior。

BrotherChameleonBehavior(变色小弟)代码以下所示:

在CoordainatorLayout中要实现子控件的依赖交互,咱们须要继承CoordinatorLayout.Behavior。实现layoutDependsOn、onDependentViewChanged、onDependentViewRemoved这三个方法,由于咱们例子中不设计关于依赖控件的删除,故没有重写onDependentViewRemoved方法。

public class BrotherChameleonBehavior extends CoordinatorLayout.Behavior<View> {

    private ArgbEvaluator mArgbEvaluator = new ArgbEvaluator();

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

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof DependedView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        int color = (int) mArgbEvaluator.evaluate(dependency.getY() / parent.getHeight(), Color.WHITE, Color.BLACK);
        child.setBackgroundColor(color);
        return false;
    }
}
复制代码

BrotherFollowBehavior(跟随小弟)代码以下所示:

public class BrotherFollowBehavior extends CoordinatorLayout.Behavior<View> {

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

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof DependedView;//判断依赖的是不是DependedView
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        //若是DependedView的位置、大小改变,跟随小弟始终在DependedView下面
        child.setY(dependency.getBottom() + 50);
        child.setX(dependency.getX());
        return true;
    }
}
复制代码

比较重要的布局文件怎么能忘了呐,对应的布局以下:

<?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-autoandroid:layout_width=“match_parent” android:layout_height=“match_parent”>


    <com.jennifer.andy.nestedscrollingdemo.view.DependedView android:layout_width=“80dp” android:layout_height=“40dp” android:layout_gravity=“center” android:background=“#f00” android:gravity=“center” android:textColor=“#fff” android:textSize="18sp”/> <TextView android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:text=“跟随兄弟” app:layout_behavior=".ui.cdl.behavior.BrotherFollowBehavior”/>

    <TextView android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:text=“变色兄弟” app:layout_behavior=".ui.cdl.behavior.BrotherChameleonBehavior”/> </android.support.design.widget.CoordinatorLayout> 复制代码

原理讲解

你们确定会很好奇,为何简简单单的设置了两个Behavior,DependedView位置发生改变的时候就能通知依赖的两个TextView呢?这要从DependedView的onTouchEvent方法提及。在onTouchEvent方法中,咱们根据手势修改了DependedView的位置,咱们都知道当子控件位置、大小发生改变的时候,会致使父控件重绘。也就是会调用onDraw方法。而CoordainatorLayout在onAttachedToWindow中使用了ViewTreeObserver,并设置了绘制前监听器OnPreDrawListener。以下所示:

@Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        resetTouchBehaviors(false);
        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver();
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
       //省略部分代码:
    }
复制代码

熟悉ViewTreeObserver的小伙伴必定清楚,该类主要是监测整个View树的变化(这里的变化指View树的状态变化,或者内部的View可见性变化等),咱们继续跟踪OnPreDrawListener,查看CoordainatorLayou在绘制前作了什么。

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

咱们发现其内部调用了onChildViewsChanged(EVENT_PRE_DRAW);方法。咱们继续查看该方法。

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

        //获取内部的全部的子控件
        for (int i = 0; i < childCount; i++) {

            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            //省略部分代码…

            //再次获取内部的全部的子控件
            for (int j = i + 1; j < childCount; j++) {

                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();

                //调用当前子控件的Behavior的layoutDependsOn方法判断是否依赖
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    //省略部分代码….
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // 若是依赖,那么就会走当前子控件Behavior中的onDependentViewChanged方法。
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                }
            }
        }
    //省略部分代码…
    }
复制代码

观察代码,咱们发现程序中使用了一个名为mDependencySortedChildren的集合,经过遍历该集合,咱们能够获取集合中控件的LayoutParam,获得LayoutParam后,咱们能够继续获取相应的Behavior。并调用其layoutDependsOn方法找到所依赖的控件,若是找到了当前控件所依赖的另外一控件,那么就调用Behavior中的onDependentViewChanged方法。

看到这里,多个控件依赖交互的原理已经很是清楚了,在CoordainatorLayout下,控件A发生位置、大小改变时,会致使CoordainatorLayout重绘。而CoordainatorLayout又设置了绘制前的监听。在该监听中,会遍历mDependencySortedChildren集合,找到依赖A控件的其余控件。并通知其余控件A控件发生了改变。当其余控件收到该通知后。就能够作本身想作的效果啦。

关于mDependencySortedChildren中存储的究竟是什么数据尚未介绍,如今咱们就来看看这个集合中是存储了什么东西。查看源码,咱们发现mDependencySortedChildren中的元素是在onMeasure方法中的prepareChildren()中进行添加的,

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        //省略部分代码…
    }
复制代码

咱们继续跟踪prepareChildren()方法。代码以下所示:

private void prepareChildren() {
        mDependencySortedChildren.clear();
        mChildDag.clear();
        //遍历内部全部孩子
        for (int i = 0, count = getChildCount(); i < count; i++) {
            final View view = getChildAt(i);

            final LayoutParams lp = getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);

            mChildDag.addNode(view);

            // 再次迭代获取子类控件,找到依赖控件并添加到"(mchildDag)图”中
            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                if (lp.dependsOn(this, view, other)) {
                    if (!mChildDag.contains(other)) {
                        //将节点添加到图中
                        mChildDag.addNode(other);
                    }
                    // 添加边(依赖的view)
                    mChildDag.addEdge(other, view);
                }
            }
        }

        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        //省略部分代码
    }
复制代码

在prepareChildren方法中,会遍历内部全部的子控件,并将子控件添加到mChildDag集合中,mChildDag的数据结构一种叫图的数据结构。经过这种数据结构,咱们能够快速的找到具备依赖关系控件。当将子控件的依赖关系处理完毕后。方法最后会将mChildDag集合中所有的数据添加到mDependencySortedChildren集合中去,这样咱们的mDependencySortedChildren就有相应数据啦。

Behavior的实例化

如今咱们来说解下一个知识点,在上述文章中,咱们描述了CoordainatorLayout中子控件的依赖交互原理,以及Behavior依赖相关方法的调用时机,咱们并无讲解Behavior是什么时候被实例化的。下面咱们就来看看Behavior是如何被实例化的。

查看oordainatorLayout源码,咱们发如今CoordainatorLayout中自定义了布局参数LayoutParams。而且在LayoutParms类中声明了Behavior成员变量。以下所示:

public static class LayoutParams extends MarginLayoutParams {
        Behavior mBehavior;
 }
复制代码

CoordainatorLayout还重写了generateLayoutParams方法。

@Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }
复制代码

熟悉自定义View的小伙伴必定熟悉generateLayoutParams方法。当咱们自定义ViewGroup时,若是但愿咱们的子控件须要一些特殊的布局参数或一些特殊的属性时,咱们通常会自定义LayoutParams。好比Relativelayout中LayoutParms中包含LEFT_OF(对应xml布局中的toLeftOf),RIGHT_OF(对应xml布局中的toRightOf)属性。当程序解析xml的时,会根据子控件声明的属性,生成对应父控件下的LayoutParam,经过该LayoutParam,咱们就能获取咱们想要的参数啦。而子控件Layoutparam的生成,必然会走到父控件的LayoutParams的构造函数。查看CoordainatorLayout下LayoutParams的构造函数:

LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);

            //省略部分代码….

            //判断是否声明了Behavior
            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_Layout_layout_behavior);
            if (mBehaviorResolved) {
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_Layout_layout_behavior));
            }
            a.recycle();

            if (mBehavior != null) {
                // If we have a Behavior, dispatch that it has been attached
                mBehavior.onAttachedToLayoutParams(this);
            }
        }
复制代码

观察代码,咱们能够发现,子控件的布局参数实例化时,会经过AttributeSet(xml中声明的标签)来判断是否声明了layout_behavior,若是声明了,就调用parseBehavior方法来实例化Behavior对象。具体代码以下所示:

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>) context.getClassLoader()
                        .loadClass(fullName);
                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);
        }
    }
复制代码

parseBehavior方法其实很简单,就是根据相应的Behavior全限定名称,经过反射调用其构造函数(自定义Behavior的时候,必定要写构造函数),并实例化其对象。固然实例化Behavior的方法不止一种,Google还为咱们提供了注解的方法设置Behavior。例如AppBarLayout中的设置:

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {}
复制代码

固然使用注解的方式,其原理也是经过反射调用相应Behavior构造函数,并实例化对象。只是须要经过合适的时间解析注解罢了,由于篇幅的限制,这里再也不讲解注解实例化Behavior的原理与时机了,有兴趣的小伙伴能够自行研究。

Behavior实现嵌套滑动的原理与过程

在上文CoordinatorLayout简介中,咱们简单介绍了CoordinatorLayout嵌套滑动事件的传递过程与Behavior嵌套滑动的相关方法,如今咱们就来了解嵌套滑动从CoordinatorLayout到Behavior的整个传递流程。以下所示:

嵌套滑动流程图.jpg

单从上图,来理解整个传递过程比较困难。咱们须要抽丝剥茧,逐个击破。下面咱们就一步步来分析吧。

CoordainatorLayout的事件传递过程

Behavior的嵌套滑动其实都是围绕CoordainatorLayout的的onInterceptTouchEventonTouchEvent方法展开的。那咱们先从onInterceptTouchEvent方法讲起,具体代码以下所示:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();
        //省略部分代码…
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
        //省略部分代码…
        return intercepted;
    }
复制代码

在CoordainatorLayout的的onInterceptTouchEvent方法中,内部实际上是调用了performIntercept来处理是否拦截事件,咱们继续查看performIntercept方法。具体代码以下所示:

private boolean performIntercept(MotionEvent ev, final int type) {
        boolean intercepted = false;
        boolean newBlock = false;

        MotionEvent cancelEvent = null;

        final int action = ev.getActionMasked();
        //获取内部的控件集合,并按照z轴进行排序
        final List<View> topmostChildList = mTempList1;
        getTopSortedChildren(topmostChildList);

        // Let topmost child views inspect first
        //获取全部子view
        final int childCount = topmostChildList.size();
        for (int i = 0; i < childCount; i++) {
            final View child = topmostChildList.get(i);

            //获取子类的Behavior
            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:
                       //调用behavior的onInterceptTouchEvent,若是拦截就拦截
                        intercepted = b.onInterceptTouchEvent(this, child, ev);
                        break;
                    case TYPE_ON_TOUCH:
                        intercepted = b.onTouchEvent(this, child, ev);
                        break;
                }
                //注意这里,比较重要找到第一个behavior对象,并赋值
                if (intercepted) {
                    mBehaviorTouchView = child;
                }
            }
            //省略部分代码….
        }
        //省略部分代码….
        return intercepted;//是否拦截与CoordinatorLayout中子view的behavior有关
    }
复制代码

整个方法代码的逻辑并非很难,主要分为两个步骤:

  • 获取内部的控件集合(topmostChildList),并按照z轴进行排序
  • 循环遍历topmostChildList,获取控件的Behavior,并调用Behavior的onInterceptTouchEvent方法判断是否拦截事件,若是拦截事件,则事件又会交给CoordinatorLayout的onTouchEvent方法处理。

这里咱们先不考虑Behavior拦截事件,通常状况下,Behavior的onInterceptTouchEvent方法基本都是返回false。特殊状况下Behavior事件拦截处理,你们能够在理解本文章全部的知识点后,结合官方提供的BottomSheetBehaviorSwipeDismissBehavior等进行深刻的研究,这里由于篇幅的限制就再也不深刻的探讨了。

那么假设如今全部的子控件中的Behavior.onInterceptTouchEvent返回为false,那么CoordinatorLayout就不会拦截事件,根据事件传递机制,事件就传递到了子控件中去了。若是咱们的子控件实现是了NestedScrollingChild接口(如RecyclerView或NestedScrollView),而且在onTouchEvent方法调用了相关嵌套滑动API,那么再根据嵌套滑动机制,会调用实现了NestedScrollingParent2接口的父控件的相应方法。又由于CoordinatorLayout实现了NestedScrollingParent2接口。那么就又回到了咱们最开始的介绍的嵌套滑动机制了。

这里的理解很是重要!!!!!很是重要!!!!很是重要!!!若是没有理解,建议多读几遍。

既然最终会调用CoordinatorLayout的嵌套滑动方法。那咱们来介绍CoordinatorLayout下比较有表明性的嵌套滑动方法,那么先来看onStartNestedScroll方法。具体代码以下:

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

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == View.GONE) {
                //若是当前控件隐藏,则不传递
                continue;
            }
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                //判断Behavior是否接受嵌套滑动事件
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                        target, axes, type);
                handled |= accepted;
                //设置当前子控件接受接受嵌套滑动
                lp.setNestedScrollAccepted(type, accepted);
            } else {
                lp.setNestedScrollAccepted(type, false);
            }
        }
        return handled;
    }
复制代码

在该方法中,咱们会发现会获取全部的内部的控件,并调用对应Behavior的onStartNestedScroll方法,须要注意的是,若是当前Behavior接受嵌套滑动事件(accepted = true),那么就会调用lp.setNestedScrollAccepted(type, accepted),这段代码很是重要,会影响Behavior后续的嵌套方法的执行。咱们接着看CoordinatorLayout下的onNestedScrollAccepted方法。代码以下所示:

@Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes, int type) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes, type);
        mNestedScrollingTarget = 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(type)) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScrollAccepted(this, view, child, target,
                        nestedScrollAxes, type);
            }
        }
    }
复制代码

一样在onNestedScrollAccepted方法中,也会调用全部控件的Behavior的onNestedScrollAccepted方法,须要注意的是,在该方法中增长了if (!lp.isNestedScrollAccepted(type))的判断,也就是说只有Behavior的onStartNestedScroll方法返回true的时候,该方法才会执行。接下来继续查看onNestedScroll方法。具体代码以下所示:

@Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                ViewCompat.TYPE_TOUCH);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        final int childCount = getChildCount();
        boolean accepted = false;

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

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

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed, type);
                accepted = true;
            }
        }

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

复制代码

一样的,在onNestedScroll方法中,也会判断当前控件对应Behavior是否接受嵌套滑动事件,若是接受就调用对应方法。在代码的最后一行,咱们会发现又调用了onChildViewsChanged(EVENT_NESTED_SCROLL)。该行代码在CoordinatorLayout下多出嵌套滑动方法中都会调用,咱们先看onNestedPreScroll方法。而后再来介绍onChildViewsChanged(EVENT_NESTED_SCROLL)方法调用下的逻辑处理。onNestedPreScroll代码以下所示:

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

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

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

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

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

                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }

        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

        if (accepted) {
            //这里也调用了onChildViewsChanged方法
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }
复制代码

一样的在该方法中,也是调用子控件的Behavior对应的方法,并最后调用了onChildViewsChanged(EVENT_NESTED_SCROLL)。该方法与其余方法的最大的不一样就是,用int[] mTempIntPair = new int[2]记录了控件在X轴与Y轴的距离,比较并获取内部子控件中最大的消耗距离后,最后将最大的消耗距离,经过int[]consumed数组在传回NestedScrollingChild。

在CoordinatorLayout下的比较重要的嵌套滑动方法基本上讲解完毕了。余下的onNestedPreFlingonNestedFling方法都大同小异,这里就再也不讲解了,如今讲解一下当onChildViewsChanged(EVENT_NESTED_SCROLL)方法调用下的逻辑处理。代码以下所示:

final void onChildViewsChanged(@DispatchChangeEvent final int type) {
        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);

                //获取对应控件的Behavior
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();

                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    //这里是理解难点,须要屡次回味。
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        //检查当前控件的嵌套滑动的标志位,若是为true,表示已经嵌套滑动过了,那么就跳过
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    //这里判断所依赖的对象是否移除或改变
                    switch (type) {
                        case EVENT_VIEW_REMOVED://移除
                            //当类型为EVENT_VIEW_REMOVED时,表示该控件移除,咱们要通知依赖该控件的其余控件,该控件已经被移除了
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            //默认状况下,通知通知依赖该控件的其余控件,该控件发生了改变
                            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)
                        //若是当前是嵌套滑动,那么就须要设置该标志位为true,方便跳过OnPreDraw方法
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
        //省略部分代码
    }
复制代码

整个方法分为一下几个步骤:

  • 获取控件的Behavior,调用其layoutDependsOn方法判断是否依赖,找到依赖该控件的其余控件。
  • 随后调用控件的LayoutParams的getChangedAfterNestedScroll()方法,检查当前控件的嵌套滑动的标志位,若是为true,表示已经嵌套滑动过了,那么就跳过。若是该标志位为false,那程序继续往下走。
  • 若是找到依赖控件其嵌套滑动标志位也为false,那么接下来会调用依赖控件的Behavior的onDependentViewChanged方法,通知其余控件依赖的控件位置、大小发生了改变。
  • 通知完毕后,若是其余的控件位置、大小发生了改变,那么须要在onDependentViewChanged方法中返回为true(handlet=true),若是type==EVENT_NESTED_SCROLL那么须要调用ChangedAfterNestedScroll,设置当前控件已经嵌套滑动的标志位为true

整个流程并非很复杂,可是我向下你们会有一个疑问,就是为何type==EVENT_NESTED_SCROLL时,须要设置控件的嵌套滑动标志位呢?为何当该标志位为true的时候,就须要跳过循环呢?其实这两个问题并不难,咱们看下图:

逻辑理解.jpg

根据上图,咱们来回顾一下整个机制的嵌套滑动过程。

  • 当CoordinatorLayout中子控件的Behvior默认不拦截事件,且内部有NestedScrollingChild控件的时候。最终会调用到某个控件的Behavior的嵌套相关方法,这里以A控件为例。
  • 在A控件部分相关嵌套方法中,会调用onChildViewsChanged(EVENT_NESTED_SCROLL)。在该方法中又会通知其余依赖A控件的其余控件。并调用onDependentViewChanged方法(上图中,蓝色与红色部分)。
  • 由于A控件在执行部分嵌套滑动方法后,会致使父控件重绘,因此又会回到本文最初讲解的onPreDraw方法,在该方法中,又会调用onChildViewsChanged(EVENT_PRE_DRAW)(上图中黄色部分)。

根据当前总体流程,咱们能够推断出,若是不经过设置控件的嵌套滑动标志位的话,那么其余依赖A控件的Behavior就会调用两次onDependentViewChanged,若是说其余控件都在该方法中发生了位置、或大小的改变。那么整个过程就会出现问题!!!!!。因此说咱们须要一个标志位来区分绘制与嵌套滑动。

固然这个嵌套滑动的标志位,是与Behavior的onDependentViewChanged方法的返回值有关,因此在平时的开发中,咱们必定要注意。若是咱们当咱们对目标控件的位置、大小形成了改变以后,咱们必定要将该方法的返回值返回为true

Behavior的布局

还有最后两个知识点了,你们加油啊~~~

咱们都知道CoordinatorLayout中被谷歌称为超级FrameLayout,其中的缘由不只由于其布局方式与测量方式与FrameLayout很是类似之外,最主要的缘由是CoordinatorLayout能够将滑动事件、布局、测量交给子控件中的Behavior。如今咱们就来看看CoordinatorLayout下的布局实现。查看其onLayout方法。

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

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Behavior behavior = lp.getBehavior();
            //获取子控件的Behavior方法,并调用其onLayoutChild方法判断子控件是否须要本身布局
            if (behavior == null || !behavior.onLayoutChild(this, child, layoutDirection)) {
                onLayoutChild(child, layoutDirection);
            }
        }
    }
复制代码

从代码中咱们能够看出,在对子 View 进行遍历的时候,CoordinatorLayout有主动向子控件的Behavior传递布局的要求,若是Behavior调用onLayoutChild了方法自主布局了子控件,则以它的结果为准,不然将调用onLayoutChild方法亲自布局。这里就不对CoordinatorLayout下的onLayoutChild方法进行过多的描述了,你们知道这个方法相似于FrameLayout的布局就好了。

Behavior的布局时机

其实确定会有小伙伴会疑惑,什么样的状况下,咱们须要设置自主布局呢?(也就是behavior.onLayoutChild()方法返回true)。在上文中咱们说过了CoordinatorLayout布局方式是相似于FrameLayout的。在FrameLayout的布局中是只支持Gravity来设置布局的。若是咱们须要自主的摆放控件中的位置,那么咱们就须要重写Behavior的onLayoutChild方法。并设置该方法返回结果为true。

Behavior的测量

最后一个知识点了!!!!!Behavior的测量。依然仍是经过CoordinatorLayout传递过来的。咱们查看CoordinatorLayout的onMeasure方法。代码以下所示:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //省略部分代码….
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            int childWidthMeasureSpec = widthMeasureSpec;
            int childHeightMeasureSpec = heightMeasureSpec;

            final Behavior b = lp.getBehavior();
            //调用Behavior的测量方法。
            if (b == null || !b.onMeasureChild(this, child, childWidthMeasureSpec, keylineWidthUsed,
                    childHeightMeasureSpec, 0)) {
                onMeasureChild(child, childWidthMeasureSpec, keylineWidthUsed,
                        childHeightMeasureSpec, 0);
            }

            widthUsed = Math.max(widthUsed, widthPadding + child.getMeasuredWidth() +
                    lp.leftMargin + lp.rightMargin);

            heightUsed = Math.max(heightUsed, heightPadding + child.getMeasuredHeight() +
                    lp.topMargin + lp.bottomMargin);
            childState = View.combineMeasuredStates(childState, child.getMeasuredState());
        }

        final int width = View.resolveSizeAndState(widthUsed, widthMeasureSpec,
                childState & View.MEASURED_STATE_MASK);
        final int height = View.resolveSizeAndState(heightUsed, heightMeasureSpec,
                childState << View.MEASURED_HEIGHT_STATE_SHIFT);
        setMeasuredDimension(width, height);
    }
复制代码

上面的代码中,我仍是省略了一些不重要的代码。观察上述代码,咱们发现该方法与CoordinatorLayout的布局逻辑很是类似,也是对子控件进行遍历,并调那个用子控件的Behavior的onMeasureChild方法,判断是否自主测量,若是为true,那么则以子控件的测量为准。当子控件测量完毕后。会经过widthUsedheightUsed 这两个变量来保存CoordinatorLayout中子控件最大的尺寸。这两个变量的值,最终将会影响CoordinatorLayout的宽高。

Behavior的测量时机

仍是类似的问题,在什么样的状况下,咱们须要重写BehavioronMeasureChild方法来自主测量控件呢?当你的控件须要从新设置位置的时候你要考虑是否须要重写该方法。什么意思呢?看下图所示:

空白区域.jpg

在上图中咱们定义了两个控件A与B,咱们假设这两个控件处于这三个条件下:

  • A、B控件都在CoordinatorLayout下,且A、B控件位置关系为控件A在B控件的下方。
  • A控件的高度为match_parent或者wrap_content
  • A、B控件的嵌套滑动关系为:B控件先处理嵌套滑动事件,当控件B向上滑动至隐藏后,控件A才能开始滑动。

那么根据上述条件,在滚动的过程当中,咱们会发现一个问题,就是当咱们的控件A逐渐滑动到顶部时,咱们会发现屏幕下方会出现一个空白区域,那缘由是什么呢?其实很简单,当控件A高度为match_parent`或者`wrap_content时,根据View的测量规则,控件A实际的高度就是整个控件剩余的高度(屏幕高度-控件B的高度),因此当控件B滚出屏幕后,那么就会出现一段空白。

那么为了使控件A在滑动过程当中始终填充整个屏幕,咱们须要在CoordinatorLayout测量该控件的高度以前,让控件自主的去测量高度,那么这个时候,Behavior的onMeasureChild方法就派上用场了。咱们能够重写该方法并设定当前控件A的高度为整个屏幕的高度。固然如何经过Behavior的onMeasureChild从新设定控件的高度是咱们后续文章将讲解的知识,你们若是有兴趣的话,能够关注后续文章。

最后

看到这里的小伙伴真的很是值得鼓励。点赞!!!!!关于CoordinatorLayout的整个下的Behavior确实理解起来须要花费很多的时间。我本人从理解到写这篇博客零零散散也花费了两周多的时间。虽说这块知识点比较偏门。可是仍是但愿能帮助到有须要的小伙伴们。能有幸帮助到你们,我也很是开心了。

相关文章
相关标签/搜索