前言
周一的早上,由于项目的开发做的差不多了,正在等待测试结果的我就开始发呆,思考端午节的去处~
“理塘?稻城?还是...解决bug?”
不行不行🙅♂️,可不能让BUG
耽误了我的假期,于是我打开公司的项目管理工具,看看测试有没有反馈问题。果然,出现了一个BUG。
大概就是一个RecycleView
,需要把其中某一项做放大效果,类似焦点放大的效果。
但是现在的APP中显示效果是会被下一个View遮挡住,我简单写了个Demo说明:
正常的效果应该是Item4
的View做放大效果,处在item3
和item5
的上一层。
但是现在的效果是Item4
在Item3
的上面,Item5
又在Item4
的上面,所以放大的Item4
被遮挡住了。
这是什么问题呢?
写个Demo
首先,我们写个Demo
复制下BUG出现的页面:
//初始化RecycleView var adapter = TestAdapter() rv.layoutManager = LinearLayoutManager(this) rv.adapter = adapter //修改数据源 var list = mutableListOf<String>() for (number in 0..10) { list.add("item$number") } adapter.addData(list) //Adapter类 class TestAdapter() : RecyclerView.Adapter<TestAdapter.ViewHolder>(){ var dataList = mutableListOf<String>() class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){ var tvName = itemView.findViewById<TextView>(R.id.item_name) fun bind(str: String,position: Int){ tvName.text=str if (position==4){ //Item4放大两倍 itemView.scaleX=2f itemView.scaleY=2f itemView.setBackgroundColor(Color.RED) }else{ itemView.scaleX=1f itemView.scaleY=1f itemView.setBackgroundColor(Color.GREEN) } } }
思考原因
把放假的事情放到一边,我慢慢理清了头绪:
由于RecycleView
是一个ViewGroup
,所以也会按顺序一个个绘制子View,也就是按照顺序调用childView的draw
方法。
所以在这个案例中,正常的绘制顺序就是:
Item0 -> Item1 ..Item3 -> Item4 -> Item5 ...
所以被放大的Item4自然也就处在Item3的上层,但会被Item5遮挡。
那怎么解决呢?
“如果能修改RecycleView的子View绘制顺序就好了~”
脑中突然浮现出这样的一句话。
对哦,如果能修改子View绘制顺序,让Item4
在Item3
和Item5
之后进行绘制,那么就不会被遮挡了。
但是,真的能修改吗?
再看draw方法
这个问题本质上还是涉及到ViewGroup
的子View
绘制问题,所以我们再次回顾View的draw方法:
//View.java public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; // Step 1, draw the background, if needed drawBackground(canvas); // Step 2, save the canvas' layers canvas.saveUnclippedLayer.. // Step 3, draw the content onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers canvas.drawRect.. // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); }
其中第三步大家都很熟悉,就是绘制View本身的onDraw
方法,而在此之后,就是dispatchDraw
方法,根据注释我们得知,这个方法就是用来绘制子View的。
//View.java protected void dispatchDraw(Canvas canvas) { }
在View中是一个空实现,既然是绘制子View,那么肯定会发生在ViewGroup
中,所以我们大胆的猜测,在ViewGroup
中应该对这个方法进行了重写:
//ViewGroup.java @Override protected void dispatchDraw(Canvas canvas) { //1、是否使用渲染节点 boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); final int childrenCount = mChildrenCount; final View[] children = mChildren; //2、预排序列表 final ArrayList<View> preorderedList = usingRenderNodeProperties ? null : buildOrderedChildList(); //3、是否自定义顺序 final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); //4、遍历子View for (int i = 0; i < childrenCount; i++) { //5、获取当前需要绘制的View序号 final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); //根据序号获取View final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { //6、绘制子view more |= drawChild(canvas, child, drawingTime); } } } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }
代码进行了精简,我们一步步来看:
- 1、这里有一个属性叫做
usingRenderNodeProperties
,我们暂且不用管它是什么,只知道默认为true即可。 - 2、初始化
preorderedList
,这个我们今天也用不到,由于usingRenderNodeProperties
为ture,所以这个列表为null
。 - 3、这个属性就很重要了,
customOrder
——是否自定义顺序,看似和我们的需求对应上了。它的值由两者决定:preorderedList == null
并且isChildrenDrawingOrderEnabled
。前者我们知道为true,所以后者isChildrenDrawingOrderEnabled
就是我们待会需要关注的。 - 4、开始遍历子View,注意这里的i还是正常的顺序,会从0一直遍历到childrenCount-1。
- 5、获取View对应的序号,所以获取子View并没有直接用遍历中的i,而是通过
getAndVerifyPreorderedIndex
方法再次获取子View的Index,然后再获取子View。 - 6、最后获取子View后,就开始调用
drawChild
也就是child.draw
方法进行子View的绘制,这样绘制就传递到子View了。
通过上面的讲解,我们知道了重点就在于获取当前需要绘制View的对应Index方法中,也就是方法getAndVerifyPreorderedIndex
:
//获取子View的序号Index private int getAndVerifyPreorderedIndex(int childrenCount, int i, boolean customOrder) { final int childIndex; if (customOrder) { final int childIndex1 = getChildDrawingOrder(childrenCount, i); childIndex = childIndex1; } else { childIndex = i; } return childIndex; } protected int getChildDrawingOrder(int childCount, int drawingPosition) { return drawingPosition; } //根据子View的序号Index获取子View private static View getAndVerifyPreorderedView(ArrayList<View> preorderedList, View[] children, int childIndex) { final View child; if (preorderedList != null) { child = preorderedList.get(childIndex); } else { child = children[childIndex]; } return child; }
getAndVerifyPreorderedIndex
方法负责返回子View的序号。getAndVerifyPreorderedView
方法按照序号childIndex,从children中取出序号对应的View。
当customOrder
为true的时候,返回的view序号会被设置为getChildDrawingOrder
方法的结果,否则就是按照正常的顺序序号,也就是i作为返回结果。
而getChildDrawingOrder
方法默认情况下也是返回的参数drawingPosition
,也就是正常顺序序号i
。
但是,我们可以通过重写改变getChildDrawingOrder
方法的返回结果,比如说:
- 当传入的正常View顺序是
0
,然后我们重写返回View序号为childCount-1
。 - 当传入的正常View顺序是
childCount-1
,然后我们重写返回View序号为0
。 - 其他情况正常返回
这样就能让原本在第一个绘制的View
和最后一个绘制的View
进行了顺序调换。
当然,getChildDrawingOrder
方法能运行的前提是customOrder
为true,而customOrder
为true的前提是isChildrenDrawingOrderEnabled
方法返回true。
final boolean customOrder = isChildrenDrawingOrderEnabled(); protected boolean isChildrenDrawingOrderEnabled() { return (mGroupFlags & FLAG_USE_CHILD_DRAWING_ORDER) == FLAG_USE_CHILD_DRAWING_ORDER; }
所以,我们就能得出修改ViewGroup子View
绘制顺序的基本方法了,主要有两步:
- 1、重写
isChildrenDrawingOrderEnabled
方法,返回true。(可以直接通过调用setChildrenDrawingOrderEnabled(true)
方法来完成) - 2、重写
getChildDrawingOrder
方法,返回当前顺序下需要进行绘制的View序号。
setChildrenDrawingOrderEnabled(true) override fun getChildDrawingOrder(childCount: Int, drawingPosition: Int): Int { if (drawingPosition == 0) { return childCount - 1 } else if (drawingPosition == childCount - 1) { return 0 } else { return drawingPosition } }
RecycleView 中的优化?
回到我们的需求,根据上述的分析,我们是不是需要自定义一个RecycleView
,然后重写isChildrenDrawingOrderEnabled
和 getChildDrawingOrder
两个方法呢?
并不需要,RecycleView
已经为我们提供了API,那就是setChildDrawingOrderCallback
方法:
public void setChildDrawingOrderCallback(@Nullable ChildDrawingOrderCallback childDrawingOrderCallback) { mChildDrawingOrderCallback = childDrawingOrderCallback; setChildrenDrawingOrderEnabled(mChildDrawingOrderCallback != null); } public interface ChildDrawingOrderCallback { int onGetChildDrawingOrder(int childCount, int i); }
可以看到,如果我们调用setChildDrawingOrderCallback
方法,并且传入一个不为空的ChildDrawingOrderCallback
,那么就会调用setChildrenDrawingOrderEnabled(true)
来完成修改绘制顺序的第一步了,也就是保证customOrder
为true。
那么getChildDrawingOrder
方法是怎么和ChildDrawingOrderCallback
回调方法产生联系的呢?
很明显,RecycleView肯定是重写了getChildDrawingOrder
方法:
@Override protected int getChildDrawingOrder(int childCount, int i) { if (mChildDrawingOrderCallback == null) { return super.getChildDrawingOrder(childCount, i); } else { return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i); } }
所以getChildDrawingOrder
方法的结果就等于回调方法ChildDrawingOrderCallback.onGetChildDrawingOrder
。
至此,修改子View绘制顺序的两步都完成了,通过RecycleView的setChildDrawingOrderCallback
即可完成。
验证时刻
终于,一切可以尘埃落定了,接下来就是见证我们分析结果的时刻,来修改Demo:
rv.setChildDrawingOrderCallback(object : RecyclerView.ChildDrawingOrderCallback { override fun onGetChildDrawingOrder(childCount: Int, i: Int): Int { if (i < 4) { return i } else if (i < childCount - 1) { return i + 1 } else { //最后绘制Item4 return 4 } } })
运行:
结束了?并没有
到此,我们的BUG是解决了,但是,关于绘制顺序的知识点我们可以再做下延伸。
在搜索getAndVerifyPreorderedIndex
方法的过程中,我发现了另外一处也用到了getAndVerifyPreorderedIndex
方法:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { //和绘制顺序几乎一样的代码,获取子View的index,然后获取子View final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; //遍历子View for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); //事件向下传递给子View if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { break; } } } } }
惊不惊喜,意不意外,在负责事件分发的dispatchTouchEvent
方法中,我们找到了和绘制子View几乎一样的代码。
同样的通过getAndVerifyPreorderedIndex
方法获取子View的序号,然后获取序号对应的子View。
有的朋友可能会疑惑,这里的preorderedList
好像不为null了呢?直接赋值的buildTouchDispatchChildList
方法?那我们就进去看看这个方法:
public ArrayList<View> buildTouchDispatchChildList() { return buildOrderedChildList(); } ArrayList<View> buildOrderedChildList() { final int childrenCount = mChildrenCount; if (childrenCount <= 1 || !hasChildWithZ()) return null; final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = 0; i < childrenCount; i++) { // add next child (in child order) to end of list final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View nextChild = mChildren[childIndex]; final float currentZ = nextChild.getZ(); // insert ahead of any Views with greater Z int insertIndex = i; while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) { insertIndex--; } mPreSortedChildren.add(insertIndex, nextChild); } return mPreSortedChildren; }
可以看到,第二句有一个判断:
if (childrenCount <= 1 || !hasChildWithZ()) return null; private boolean hasChildWithZ() { for (int i = 0; i < mChildrenCount; i++) { if (mChildren[i].getZ() != 0) return true; } return false; }
childrenCount <= 1
肯定是不成立的。!hasChildWithZ()
这个一般情况下都是为true的,因为一般View是没有Z轴位置的(需要setZ方法设置z轴坐标)。
所以在事件分发的子View遍历中,preorderedList
还是为null,所以和上述的子View绘制逻辑是一模一样的,还是靠isChildrenDrawingOrderEnabled
方法和getChildDrawingOrder
方法来完成修改事件分发的子View遍历顺序。
总结
- 1、
ViewGroup
可以通过调用setChildrenDrawingOrderEnabled(true)
方法,以及重写getChildDrawingOrder
方法修改子View绘制顺序。 - 2、
RecycleView
中将两者进行了封装,只需要调用setChildDrawingOrderCallback
方法即可完成修改子View绘制顺序的需求。 - 3、事件分发的过程中,遍历子View的顺序和绘制子View的顺序
获取机制
是相同的。
(这里要注意,只是两者的顺序获取机制是相同的,都是通过getChildDrawingOrder
方法获取,但是两者顺序并不是完全相同的。因为事件分发中遍历子View是倒序的,也就是从最后一个View开始遍历。而绘制子View的顺序是正序,也就是从第一个View开始遍历)
- 4、所以在我们修改
子View绘制顺序
的同时,其实也修改了事件分发的子View遍历顺序
。