2behavior原理解析

来源:互联网 发布:c语言逻辑表达式符号 编辑:程序博客网 时间:2024/06/05 19:23

在上篇文章中,我们简单介绍了一下Behavior,今天对他的原理做进一步分析。主要介绍behavior如何自定义,behavior的构造,onPreDraw,fab为何随snackbar变化的相关的知识。

自定义behavior

先看个例子,上篇文章主要是重点分析了下,为什么snackbar出现和消失的时候,fab会做出相应变化,那我们能否修改这种变化呢?
比如我想要snackbar出现的时候,fab往上移动100,snackbar消失的时候fab再往上移动100,能否实现呢?
当然可以,代码也很简单,自定义一个MyBehavior,注意必须加入一个MyBehavior的构造器,带有Context和AttributeSet参数,原因后文会说。

public class MyBehavior extends CoordinatorLayout.Behavior<View> {    //此构造函数必须加入    public MyBehavior(Context context, AttributeSet attrs) {        super(context,attrs);    }    //child就是绑定此behavior的view,dependency是发送变化的view    @Override    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {        return dependency instanceof Snackbar.SnackbarLayout;    }    @Override    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {        //此处child 就是fab,dependency是被依赖的view        if (dependency instanceof Snackbar.SnackbarLayout) {            //SnackbarLayout 变化了,A该如何变化在这里写            child.setTranslationY(child.getTranslationY() - 100);            return true;        }        return false;    }    @Override    public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {        if (dependency instanceof Snackbar.SnackbarLayout) {            //SnackbarLayout 变化了,fab该如何变化在这里写            child.setTranslationY(child.getTranslationY() - 100);        }    }}

然后在xml内配置behavior,其实就是加入了一行代码 app:layout_behavior=”com.fish.a2.MyBehavior”

    <android.support.design.widget.FloatingActionButton        android:id="@+id/fab"        app:layout_behavior="com.fish.a2.MyBehavior"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="bottom|end"        android:layout_margin="@dimen/fab_margin"        android:src="@android:drawable/ic_dialog_email" />

看效果

所以说自定义Behavior也是很简单的事情

原理分析

behavior到底是什么

从上边的代码看来,behavior像是view的一个属性,其实他是view的LayoutParam的一个属性,就像宽高一样。当然不是任何一个view的LayoutParam都有这个属性的,只有LayoutParam为android.support.design.widget.CoordinatorLayout.LayoutParams才有这个属性,说白了,就是只有CoordinatorLayout的子view的LayoutParam可以设置behavior。我们看看CoordinatorLayout.LayoutParams的代码,可以发现Behavior变量的确在里面。

  public static class LayoutParams extends ViewGroup.MarginLayoutParams {        Behavior mBehavior;        boolean mBehaviorResolved = false;        ...        final Rect mLastChildRect = new Rect();        }

再来看看app:layout_behavior=”com.fish.a2.MyBehavior”这行代码是怎么导致LayoutParams内的mBehavior被赋值的
我们知道infate的时候,会根据xml去构造LayoutParams,所以我们看CoordinatorLayout.LayoutParams的构造函数

LayoutParams(Context context, AttributeSet attrs) {    super(context, attrs);    final TypedArray a = context.obtainStyledAttributes(attrs,            R.styleable.CoordinatorLayout_LayoutParams);    ...    mBehaviorResolved = a.hasValue(            R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);    if (mBehaviorResolved) {        mBehavior = parseBehavior(context, attrs, a.getString(                R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));    }    a.recycle();}

当我们构造fab的LayoutParams时,走到L7,查一下是否存在layout_behavior值,如果存在,那就parseBehavior并且赋值给mBehavior。parseBehavior就是把字符串com.fish.a2.MyBehavior变成一个对象,用反射的方法。主要代码如下所示

// 这里是指定的Behavior构造器的参数类型static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {        Context.class,        AttributeSet.class};...static Behavior parseBehavior(Context context, AttributeSet attrs, String 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>) Class.forName(fullName, true,                    context.getClassLoader());            //获取特定参数的构造器                    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);    }}

主要L23,这里是用特定参数的构造器来c.newInstance的,CONSTRUCTOR_PARAMS就是一个Context,一个AttributeSet,为什么我们开头的时候说自定义Behavior必须带一个这种类型的构造器的,现在应该有答案了。
那么现在问题来了,在上一篇文章中,我们根本就没有设置layout_behavior,那这个LayoutParams里面的mBehavior是在哪里复制的呢?
我们再来看看, FloatingActionButton有个注解,CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class),在这里指定了默认的Behavior

@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)public class FloatingActionButton extends VisibilityAwareImageButton 

