CoordinatorLayout 布局中的 Behavior 对象的由来简介

CoordinatorLayout 是一种新的布局方式,算是 FrameLayout 的加强版,它的直接子控件的属性 LayoutParams 有个 Behavior 属性。

第一种方法,我们可以在xml布局中来定义 Behavior 的路径,一般是全路径, 一般情况下我们需要在xml布局中声明这个属性来获取它,看看代码

    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();
    }

xml布局转换为view时,会调用 LayoutParams 的构造方法,如果设置了 app:layout_behavior 属性,则会执行 parseBehavior() 方法

    static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] { Context.class,AttributeSet.class };

    static final ThreadLocal<Map<String, Constructor<Behavior>>> sConstructors = new ThreadLocal<>();

    static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
        if (name.startsWith(".")) {
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            fullName = name;
        } else {
            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>) 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);
        }
    }

首先对传进来的值 name 做了一系列的判断,如果name是以 . 开头,则在它前面拼上包名;如果name中包含 . 并且 . 的前后都有字符串,则name就是全名称了,直接使用;除了前面两种外,如果 WIDGET_PACKAGE_NAME 值获取了CoordinatorLayout 的包名,即 android.support.design.widget ,则在name的前面拼上包名和 . ,否则就直接使用name。 sConstructors 是个 ThreadLocal 类型包裹的,ThreadLocal 是为了解决并发而存在的,用它修饰说明同线程使用 constructors 集合,看看下面 Constructor<Behavior> c = constructors.get(fullName) 这行代码,这个明显是为了复用,以 fullName 为key值,如果获取的对象为空,则创建并且存储,我们看看它用的是反射来创建一个Class,然后通过 getConstructor() 方法来获取构造方法,传入了一个 CONSTRUCTOR_PARAMS 参数,它是两个元素的数组,因此相对应的 Behavior 创建时需要有包含两个参数的构造方法,最后调用 c.newInstance(context, attrs) 方法传入 CONSTRUCTOR_PARAMS 数组中对应类型的参数,创建对象。这里需要注意的一点就是我们自定义 Behavior 时,一些要写两个参数的构造方法,否则使用 xml 布局中定义路径时,就会因为找不到对应的构造方法而创建失败。 例如系统中的 ViewOffsetBehavior 类

class ViewOffsetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {

    public ViewOffsetBehavior() {}

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

我们可以仿照它来写构造方法。


第二种是通过注解来生成对应的 Behavior,典型的例子如 AppBarLayout 类,看看它的 写法

@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
    ...
    public static class Behavior extends HeaderBehavior<AppBarLayout> {
         ...
    }
}

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {
    ...

    @Retention(RetentionPolicy.RUNTIME)
    public @interface DefaultBehavior {
        Class<? extends Behavior> value();
    }
}

这里用到的是 CoordinatorLayout 内部类,DefaultBehavior 是个自定义注解,AppBarLayout 的注释括号 AppBarLayout.Behavior.class 就是 DefaultBehavior 中的 value() 对应的值,使用的 Behavior 就是 AppBarLayout 中自定义的 Behavior 对象。它的原理是什么呢,在哪个地方把注解中的值转换为对象呢?我们看看 CoordinatorLayout 的 onMeasure() 测量方法,第一行就是 prepareChildren() 方法

    private void prepareChildren() {
        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);

            mDependencySortedChildren.add(child);
        }
        selectionSort(mDependencySortedChildren, mLayoutDependencyComparator);
    }

方法中会遍历每个直属子控件,调用 getResolvedLayoutParams(child) 方法,看看它里面做了什么操作

    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 {
                    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;
    }

方法中,if判断中用到了 LayoutParams 的 mBehaviorResolved 属性,从方法一种可以看到,如果xml中有配置 Behavior 的路径,mBehaviorResolved 值为true,否则为false,这个if中判断的意思是,如果xml中没有配置路径,则进入方法中,执行下面的逻辑。先找到 child ,以 AppBarLayout 为例,它必须是 CoordinatorLayout 的直接子控件,获取到 AppBarLayout 的 Class,childClass.getAnnotation(DefaultBehavior.class) 这行代码的意思是获取到  childClass 文件对应的类中头部注解的 DefaultBehavior 对象,while循环的意思是如果本身没找到注解,则找它的父类,一直到找到为止,找到 defaultBehavior 后,defaultBehavior 不为空的情况,defaultBehavior.value() 可以获取到 AppBarLayout 类注解中括号中 (AppBarLayout.Behavior.class)的值,即 AppBarLayout.Behavior.class, defaultBehavior.value().newInstance() 意思是通过反射,创建对象,然后通过 LayoutParams 中的 setBehavior() 方法赋值,同时修改 mBehaviorResolved 属性。这里反射用的是无参构造,和xml中的反射不一样,要注意。


第三种方法,其实就是第二种的变形,我们在代码中获取到view的 CoordinatorLayout.LayoutParams 属性,然后通过 new 创建一个自定义的 Behavior 对象,通过 setBehavior() 来完成 Behavior 的赋值。这三种方法都涉及到了构造方法,所以我们自定义的时候,尽量把无参构造和双参数构造方法都写上去,防止出错。


这里介绍了 Behavior 的获取方法,那么什么是 Behavior 呢?看代码,它是一个辅助类,使用了泛型,要求里面的一些方法的形参必须是view类型或是它的子类

public static abstract class Behavior<V extends View> {

    public Behavior() {
    }

    public Behavior(Context context, AttributeSet attrs) {
    }

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

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

以 onInterceptTouchEvent() 和 onTouchEvent() 两个方法为例,看看它是怎么生效的。以 CoordinatorLayout 为例,看看它的 onInterceptTouchEvent() 和 onTouchEvent() 方法

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ...
        final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
        ...
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
       ...
        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);
            }
        }
        ...
        return handled;
    }

两个方法中都调用了 performIntercept() 方法,onInterceptTouchEvent() 传入的参数是 TYPE_ON_INTERCEPT;onTouchEvent() 传入的参数是 TYPE_ON_TOUCH,并且也调用了 Behavior 的 onTouchEvent() 方法,我们着重看看 performIntercept() 方法

    private boolean performIntercept(MotionEvent ev, final int type) {
        ...
        for (int i = 0; i < childCount; i++) {
            ...
            final Behavior b = lp.getBehavior();
            if (!intercepted && b != null) {
                switch (type) {
                    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;
                }
            }
            ...
        }
         ...
        return intercepted;
    }

在这个方法中,经过一系列逻辑判断可以发现,根据传入的参数,最终还是调用了 Behavior 的两个触摸方法,TYPE_ON_INTERCEPT 对应 onInterceptTouchEvent() 方法,TYPE_ON_TOUCH 对应 onTouchEvent() 方法,看到这,就有点明悟了,Behavior 是个委托桥梁,把view中的一些逻辑给独立出来,可以解耦,使得控件更灵活。


 

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/98774558