CoordinatorLayout Behavior一些笔记

最近因为需要研究一个滑动悬浮效果,偶然间发现了CoordinatorLayout这个很强大的布局,这个控件一般需要配合AppBarLayout、CollapsingToolbarLayout使用来实现一些悬浮和渐变的高级效果,相关的使用文章有很多,这篇就不介绍这些了,写这篇的主要目的是要记录一个问题,给CoordinatorLayout的子View设置Behavior后,Behavior 的layoutDependsOn和onDependentViewChanged方法是CoordinatorLayout何时进行回调来达到协调的目的的。
如果不是太了解CoordinatorLayout可以先看一下这两篇文章:
CoordinatorLayout (这篇介绍了CoordinatorLayout 最基本的一个使用方式)
一步一步深入理解CoordinatorLayout( 这篇介绍了一下部分代码)

下面开始我的分析和记录

创建&&使用自定义的Behavior

  • 当我们想自定义Behavior时需要继承CoordinatorLayout.Behavior<V extends View> ,例如我定义了如下Behavior
public class MyBehavior extends CoordinatorLayout.Behavior<Button> {
    public MyBehavior(Context context, AttributeSet attrs){
        super(context,attrs);
    }

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


    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
        //do something
        return super.onDependentViewChanged(parent, child, dependency);
    }
}

然后当我使用时我可以在xml中定义一个Button类型的ChildView使用这个Behavior

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">
    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="300dp"
        android:layout_marginTop="300dp"
        android:background="#FFCC00"
        android:text="Hello"
        app:layout_behavior="com.humorous.myapplication.coordinatorTest.behavior.MyBehavior"/>
    
    <com.humorous.myapplication.coordinatorTest.widget.TestTextView
        android:id="@+id/textView"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginLeft="300dp"
        android:layout_marginTop="300dp"
        android:background="#3366CC" />

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

这样当我UI中TestTextView控件有相关变化时,就会回调Behavior中的方法,来改变我们的childView

何时回调layoutDependsOn 和onDependentViewChanged?

  • 这里才是我这篇文章想要记录的重点,我很好奇,我的dependency View改变时CoordinatorLayout是怎么通知我的Behavior的,这里就需要贴一些源码了
 private OnPreDrawListener mOnPreDrawListener;
....
@Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        resetTouchBehaviors();
        if (mNeedsPreDrawListener) {
            if (mOnPreDrawListener == null) {
                mOnPreDrawListener = new OnPreDrawListener();
            }
            final ViewTreeObserver vto = getViewTreeObserver(); 
            //在这里将实现了OnPreDrawListener的对象注册到ViewTreeObserver中
            vto.addOnPreDrawListener(mOnPreDrawListener);
        }
        if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
            // We're set to fitSystemWindows but we haven't had any insets yet...
            // We should request a new dispatch of window insets
            ViewCompat.requestApplyInsets(this);
        }
        mIsAttachedToWindow = true;
    }
....
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            onChildViewsChanged(EVENT_PRE_DRAW);
            return true;
        }
    }

上面的代码表明,CoordinatorLayout的一个内部类OnPreDrawListener实现了ViewTreeObserver.OnPreDrawListener,然后注册到了ViewTreeObserver上,OnPreDrawListener是ViewTreeObserver上的一个回调接口内部声明如下:

 /**
     * Interface definition for a callback to be invoked when the view tree is about to be drawn.
     */
    public interface OnPreDrawListener {
        /**
         * Callback method to be invoked when the view tree is about to be drawn. At this point, all
         * views in the tree have been measured and given a frame. Clients can use this to adjust
         * their scroll bounds or even to request a new layout before drawing occurs.
         *
         * @return Return true to proceed with the current drawing pass, or false to cancel.
         *
         * @see android.view.View#onMeasure
         * @see android.view.View#onLayout
         * @see android.view.View#onDraw
         */
        public boolean onPreDraw();
    }

这个接口会在viewTree准备绘制时回调,可以利用这个方法在绘制发生之前去调整滚动的边界或者去请求一个新的layout,所以CoordinatorLayout就是在onPreDraw()方法中回调我们Behavior中的方法,具体的调用方法就是CoordinatorLayout 中onChildViewsChanged,该方法的代码如下(省略部分):

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++) {
             //获取根据z轴排序的子View
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
                // Do not try to update GONE child views in pre draw updates.
                continue;
            }

            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }

            // Get the current draw rect of the view
            getChildRect(child, true, drawRect);

            // Accumulate inset sizes
          
            ....//省略部分代码

            // Dodge inset edges if necessary
           
           ....//省略部分代码
 
            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                //获取i+1位置开始的ChildView
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                 //获取Child的Behavior
                final Behavior b = checkLp.getBehavior();
                //Child的Behavior不为空,并且Behavior的b.layoutDependsOn返回了true
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
                        // If this is from a pre-draw and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }

                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                            // 在这里回调了Behavior的b.onDependentViewChanged方法来通知ChildView的dependency发生了改变
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }

        releaseTempRect(inset);
        releaseTempRect(drawRect);
        releaseTempRect(lastDrawRect);
    }

