CoordinatorLayout及Behavior实现源码阅读

前言

MD设计风格引入了CoordinatorLayout布局,它能够协调子控件之间的操作,使得子控件相互能够做一些复杂的交互操作,这些交互主要通过Behavior对象来实现,这里就来查看一下Behavior和CoordinatorLayout布局实现源代码。

代码分析

首先查看Behavior的源码,它是CoordinatorLayout的一个内部抽象类,主要的接口可以大致上分成三组:依赖处理、嵌套滑动和事件派发相关,要注意的是依赖处理和嵌套滑动这两种机制是相互独立的,它们之间没有必然的联系。

接口分组 接口
依赖处理 public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency)
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency)
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency)
嵌套滑动 onStartNestedScroll
onStartNestedScroll
….
事件派发 public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev)
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev)

需要记住这三组回调接口,在后面的代码分析中会在不同的处理方法中调用这些回调。写过MD的AppBarLayout布局和RecyclerView视差实现都记得在定义界面的XML中会写上app:layout_behavior这样的属性,这个属性其实是在CoordinatorLayout为子控件生成LayoutParams时解析的,查看这个函数的代码:

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

public static class LayoutParams extends MarginLayoutParams {
    LayoutParams(Context context, AttributeSet attrs) {
        super(context, attrs);

        mBehaviorResolved = a.hasValue(
                R.styleable.CoordinatorLayout_Layout_layout_behavior);
        if (mBehaviorResolved) {
            // 如果Behavior还未解析,那么开始解析
            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);
        }
    }
}

我们知道layout_behavior的字符串其实就是实现Behavior的类名,parseBehavior方法其实就是利用反射获取到Behavior的构造函数,通过构造函数生成Behavior对象,这样CoordinatorLayout.LayoutParams里的Behavior对象就成功的被初始化了。

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

    // 解析Behavior字符串为类名
    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) {
            // 加载Behavior类并且获取它的构造函数放到缓存中,下次不必在使用反射初始化
            final Class<Behavior> clazz = (Class<Behavior>) context.getClassLoader()
                    .loadClass(fullName);
            c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
            c.setAccessible(true);
            constructors.put(fullName, c);
        }
        // 生成Behavior对象
        return c.newInstance(context, attrs);
    } catch (Exception e) {
        throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
    }
}

除了在xml中指定还可以通过CoordinatorLayout.LayoutParams的setBehavior方法用代码设置某个子控件的Behavior对象,还可以通过注解的方式注入,这里就不再赘述了。接着来查看成功初始化布局参数里的Behavior之后它又是如何起作用的,首先看子控件的依赖实现源码。

我们知道View子视图之间的相互依赖则是由用户自己定义,为了能够高效的查找到View之间的相互依赖关系,在onMeasure方法中首先调用了prepareChildren方法,这个方法会遍历所有的子控件(不包含子控件内部的控件),通过调用指定View的CoordinatorLayout.LayoutParams的dependOn方法判断它们之间是否存在依赖关系,如果有就将被依赖的控件加入到有向无环图中,之后再把有向无环图排序后添加到mDependencySortedChildren属性中,这样从一个控件开始之后的一段控件都是它依赖的子控件。

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

        // Now iterate again over the other children, adding any dependencies to the graph
        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)) {
                    // Make sure that the other node is added
                    mChildDag.addNode(other);
                }
                // Now add the dependency to the graph
                mChildDag.addEdge(other, view);
            }
        }
    }

    // Finally add the sorted graph list to our list
    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    // We also need to reverse the result since we want the start of the list to contain
    // Views which have no dependencies, then dependent views after that
    Collections.reverse(mDependencySortedChildren);
}

// LayoutParams
boolean dependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency == mAnchorDirectChild
            || shouldDodge(dependency, ViewCompat.getLayoutDirection(parent))
            || (mBehavior != null && mBehavior.layoutDependsOn(parent, child, dependency));
}

LayoutParams的dependsOn方法就会调用mBehavior.layoutDependsOn方法判断当前遍历的子控件是否是child依赖View控件。接着查看如何监视某个子控件被添加或移除,ViewGroup提供了接口OnHierarchyChangeListener每当有子控件添加删除就会做回调操作。

public interface OnHierarchyChangeListener {
    void onChildViewAdded(View parent, View child);
    void onChildViewRemoved(View parent, View child);
}


public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
    mOnHierarchyChangeListener = listener;
}

在CoordinatorLayout中定义了自己的监听器,并且会将子控件移除操作发出通知,这里对应调onChildViewsChanged(EVENT_VIEW_REMOVED)方法,在该方法内部会遍历所有的依赖它的子控件并且调用onDependentViewRemoved方法。

private class HierarchyChangeListener implements OnHierarchyChangeListener {
    HierarchyChangeListener() {
    }

