深入解析Android Design包——Behavior

上一篇深入解析AndroidDesign包——NestedScroll 已经说过了,在AndroidDesign包中主要有两个核心概念:一是NestedScroll,另一个就是Behavior。
相比于NestedScroll这个概念来说,Behavior分析起来会难很多,因为它几乎遍布了AndroidDesign包的每一个控件,种类繁多;另外Behavior提供了二十多个空方法给使用者来重写,主要分为四类:
1.与Touch事件相关的方法
2.与NestedScroll相关的方法
3.与控件依赖相关的方法(依赖这个概念可能接触的不多,就是如果A依赖B,那么当B变化时会通知A跟着变化)
4.其他方法,如测量和布局等
由此可见,Behavior的使用是非常灵活的,所以功能也是非常的强大。但是,对于越灵活的东西,就越难将它讲清除。它有一百种用法,总不能我就举出一百个例子来进行说明,因此本文只能起到一个抛砖引玉的作用,要真正融会贯通还得靠各位自己去揣摩。

从CoordinatorLayout入手

好好的干嘛扯到CoordinatorLayout呢?
如果你这么问那你就外行了,因为如果没有CoordinatorLayout,光有Behavior是啥用都没有滴。
CoordinatorLayout就是一个容器,主要功能就是为它里面的控件传递命令,更准确的说就是使用Behavior来让子控件们相互调用。
CoordinatorLayout有自己的LayoutParams类

    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        /**
         * A {@link Behavior} that the child view should obey.
         */
        Behavior mBehavior;

它的布局参数类定义的第一个属性就是Behavior,而且还有layout_behavior属性供布局文件使用,可在布局文件中为CoordinatorLayout内部的控件设置behavior对象。
另外,所有的Behavior的祖宗都是CoordinatorLayout.Behavior,这是一个静态-内部-虚拟类,头衔有点长~ 我们抓住这个静态内部类就算是抓到Behavior的精髓了。
除了以上两点,最重要的一层关系是:所有Behavior的方法都是在CoordinatorLayout中调用的,比如来了个NestedScroll事件,那么CoordinatorLayout会调用自己的onNestedScroll()方法,然后在方法内部,就会调用childView的behavior对应的onNestedScroll()方法了。
具体过程,我们来详细分析。

如何处理Touch相关事件

找到CoordinatorLayout中的Behavior类,可以发现该类中定义了如下两个方法:

        public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }

        public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
            return false;
        }

很显然,这是拦截触摸事件和处理触摸事件的方法。
我们看看这两个方法是如何被CoordinatorLayout调用的。

先看onInterceptTouchEvent

根据View的事件体系可知,对事件是否拦截的处理在onInterceptTouchEvent()方法中,于是找到CoordinatorLayout的这个方法:

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

        final int action = MotionEventCompat.getActionMasked(ev);

        // Make sure we reset in case we had missed a previous important event.
        if (action == MotionEvent.ACTION_DOWN) {
            resetTouchBehaviors();
        }
        //这里才是处理事件拦截的代码
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);

        if (cancelEvent != null) {
            cancelEvent.recycle();
        }

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            resetTouchBehaviors();
        }

        return intercepted;
    }

从上面代码可知,真正的处理逻辑在performIntercept()方法中,注意它的第二个参数TYPE_ON_INTERCEPT。然后再看performIntercept方法:

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

        ...
        //遍历所有显示出来了的childView
        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) {
                // 如果事件已经被拦截,那么向其他childView发送cancelEvent
                //...省略代码...
                continue;
            }

            if (!intercepted && b != null) {
                switch (type) {
                    //如果事件未拦截,且childView设置了behavior,则进行拦截判断
                    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;
    }

对代码进行简化之后,逻辑就很明显了。先从childView中取出LayoutParams对象,然后从LayoutParams对象中取出Behavior对象,如果performIntercept()方法第二个参数传进来的是TYPE_ON_INTERCEPT,则调用behavior.onInterceptTouchEvent()方法判断是否拦截事件。换句话说就是,是否拦截事件跟CoordinatorLayout本身没有一毛钱关系。

再看onTouchEvent

直接看CoordinatorLayout中的onTouchEvent()方法源码:

public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        boolean cancelSuper = false;
        MotionEvent cancelEvent = null;

        final int action = MotionEventCompat.getActionMasked(ev);
        
        //先判断mBehaviorTouchView是否为null,如果不为null,则不会执行后面的performIntercept()
        //如果等于null,则调用performIntercept方法,该方法如果返回true会对mBehaviorTouchView赋值
        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);
            }
        }

        // 经过上面的两重判断之后,如果mBehavior还是null,则说明childView不消费touch事件
        // 那么该touch事件交给CoordinatorLayout的parent去处理
        if (mBehaviorTouchView == null) {
            handled |= super.onTouchEvent(ev);
        } 
        ...
        return handled;
    }

