写点什么

Android 记一次解决问题的过程:从源码中分析永远是解决问题的最有效方法

用户头像
Android架构
关注
发布于: 2021 年 11 月 05 日

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


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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(实际上比预期多花了不少精力),包知识脉络 + 诸多细节。


还有 高级架构技术进阶脑图 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android 记一次解决问题的过程:从源码中分析永远是解决问题的最有效方法