CoordinateLayout的实现原理(二)-子组件间相互依赖原理

引言

上文CoordinateLayout的实现原理描述了CoordinateLayout如何实现Behavoir的绑定和事件转发。CoordinateLayout除了该能力外,还支持子View之间相互依赖,当A控件发生改变时能立即通知到B控件的Behavoir。这只需要我们在B的Behavior中重写layoutDependsOn、onDependentViewChanged和onDependentViewRemoved方法,其中layoutDependsOn方法用于返回A和B是否依赖,onDependentViewChanged和onDependentViewRemoved用于B控件响应A控件的依赖。

用法

public class DependOnBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
    @Override
    public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency) {
        child.scrollTo((int)dependency.getX(), (int)dependency.getY());
        return true;
    }

    @Override
    public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency) {
        return dependency.getTag().equals("dependency");
    }
}
<coordinateLayout>
  <View tag="dependency"/>
  <View layout_behavoir="DependOnBehavior"/>
</coordinateLayout>

上述为简化的代码。

原理

从上述用例来看,在B的Behavoir中只需要在layoutDependsOn中返回依赖为true,之后只要A发生变动都能通知到B的Behavoir。
基于这一点,我们可以猜想到:
1、在CoordinateLayout中一定保存了B与A的依赖关系;
2、在CoordinateLayout中一定设置了视图改变时的Listener,在A View重绘之前的某个时机通知到B的Behavior,调用onDependentViewChanged方法;
这使得我们产生如下疑问:
1、依赖关系在什么时候保存的?
2、如果A依赖B,B依赖C的情况,我们如何保证渲染的先后顺序,也就是必须先让C先改变,之后再B随C改变,之后再A随B改变?
3、循环依赖怎么处理?
4、在什么时机添加View将要重绘的Listener,以及这个Listener在什么时机会被回调?
带着这四个问题,我们分析CoordinateLayout的工作原理。

1、依赖关系在什么时候保存的?

在finishInflate之后?从CoordinateLayout的实现原理一文可知,CoordinateLayout收集childView的Behavoir是在addView是通过调用parent的generateParams来收集的,然而有的时候addView发生在finishInflate之后,也就意味着,我们在CoordinateLayout的finishInflate之后才能获取到Behavior,只有获取到Behavoir,我们才能调用它的layoutDependsOn方法获取依赖关系。
如,我们如果自己调用LayoutInflate来初始化View,然后setContentView到dialog/activity(其实都是window)时,addView肯定在inflate之后的。

 // activity的setContentView
@Override
public void setContentView(View v) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}
// dialog的setContentView
@Override
public void setContentView(int layoutResID) {
   // 省略其他代码
   mLayoutInflater.inflate(layoutResID, mContentParent);
  // 省略其他代码...
}

如果是通过setContentView(resId)的方式,那么finishInflate的回调会在genenerateParams和addView之后。

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // ... 省略其他代码
    final View view = createViewFromTag(parent, name, context, attrs);
    final ViewGroup viewGroup = (ViewGroup) parent;
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    rInflateChildren(parser, view, attrs, true);
    viewGroup.addView(view, params);
    // ... 省略其他代码
    if (finishInflate) {
        parent.onFinishInflate();
    }
}

综上,我们直接在finishInflate之后进行依赖关系的保存是不可行的,那么只能继续从addView之后着手。因此, 我的猜想:我们只需要在addView之后回调到CoordinateLayout,然后保存依赖关系即可,而刚好ViewGroup中可以设置OnHierarchyChangeListener(childViewAdded回调和childViewRemoved回调)。但CoordinateLayout不是这样做的,它是在每次onMeasure之前(这个时机addView是肯定执行完了),清空了原来的依赖关系,然后重新检查一遍依赖关系并保存(这里不太清楚为什么CoordinateLayout为什么要这么做,我们为了性能按理说不应该放到onMeasure中来处理的,我怀疑CoordinateLayout可能考虑到随着控件的动态变化,依赖关系可能更新,所以每次onMeasure之前重新检查一遍依赖关系)。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      prepareChildren();
      ensurePreDrawListener();
  // 省略其他代码
}

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)) {
                    mChildDag.addNode(other);  // 增加一个图节点
                }
                mChildDag.addEdge(other, view);  
            }
        }
    }
    mDependencySortedChildren.addAll(mChildDag.getSortedList());
    Collections.reverse(mDependencySortedChildren);
}

void ensurePreDrawListener() {
// 省略其他代码...
    if (hasDependencies) {
        addPreDrawListener();
    } else {
        removePreDrawListener();
    }
// 省略其他代码
}

void addPreDrawListener() {
  // 省略其他代码...
    final ViewTreeObserver vto = getViewTreeObserver();
    vto.addOnPreDrawListener(mOnPreDrawListener);
  // 省略其他代码...
}

2、如果A依赖B,B依赖C的情况,我们如何保证渲染的先后顺序,也就是必须先让C先改变,之后再B随C改变,之后再A随B改变?

注意,在CoordinateLayout中是不支持循环依赖到,也就是说,这里的依赖关系必须是一个有向无环图。从前文可知,依赖关系是在onMeasure的prepareChildren()中保存的,也就是说,在这里建立的依赖。
这里需要用图的数据结构来保存依赖,并通过拓扑排序,将排序后的顺序反向保存到一个数组中,之后每次都在draw之前循环遍历该数组,并通过layoutDependsOn函数来判断A是否依赖B。
上述代码中的mChildDag即为DirectedAcyclicGraph,是一个图结构,有给getSortedList可以帮助我们获取到拓扑排序后的结果。拓扑排序的方法,一句话来说就是先找到入度为0的节点,入栈,然后依次出栈,出栈的时候继续将入度为0的节点入栈,直到所有的节点遍历完成。

4、在什么时机添加View将要重绘的Listener,以及这个Listener在什么时机会被回调?、

从1中的代码可知,这里还添加了一个PreDrawListener,我们看到这里通过ViewTreeObserver.addonPreDrawListener监听,该监听每次ViewTree onDraw之前都会被回调。其也是在onMeasure时被添加的。

总结

本文主要介绍了CoordinateLayout的layoutDependsOn的用法及其实现原理,首先描述了其用法,后续通过4个问题介绍其原理,描述了其调用时机,以及其依赖的保存方式是通过一个有向无环图的数据结构来爆粗的,并通过拓扑排序来将其保存到数组中。之后每次在onDraw之前,先通过该数组获取到对应依赖的View,之后再进传递到onLayoputChange中。
个人感觉这里有几个可以优化的点,1、如果页面的依赖不是经常变动,那么可以不必每次在onMeasure中重新生成图来计算依赖。2、依赖可以直接保存到Map结构中,不必每次在onDraw之前再次通过双层for来判定依赖。

posted @ 2021-09-25 20:54  、、、路遥  阅读(173)  评论(0编辑  收藏  举报