    @Override
    public void onChildViewAdded(View parent, View child) {
        if (mOnHierarchyChangeListener != null) {
            mOnHierarchyChangeListener.onChildViewAdded(parent, child);
        }
    }

    @Override
    public void onChildViewRemoved(View parent, View child) {
        // 提示子控件发生了删除事件
        onChildViewsChanged(EVENT_VIEW_REMOVED);

        if (mOnHierarchyChangeListener != null) {
            mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
        }
    }
}


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

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

            // 判断是否是依赖的子控件
            if (b != null && b.layoutDependsOn(this, checkChild, child)) {

                final boolean handled;
                switch (type) {
                    case EVENT_VIEW_REMOVED:
                        // 如果是移除事件就调用onDependentViewRemoved回调方法
                        b.onDependentViewRemoved(this, checkChild, child);
                        handled = true;
                        break;
                    default:
                        // 否则调用onDependentViewChanged()
                        handled = b.onDependentViewChanged(this, checkChild, child);
                        break;
                }
            }
        }
    }
}

除了前面的监控控件树是否发生改变还有dispatchDependentViewsChanged方法,它会在CoordinatorLayout内部的子控件发生变化是自动调用,这时会将依赖该控件的所有View都查找处理并且调用onDependentViewChanged方法。

public void dispatchDependentViewsChanged(View view) {
    final List<View> dependents = mChildDag.getIncomingEdges(view);
    if (dependents != null && !dependents.isEmpty()) {
        for (int i = 0; i < dependents.size(); i++) {
            final View child = dependents.get(i);
            LayoutParams lp = (LayoutParams)
                    child.getLayoutParams();
            Behavior b = lp.getBehavior();
            if (b != null) {
                b.onDependentViewChanged(this, child, view);
            }
        }
    }
}

上面的代码逻辑就是CoordinatorLayout和Behavior共同实现的控件依赖实现,接着再继续查看通过CoordinatorLayout实现内部嵌套滑动交互操作。在之前学习嵌套滑动原理时我们提到内部的子View会先查找需要和它做交互的外部滑动父布局,很显然嵌套滑动的双方必须是直系祖先对象,如果希望和其他的旁系祖先或兄弟交互无法实现。CoordinatorLayout布局在这种情况下会作为中间对象,嵌套的子控件和CoordinatorLayout交互,CoordinatorLayout再和子控件实际需要交互的兄弟控件做交互,实现CoordinatorLayout内部的控件的嵌套滑动互动操作。

嵌套滑动需要回调的接口有很多,详细的交互过程在嵌套滑动机制源码阅读已经讨论过,这里不再赘述。现在值查看最简单的onStartNestedScroll方法,这个方法会返回是否需要拦截子控件的滑动。

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
    return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}

@Override
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) {
            // If it's GONE, don't dispatch
            continue;
        }
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final Behavior viewBehavior = lp.getBehavior();
        if (viewBehavior != null) {
            // 检查View是否需要嵌套滑动
            final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
                    target, axes, type);
            handled |= accepted;
            lp.setNestedScrollAccepted(type, accepted);
        } else {
            lp.setNestedScrollAccepted(type, false);
        }
    }
    return handled;
}

在onStartNestedScroll方法中会遍历内部的所有子控件通过Behavior.onStartNestedScroll来判断当前遍历的View是否需要嵌套滑动,这个Behavior内部可以覆盖onStartNestedScroll方法的默认实现,如果需要定义Behavior的控件自己先滑动可以返回true代表包含Behavior的布局需要自己先滑动,其他的嵌套布局代码类似这里不再赘述。

最后的onInterceptTouchEvent和onTouchEvent专门负责事件派发机制的回调,这两个接口在事件派发机制代码阅读也详细讲述过,CoordinatorLayout布局内部会定义mBehaviorTouchView字段,它会在某些特殊情况拦截MotionEvent事件,确认拦截时mBehaviorTouchView不为空那么就会执行它所包含的Behavior回调接口。

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

    final int action = ev.getActionMasked();

    // 如果决定拦截事件,那么就会调用 b.onTouchEvent回调方法
    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;
}

总结

CoordinatorLayout布局通过使用Behavior实现内部子控件的协调交互,Behavior主要包含控件依赖、嵌套滑动和事件派发三种功能的接口。控件依赖会在CoordinatorLayout的onMeasure执行时生成有向无环图确定子控件的依赖关系,通过监听控件树的改变将被依赖View改变或移除事件派发到依赖控件;默认的嵌套滑动只支持存在直系父子祖孙关系的两个控件之间,CoordinatorLayout将自己作为中间对象,需要嵌套交互的子控件通过CoordinatorLayout中转嵌套回调;事件派发功能会在CoordinatorLayout的事件回调方法中调用Behavior接口实现拦截默认事件派发功能。

猜你喜欢

转载自blog.csdn.net/xingzhong128/article/details/81346960