Android 记一次解决问题的过程:从源码中分析永远是解决问题的最有效方法
index++;
}
}
return views;
}
修改好,运行测试一下,当 view 吸顶时,能正常显示在最上层,不会被下面的 view 覆盖了,好像问题已经完美解决了。可是当我点击界面上的控件时,新的问题出现了,我点击的 view 和响应的 view 不是同一个,事件的传递乱了。因为我们把 view 的绘制顺序改变了,所以我们实际看到的、操作的 view,跟系统判断的可能不是同一个 view 了。显然这种解决方法引发了新的问题,是不可取的。
分析源码
既然通过修改 mChildren 的方法行不通,只能另寻方案。我尝试跟踪 view 的绘制源码,期待能有一些新思路。ViewGroup 绘制子 view 的源码调用路径是:draw()-->dispatchDraw()。ViewGroup 中的 dispatchDraw()方法是绘制子 view 的关键代码,通过阅读源码,我发现了几句关键代码。
@Override
protected void dispatchDraw(Canvas canvas) {
// step 1:获取预定义的排序列表
final ArrayList<View> preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
// step 2:判断是否需要自定义排序
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// ste
p 3:根据绘制顺序获取 view 下标
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
// step 4:根据下标获取子 view
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
// step 5:绘制子 view
more |= drawChild(canvas, child, drawingTime);
}
}
}
第一步:获取预定义的排序列表。如果开启了硬件加速 usingRenderNodeProperties 为 true,preorderedList 为 null。否则执行 buildOrderedChildList()方法,这个方法大部分情况下也直接返回 null,所以 preorderedList 一般都是 null 的。buildOrderedChildList()方法只有在没有设置硬件加速,并且子 view 设置了 Z 轴高度的情况下才不会返回 null。我们知道,Android 4.0 后,默认都是开启硬件加速的,而 5.0 前,是不支持 view 的 Z 轴的,所以只有在 5.0 后关闭硬件加速,并且设置了子 view 的 Z 轴,buildOrderedChildList()方法才不会返回 null,这个方法就是处理这种情况的,而且它对 view 的排序处理跟我们下面分析的逻辑基本一样,所以这个方法我们可以忽略不看。
第二步:判断是否需要自定义排序。既然 preorderedList 为 null,那么是否需要自定义排序的判断就是 isChildrenDrawingOrderEnabled()方法,这个方法默认为 false,只有设置为 true,自定义的排序才生效,这是我们需要关注的第一个方法。
第三步:根据绘制顺序获取 view 下标。直接看代码:
private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) {
final int childIndex;
if (customOrder) {
// 如果自定义排序,根据顺序获取 view 下标
final int childIndex1 = getChildDrawingOrder(childrenCount, i);
if (childIndex1 >= childrenCount) {
throw new IndexOutOfBoundsException("getChildDrawingOrder() "
"returned invalid index " + childIndex1
" (child count is " + childrenCount + ")");
}
childIndex = childIndex1;
} else {
// 不是自定义排序,下标和顺序一致
childIndex = i;
}
return childIndex;
}
在这个方法里,如果不排序,返回的下标和顺序一样,所以默认绘制顺序就是 view 的添加顺序。如果需要排序,通过 getChildDrawingOrder 获取需要绘制的 view 的下标,绘制顺序由这个方法的返回值决定。
protected int getChildDrawingOrder(int childCount, int drawingPosition) {
return drawingPosition;
}
可以看到,这个方法的返回值依然是顺序本身,所以它的默认绘制顺序也 view 的添加顺序。但是这个方法是 protected,也就是说我们可以覆写这个方法,返回我们想要的 index,改变 view 的绘制顺序。这是我们需要关注的第二个方法。
第四步:根据下标,调用 getAndVerifyPreorderedView 或者需要绘制的子 view。
private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children,
int childIndex) {
final View child;
if (preorderedList != null) {
child = preorderedList.get(childIndex);
if (child == null) {
throw new RuntimeException("Invalid preorderedList contained null child at index "
childIndex);
}
} else {
child = children[childIndex];
}
return child;
}
这个方法很简单,就是根据下标或者 view,如果有预定义排序,就从 preorderedList 中获取,否则就从 children 数组获取,children 数组就是保存子 view 的数组,按添加顺序排列。
第五步:drawChild,就是调用 child 的 draw 方法绘制子 view。
最终实现
现在我们知道,想要改变 ViewGroup 的子 view 绘制顺序,只有开启自定义排序,并且覆写 getChildDrawingOrder 方法就可以了。
在自定义 ViewGroup 的构造方法中调用:
// 开启自定义排序
setChildrenDrawingOrderEnabled(true);
预先处理 view 的排序
// 保存预先处理的排序
private final List<View> mViews = new ArrayList<>();
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//忽略其他的代码
// 排序
sortViews();
}
private void sortViews() {
List<View> list = new ArrayList<>();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 添加非吸顶 view
if (!isStickyChild(child)) {
list.add(child);
}
}
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 添加吸顶 view
if (isStickyChild(child)) {
list.add(child);
}
}
mViews.clear();
mViews.addAll(list);
}
这里要说明一下,因为 getChildDrawingOrder 方法是根据绘制的顺序 drawingPosition 返回需要绘制的子 view 下标,所以我们需要提前知道最终绘制的顺序,才能根据 drawingPosition 找到相应的 index,所以需要提前对 view 排序好。而把排序的时机选择在 onLayout,是因为在我的需求里,子 view 的添加、移除、和 setLayoutParams 都有可能改变排序,而这些操作恰好都会重新调用父布局的 onLayout 方法。最后排序的方式是先添加非吸顶 view,后添加吸顶 view,这样保证了吸顶 view 在最后绘制,view 重叠时也就不会被其他 view 覆盖了。
最后覆写 getChildDrawingOrder
@Override
protected int getChildDrawingOrder(int childCount, int drawingPosition) {
if (mViews.size() > drawingPosition) {
// 根据 drawingPosition 找到子 view,返回子 view 在 ViewGroup 中的 index
return indexOfChild(mViews.get(drawingPosition));
}
return super.getChildDrawingOrder(childCount, drawingPosition);
}
至此,我们的功能就实现好了。
写在最后
这篇文章的重点就一个 getChildDrawingOrder 方法,但是如果我只是想告诉大家有这么一个方法,那么完全没有必要写这篇文章。我写这篇文章的主要目的是记录这个问题的解决过程,中间会踩坑,也会有意外收获。网上有朋友吐槽,面试时面试官会问:“你遇到过哪些难题,最后时怎么解决的”。很多人都不知道怎么回答,因为所有已经被解决的问题都不是问题,而没有被解决的问题你是不会提起的。就拿我的这个问题来说,如果我早知道有这么个方法,这还是问题吗?我们往往在解决问题后就忽略了问题的解决过程,甚至是问题本身,决定原来这个问题如此简单。却不知,这个过程对我们才是最有意义和收获的。
最后说一句:从源码中寻找答案,永远是解决问题的最有效方法。
最后为了帮助大家深刻理解 Android 相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的 24 套腾讯、字节跳动、阿里、百度 2019-2020BAT 面试真题解析,我把大厂面试中**[常被问到的技术点](
)**整理成了视频和 PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节。
还有 高级架构技术进阶脑图 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
评论