上面的代码加了注释之后,应该不需要多说什么了。
还是那句话,CoordinatorLayout本身也不会消费touch事件。

如何处理NestedScroll相关事件

先看Behavior中跟NestedScroll相关的方法

        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                V child, View directTargetChild, View target, int nestedScrollAxes) {
            return false;
        }

        public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
                View directTargetChild, View target, int nestedScrollAxes) {
            // Do nothing
        }

        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
            // Do nothing
        }

        public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            // Do nothing
        }

        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dx, int dy, int[] consumed) {
            // Do nothing
        }

        public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
                float velocityX, float velocityY, boolean consumed) {
            return false;
        }

        public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
                float velocityX, float velocityY) {
            return false;
        }

大家不要被这么多方法给吓到了,如果你有阅读这篇文章深入解析Android Design包——NestedScroll 就能够发现,这些方法都是NestedScrollingParent接口中定义的方法。并且CoordinatorLayout本身是实现了NestedScrollingParent接口的,那么CoordinatorLayout会如何调用Behavior的这些方法呢? 肯定是一一对应的来调用。
我想Google这么设计的目的应该是为了解耦,只要给控件提供一个Behavior就可以拥有NestedScrollingParent的功能,这样一来控件本身就与NestedScrollingParent完全无关了。
由于方法比较多,这里就不一一展示调用过程了,挑onNestedScroll方法来说一下吧。
先看CoordinatorLayout中的onNestedScroll方法:

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

        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }
            //获取childView的behavior,并调用behavior的onNestedScroll方法
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
                accepted = true;
            }
        }

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

代码逻辑非常清晰,就是直接把nestedScroll事件通过behavior传递给childView去处理。
但是,我们注意到最后一段代码,调用了一个onChildViewsChanged()方法。
这个方法具体逻辑我们在下一小结分析,它主要是处理那些依赖控件的。之所以在此处加一句,是为了那些跟滑动控件存在依赖关系的其他控件,也可以做出响应。

如何处理依赖相关事件

接下来,我们来看看Behavior中依赖相关的方法

    //判断child和dependency是否存在依赖关系
    public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }
    
    //dependency发生改变时,回调此方法
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

    //dependency被移除时,回调此方法
    public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
        }

要完成上面三个方法的使命,需要满足两点:
1.需要对CoordinatorLayout所有childView进行两两判断,看它们是否存在依赖关系。
2.当一个childView发生布局改变时,CoordinatorLayout需要回调通知与其有依赖关系的其他childView。

判断依赖关系

一个View在Android系统中的显示都是:onMeasure, onLayout, onDraw
所以先看onMeasure:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        prepareChildren();
        ...
    }

没想到第一句就看到重点了,prepareChildren就是为了整理CoordinatorLayout内部的childView,自然也会将childView之间的依赖关系确定好,来看代码:

private void prepareChildren() {
        mDependencySortedChildren.clear(); //List<View> 
        mChildDag.clear(); //图结构 --- 无回路有向图
        //遍历所有childView
        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);
            //再次遍历,需要双重遍历才能将childView两两判断dependency
            //将判断的结果保存在有向图mChildDag中
            for (int j = 0; j < count; j++) {
                if (j == i) {
                    continue;
                }
                final View other = getChildAt(j);
                final LayoutParams otherLp = getResolvedLayoutParams(other);
                //这个dependsOn方法就是判断依赖关系的,内部会调用Behavior.layoutDependsOn()方法
                if (otherLp.dependsOn(this, other, view)) { 
                    if (!mChildDag.contains(other)) {
                        mChildDag.addNode(other);
                    }
                    mChildDag.addEdge(view, other);
                }
            }
        }
        //将图中的数据排序,并保存在List中
        mDependencySortedChildren.addAll(mChildDag.getSortedList());
        //将List倒序设置,让被依赖的childView排在前面,依赖于它的排在后面
        Collections.reverse(mDependencySortedChildren);
    }