看到这个方法,我心中的疑惑就解开了,当子View改变时,会引起ViewTree重新绘制,然后因为CoordinatorLayout 设置了OnPreDrawListener会在重新绘制前通知CoordinatorLayout,CoordinatorLayout在通过调用onChildViewsChanged来遍历子View,因为子View已经经过排序,遍历到每一个子View时,会在去遍历当前这个子View之后的View,过程如下:

  // Update any behavior-dependent views for the change
          for (int j = i + 1; j < childCount; j++) {
                //省略具体代码
         }

然后在遍历到每一个i+1位置开始时的子View时,会获取这个子View 的LayoutParams,然后调用getBehavior方法获取Behavior,过程如下:

for (int j = i + 1; j < childCount; j++) {
              //获取i+1位置开始的ChildView
              final View checkChild = mDependencySortedChildren.get(j);
              final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
               //获取Child的Behavior
              final Behavior b = checkLp.getBehavior();
              //省略后面的代码
        }

在拿到这个Behavior后,如果这个Behavior不为空,并且Behavior的layoutDependsOn返回了true,代表j位置的子View依赖于i位置的子View,才会回调Behavior的onDependentViewChanged,过程如下:

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
                //Child的Behavior不为空,并且Behavior的b.layoutDependsOn返回了true
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                   //省略部分代码。。。
                    final boolean handled;
                    switch (type) {
                        case EVENT_VIEW_REMOVED:
                            // EVENT_VIEW_REMOVED means that we need to dispatch
                            // onDependentViewRemoved() instead
                            b.onDependentViewRemoved(this, checkChild, child);
                            handled = true;
                            break;
                        default:
                            // Otherwise we dispatch onDependentViewChanged()
                           // 在这里回调了Behavior的b.onDependentViewChanged方法来通知ChildView的dependency发生了改变
                            handled = b.onDependentViewChanged(this, checkChild, child);
                            break;
                    }

                    if (type == EVENT_NESTED_SCROLL) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }

我自定的Behavior中的实现:

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
        return dependency instanceof TestTextView;
    }
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
        //do something
        return super.onDependentViewChanged(parent, child, dependency);
    }

这样此时的i位置的子View是TestTextView类型时,我的layoutDependsOn会返回true,然后会回调onDependentViewChanged,我可以在拿到Button child, View dependency后,可以做一些变化操作,例如开头推荐阅读的第一篇文章中的效果。
最后,其实onChildViewsChanged并不只是在onPreDraw中才会回调,通过入参我们可以看到,onChildViewsChanged需要传入一个@DispatchChangeEvent final int type的参数,这个type一共有三种类型

    static final int EVENT_PRE_DRAW = 0;
    static final int EVENT_NESTED_SCROLL = 1;
    static final int EVENT_VIEW_REMOVED = 2;

最后我们可以通过查看onChildViewsChanged方法前面的描述来知道它的作用到底是在做什么,这里我只把这个描述贴出来,就不做翻译了,因为我的翻译水平有限,会破坏了原有的意境

/**
     * Dispatch any dependent view changes to the relevant {@link Behavior} instances.
     *
     * Usually run as part of the pre-draw step when at least one child view has a reported
     * dependency on another view. This allows CoordinatorLayout to account for layout
     * changes and animations that occur outside of the normal layout pass.
     *
     * It can also be ran as part of the nested scrolling dispatch to ensure that any offsetting
     * is completed within the correct coordinate window.
     *
     * The offsetting behavior implemented here does not store the computed offset in
     * the LayoutParams; instead it expects that the layout process will always reconstruct
     * the proper positioning.
     *
     * @param type the type of event which has caused this call
     */
    final void onChildViewsChanged(@DispatchChangeEvent final int type)
到这就结束了,在这里简单的做个笔记,对于源码的阅读,虽然相比刚毕业那会清晰了不少,但是还有些不那么流畅,这东西估计只能一点点来靠时间的积累了,希望读过的人可以对整个过程有些了解,也欢迎大家对发现的错误批评指正~
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容