在CoordinatorLayout的onMeasure的时候会调用prepareChildren,进而调用getResolvedLayoutParams,在getResolvedLayoutParams里会把注解里的默认Behavior赋值给mBehavior,主要代码如下

    LayoutParams getResolvedLayoutParams(View child) {        final LayoutParams result = (LayoutParams) child.getLayoutParams();        //如果xml内写了behavior,此时result.mBehaviorResolved就为true,不会进去        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 {                    result.setBehavior(defaultBehavior.value().newInstance());                } catch (Exception e) {                    Log.e(TAG, "Default behavior class " + defaultBehavior.value().getName() +                            " could not be instantiated. Did you forget a default constructor?", e);                }            }            result.mBehaviorResolved = true;        }        return result;    }

所以到了现在,我们知道设置一个view的behavior有2种方式,xml内指定,或者注解里指定,xml优先级高。xml内指定的话,是在inflate的时候对mBehavior赋值的,在注解里指定的话,是在onMeasure内赋值的,稍有不同。

behavior如何发挥作用

前面说了如何给view配置behavior,那配了behavior又有什么用呢?为何behavior能够监测到另一个view的变化情况,这都是CoordinatorLayout的功劳。

onMeasure

我们再来看看onMeasure的代码,主要看prepareChildren和ensurePreDrawListener

    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        prepareChildren();        ensurePreDrawListener();        。。。

先看prepareChildren

    private void prepareChildren() {         //清空mDependencySortedChildren        mDependencySortedChildren.clear();        for (int i = 0, count = getChildCount(); i < count; i++) {            final View child = getChildAt(i);            final LayoutParams lp = getResolvedLayoutParams(child);            lp.findAnchorView(this, child);            //加入child            mDependencySortedChildren.add(child);        }        // We need to use a selection sort here to make sure that every item is compared        // against each other        //排序        selectionSort(mDependencySortedChildren, mLayoutDependencyComparator);    }

prepareChildren内做了什么,主要是搞出来一个mDependencySortedChildren,根据依赖关系对child进行排序。首先L3把mDependencySortedChildren clear,然后遍历子view,全部加入到mDependencySortedChildren内,最后对mDependencySortedChildren进行排序。注意每次measure都会调用prepareChildren来搞出一个mDependencySortedChildren。
我们在看看排序的代码(用的冒泡),看mLayoutDependencyComparator就行了,看下边代码可以知道,被依赖的view放前面,比如我们fab依赖于snackbar,那么snackbar必然放在fab的前边。这么排序有什么用?其实是提高一点效率,后文会说的。

    final Comparator<View> mLayoutDependencyComparator = new Comparator<View>() {        @Override        public int compare(View lhs, View rhs) {            if (lhs == rhs) {                return 0;            } else if (((LayoutParams) lhs.getLayoutParams()).dependsOn(                    CoordinatorLayout.this, lhs, rhs)) {                return 1;            } else if (((LayoutParams) rhs.getLayoutParams()).dependsOn(                    CoordinatorLayout.this, rhs, lhs)) {                return -1;            } else {                return 0;            }        }    };

再看ensurePreDrawListener

在prepareChildren确定mDependencySortedChildren之后,会执行ensurePreDrawListener,在这里写判断下CoordinatorLayout的子view是否存在依赖关系,如果存在的话就hasDependencies为true,后边会加入PreDrawListener。

    void ensurePreDrawListener() {         //判断是否存在依赖关系        boolean hasDependencies = false;        final int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            final View child = getChildAt(i);            if (hasDependencies(child)) {                hasDependencies = true;                break;            }        }        if (hasDependencies != mNeedsPreDrawListener) {            if (hasDependencies) {                //加入PreDrawListener                addPreDrawListener();            } else {                removePreDrawListener();            }        }    }

PreDrawListener是什么?看下边代码,简单,就是在onPreDraw的时候调用dispatchOnDependentViewChanged。

    void addPreDrawListener() {        if (mIsAttachedToWindow) {            // Add the listener            if (mOnPreDrawListener == null) {                mOnPreDrawListener = new OnPreDrawListener();            }            final ViewTreeObserver vto = getViewTreeObserver();            vto.addOnPreDrawListener(mOnPreDrawListener);        }        // Record that we need the listener regardless of whether or not we're attached.        // We'll add the real listener when we become attached.        mNeedsPreDrawListener = true;    }
    class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {        @Override        public boolean onPreDraw() {            dispatchOnDependentViewChanged(false);            return true;        }    }

onPreDraw这个回调和onGlobalLayout类似的,他们的对象是ViewTreeObserver,而不是某个view。在即将绘制的时候,会调用mTreeObserver.dispatchOnPreDraw(),然后分发到各个OnPreDrawListener,在回调onPreDraw的。简单的说,就是在重绘之前,会调用onPreDraw。我们在onPreDraw里面调用了dispatchOnDependentViewChanged,这个函数是CoordinatorLayout非常重要的函数。Behavior的主要行为都是写在这里面的。我们先总结下ensurePreDrawListener做了什么,判断子view是否有依赖行为,如果有的话注册一个onPreDraw监听

dispatchOnDependentViewChanged

这里传进来的fromNestedScroll为false,遍历mDependencySortedChildren,查一下每个view的rect是否发生了变化,如果发生了变化(假设变化的view为A),就遍历后边的view,判断后边view是否依赖于A(L33),如果依赖就做出相应变化(L36)。看到L33和L36,终于舒了口气,和Behavior里的行为扯上了关系。

    void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {        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();            // 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);                }            }            // Did it change? if not continue            final Rect oldRect = mTempRect1;            final Rect newRect = mTempRect2;            getLastChildRect(child, oldRect);            getChildRect(child, true, newRect);            if (oldRect.equals(newRect)) {                continue;            }            recordLastChildRect(child, newRect);            // 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();                    //这里调用了behavior的layoutDependsOn                if (b != null && b.layoutDependsOn(this, checkChild, child)) {                。。。                //这里调用了behavior的onDependentViewChanged                    final boolean handled = b.onDependentViewChanged(this, checkChild, child);                    ...                }            }        }    }

这里再说几点,怎么知道哪些view发生了变化,代码如下,就是看oldRect和 newRect 是否一致,getChildRect就是获取view的当前rect,而getLastChildRect是获取view的旧的rect,这个比较奇怪,居然知道旧的rect。

            final Rect oldRect = mTempRect1;            final Rect newRect = mTempRect2;            getLastChildRect(child, oldRect);            getChildRect(child, true, newRect);            if (oldRect.equals(newRect)) {                continue;            }            recordLastChildRect(child, newRect);

看看getLastChildRect的代码,原来CoordinatorLayout的LayoutParams里面存储了mLastChildRect。看上边的L8可以知道,会记录newRect到LayoutParams里。

    void getLastChildRect(View child, Rect out) {        final LayoutParams lp = (LayoutParams) child.getLayoutParams();        out.set(lp.getLastChildRect());    }

还有个问题,比如我们知道子view A发生了变化,可能有B依赖于A,C依赖于A,怎么去找B,C呢,看上边L28,只要遍历A后边的代码就可以了,为什么?看看前文的mDependencySortedChildren的排序规则就知道了,B,C绝对是在A的后边。可以省去找前面的view,这就是mDependencySortedChildren排序的作用。

再后边代码就是,先判断下b.layoutDependsOn是否返回true,然后执行b.onDependentViewChanged

                if (b != null && b.layoutDependsOn(this, checkChild, child)) {                。。。                //这里调用了behavior的onDependentViewChanged                    final boolean handled = b.onDependentViewChanged(this, checkChild, child);                    ...                }            }

好了,behavior的原理基本分析完了。有点绕,但不复杂。
我个人认为,这个实现过程还可以优化一下,比如上文第一个for循环,是遍历了所有的子view,实际上只要遍历被依赖的子view 就好了。而第二个for循环,是遍历了child(rect变化的view)之后的所有子view,其实也没这个必要,因为依赖关系是早就定好的,可以建一个数组存储哪些view依赖了child,这样只要遍历这个数组就可以了。
如果是我来写,我会给每个view设计一个依赖者数组,比如Aview的依赖者数组内有B,C,就代表B依赖于A,C依赖于A。 那第一个for循环遍历依赖者数组非空的view即可,而第二个for循环遍历依赖者数组就好。
还有一点,view的rect发生变化肯定在onLayout之后就知道了,如果在onLayout里把发生变化的view记录下来,那么第一个for循环就可以更简单了,也没必要在LayoutParam里面设计一个mLastChildRect了。
以上是我的个人想法,如有不对,欢迎指正,可能代码google认为反正CoordinatorLayout的子view很小,所以没必要搞那么复杂。

onDependentViewRemoved

我们开篇自定布局的时候还写了onDependentViewRemoved,那这个onDependentViewRemoved是在哪里被调用的呢?
ViewGroup内有个mOnHierarchyChangeListener,view结构发生变化时会触发OnHierarchyChangeListener回调。

protected OnHierarchyChangeListener mOnHierarchyChangeListener;    public interface OnHierarchyChangeListener {        /**         * Called when a new child is added to a parent view.         *         * @param parent the view in which a child was added         * @param child the new child view added in the hierarchy         */        void onChildViewAdded(View parent, View child);        /**         * Called when a child is removed from a parent view.         *         * @param parent the view from which the child was removed         * @param child the child removed from the hierarchy         */        void onChildViewRemoved(View parent, View child);    }

再看CoordinatorLayout内自己定义了一个HierarchyChangeListener,在onChildViewRemoved的时候会调用dispatchDependentViewRemoved,这个HierarchyChangeListener在构造函数内set。所以有view被remove调的时候回回调到
dispatchDependentViewRemoved。

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

dispatchDependentViewRemoved的代码也很简单,会根据需要触发onDependentViewRemoved

   void dispatchDependentViewRemoved(View view) {        final int childCount = mDependencySortedChildren.size();        boolean viewSeen = false;        for (int i = 0; i < childCount; i++) {            final View child = mDependencySortedChildren.get(i);            if (child == view) {                // We've seen our view, which means that any Views after this could be dependent                viewSeen = true;                continue;            }            if (viewSeen) {                CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams)                        child.getLayoutParams();                CoordinatorLayout.Behavior b = lp.getBehavior();                if (b != null && lp.dependsOn(this, child, view)) {                    b.onDependentViewRemoved(this, child, view);                }            }        }    }

上述代码都是为了监听某个view被remove而加的,那为什么增加一个view的时候没这么麻烦,删除一个view就这么麻烦呢。因为增加了一个view,那这个view,必然在mDependencySortedChildren内,而删除了一个view,这个view在mDependencySortedChildren就找不到了,所以加了这一堆代码

泛型类Behavior

要知道Behavior其实是个泛型类

    public static abstract class Behavior<V extends View> 

所以自定义Behavior可以这么写,这样更优雅,免去了强转

    public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {        // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is        // because we can use view translation properties which greatly simplifies the code.        private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11;        private ValueAnimatorCompat mFabTranslationYAnimator;        private float mFabTranslationY;        private Rect mTmpRect;        @Override        public boolean layoutDependsOn(CoordinatorLayout parent,                FloatingActionButton child, View dependency) {            // We're dependent on all SnackbarLayouts (if enabled)            return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;        }        @Override        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,                View dependency) {            if (dependency instanceof Snackbar.SnackbarLayout) {                updateFabTranslationForSnackbar(parent, child, dependency);            } else if (dependency instanceof AppBarLayout) {                // If we're depending on an AppBarLayout we will show/hide it automatically                // if the FAB is anchored to the AppBarLayout                updateFabVisibility(parent, (AppBarLayout) dependency, child);            }            return false;        }        @Override        public void onDependentViewRemoved(CoordinatorLayout parent, FloatingActionButton child,                View dependency) {            if (dependency instanceof Snackbar.SnackbarLayout) {                updateFabTranslationForSnackbar(parent, child, dependency);            }        }

总结

1、view的behavior有2种方式,xml内指定,或者注解里指定,xml优先级高。xml内指定的话,是在inflate的时候对mBehavior赋值的,在注解里指定的话,是在onMeasure内赋值的,稍有不同。
2、behavior能够检测到view的尺寸变化以及view被remove
3、CoordinatorLayout内的mDependencySortedChildren里,被依赖的view放前面,比如我们fab依赖于snackbar,那么snackbar必然放在fab的前边。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 5d轻轻泥干了怎么办 手机炉石一直闪退怎么办 鸟之羽任务失败怎么办 巫师3没血没药怎么办 荣威rx5灯光不亮怎么办 点滴打没了回血怎么办 加了低标号的油怎么办 别克车钥匙丢了怎么办 霜子哀伤断了怎么办 侧车窗外有雨水怎么办 昂科威15t变速箱异响怎么办 别克昂科拉一公里9毛怎么办? 雷诺科雷傲车钥匙丢了怎么办 奥迪a6l烧机油了怎么办 卡地亚手镯刮花怎么办 卡地亚戒指花了怎么办 手表摔了不走了怎么办 ck手表表扣很难打开怎么办 小天才泡了水怎么办 小天才手表掉水里了怎么办 小天才电话手表进水了怎么办 小天才手表进水了怎么办 小天才电话手表丢了怎么办 小天才电话手表黑屏怎么办 安全守护注册码忘记了怎么办 儿童安全锁的门打不开怎么办 守护宝老年机打不开了怎么办 小米电话手表坏了怎么办 小米手表带坏了怎么办 小米电话手表屏幕坏了怎么办 雷诺梅甘娜06款系统错乱怎么办 轿车碰了一个坑怎么办 车子卖了没过户怎么办 9岁儿童肠胃痉挛怎么办 鹿角胶水放多了怎么办 打胰岛素血糖还是高怎么办 血糖高打胰岛素降不下去怎么办 儿童低烧37度1怎么办 小孩发烧一会冷一会热怎么办 月子里得的风湿怎么办 腰窝中间凸起肉怎么办