这里涉及到一种比较复杂的数据结构——无回路有向图,篇幅有限就不在这里多说,我们只要知道会调用layoutDependsOn方法来判断依赖关系,然后将数据最后保存在mDependencySortedChildren这个List中。
这个mDependencySortedChildren列表中保存的都是childView,不过是按照特定的顺序进行了排序:
如果childView被其他view依赖的次数最多,则排在最前面,以此类推。
至于依赖关系并没有保存,到时候要用到时,再次调用layoutDependsOn方法来判断,写到这里我好像明白了为什么要将依赖次数多的放列表前面了。

childView发生布局改变

OK,依赖关系确定了,那就看看当childView发生改变时,如何让依赖的view跟着改变。
其实在NestedScroll相关方法中,最后都会调用一句代码

    if (accepted) {
        onChildViewsChanged(EVENT_NESTED_SCROLL);
    }

上面也有提到,这个方法就是处理依赖控件的变化的。在分析它之前,还有必要看看其他地方有没有调用此方法。
然后,就看到了在onAttachToWindow()方法中,为CoordinatorLayout设置了OnPreDrawListener 回调,也就是说在执行onDraw之前,回执行onPreDraw方法中的代码。
我们先来看OnPreDrawListener:

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

onPreDraw()的代码很简单,就是执行onChildViewsChanged()方法,也就是说每次onDraw都会调用这个方法来处理依赖控件。

接下来重点看onChildViewsChanged()方法了。

final void onChildViewsChanged(@DispatchChangeEvent final int type) {

        final int childCount = mDependencySortedChildren.size();
    
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // ... 省略了对anchor和inset相关的代码 ...       
            //可以说,上面的代码都是为了提升效率,下面的才是真正的处理逻辑
            
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                //获取对应的Behavior对象
                final Behavior b = checkLp.getBehavior();

                //判断依赖关系
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        //当NestedScroll 和 Draw都会触发这个方法,
                        //这里二者只能有一个继续往下执行
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // 调用Behavior对应的方法,通知依赖这个child的其他view
                            //这里的child的被依赖,checkChild是依赖于它
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // 除了删除事件,其他的都调用下面的方法
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        //这里跟上面的checkLp.getChangedAfterNestedScroll()对应
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
        ...
    }

同样的,为了突出处理逻辑,把一些代码省略掉了。
代码不长,希望大家根据我的注释读一下代码,自然就明白了。
其实,onChildViewsChanged这个方法也只是在调用Behavior的相关方法而已,也就是说如果childA依赖于childB,那么当childB发生布局变化时,childB的Behavior就会把这个变化同时作用到childA身上。

结语

说到这里,一句话总结CoordinatorLayout本身啥事儿也不干,全让底下的childView去干了。
而Behavior之所以强大,是因为在我们不修改View的情况下,可以对View的行为进行修改和控制。

本来是要举例说明一下Behavior的具体用法的,但是没想到一写一写已经这么长了,太长的文章不利于阅读,也不利于学习吸收,就只能把举例说明移到下一篇去写了。
这里大概说一下会分析一个什么样的示例吧。
布局层级大概就是下面这个样子

<CoordinatorLayout> 

    <AppBarLayout>
        <CollapsingToolbarLayout />
    </AppBarLayout>
    
    <NestedScrollView />
    
</CoordinatorLayout>

这个例子涉及到四个控件,之所以选它来说明Behavior的用法是因为,这个例子中Android都为我们提供了相应的Behavior,分析起来也更有权威性。
如果大家感兴趣,就敬请期待吧~

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

推荐阅读更多精彩内容