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来判定依赖。