ViewGroup 默认顺序绘制子 View,如何修改?什么场景需要修改绘制顺序?
一、认顺序
大家好,序绘修改修改我是何绘制承香墨影,许久不见,什场顺序甚是景需想念!
今天我们来聊聊 View 绘制流程的一个小细节,自定义绘制顺序。认顺
View 的序绘修改修改三大流程:测量、布局、何绘制绘制,什场顺序我想大家应该都烂熟于心。景需而在绘制阶段,认顺ViewGroup 不光要绘制自身,序绘修改修改还需循环绘制其一众子 View,何绘制这个绘制策略默认为顺序绘制,什场顺序即 [0 ~ childCount)。景需
这个默认的策略,有办法调整吗?例如修改成 (childCount ~ 0],或是修成某个 View 最后绘制。同时又有什么场景需要我们做这样的修改?
需要注意的是,绘制顺序会影响覆盖顺序,同时也会影响 View 的事件分发,这些都是关联影响的,高防服务器可谓是牵一发而动全身。
今天就来聊聊这个问题。
二、TV App 的 Item 处理
修改 View 的绘制顺序,在日常开发中,基本用不到。众多手机端 App 的 UI 设计,大部分采用扁平化的设计思想,除非是一些很特别的自定义 View,多数情况下,我们无需考虑 View 的默认绘制顺序。
这也很好理解,正常情况下,ViewGroup 中后添加的 View,视觉上就是应该覆盖在之前的 View 之上。
但是有一个场景的设计,很特别,那就是 Android TV App。
在 TV 的云服务器设计上,因为需要遥控器按键控制,为了更丰富的视觉体验,是需要额外处理 View 对焦点状态的变化的。
例如:获取焦点的 ItemView 整个高亮,放大再加个阴影,都是很常见的设计。
那么这就带来一个问题,正常我们使用 RecyclerView 实现的列表效果,当 Item 之间的间距过小时,单个 Item 被放大就会出现遮盖的效果。
例如上图所示,一个很常见的焦点放大高亮的设计,但却被后面的 View 遮盖了。
这样的情况,如何解决呢?
拍脑袋想,既然是间距太小了,那我们就拉大间距就好了。b2b供应网修改一个属性解决一个需求,设计师哭晕在工位上。
不过确实有一些设计效果,间距足够,也就不存在遮盖的现象,例如 Bilibili TV 端的部分页面。
但是我们不能只靠改间距解决问题,多数情况下,设计师留给我们的间距并不多。大部分 TV App 是这样的。
既然逃不掉,那就研究一下如何解决。
三、修改绘制顺序原理
修改绘制顺序,其实很简单,Android 已经为我们留出了扩展点。
我们知道,ViewGroup 通过其成员 mChildren 数组,存储子 View。而在 ViewGroup 绘制子 View 的 dispatchDraw() 方法循环中,并不是直接利用索引从 mChildren 数组中取值的。
@Override protected void dispatchDraw(Canvas canvas) { // ... final ArrayList<View> preorderedList = usingRenderNodeProperties ? null : buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); for (int i = 0; i < childrenCount; i++) { // ... final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); // 并非直接从 mChildren 中获取 final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } // ... }可以看到,child 并非是从 mChildren 中直取,而是通过 getAndVerifyPreorderedView() 获得,它的参数除了 children 外,还有一个 preorderedList 的 ArrayList,及子 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; }在其中,若 preorderedList 不为空,则从其中获取子 View,反之则还是从 children 中获取。
回到前面 dispatchDraw() 中,这里使用的 preorderedList 关键列表,来自 buildOrderedChildList(),在方法中通过 getAndVerifyPreorderedIndex() 获取对应子 View 的索引,此方法需要一个 Boolean 类型的 customOrder,即表示是否需要自定义顺序。
ArrayList<View> buildOrderedChildList() { // ... 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; }buildOrderedChildList() 的逻辑就是按照 Z 轴调整 children 顺序,Z 轴值相同则参考 customOrder 的配置。
通常 ViewGroup 中的子 View,Z 值一致,所以关键参数是 customOrder 开关。
从代码上了解到 customOrder 是通过 isChildrenDrawingOrderEnabled() 方法获取,与之对应的是 setChildrenDrawingOrderEnabled() 可以设置 customOrder 的取值。
也就是说,如果我们要调整顺序,只需 2 步调整:
调用 setChildrenDrawingOrderEnable(true) 开启自定义绘制顺序
重写 getChildDrawingOrder() 修改 View 的取值索引
四、实例
最后,我们写个 Demo,重写 RecycleView 的 getChildDrawingOrder() 方法,来实现获得焦点的 View 最后绘制。
@Override protected int getChildDrawingOrder(int childCount, int i) { View view = getLayoutManager().getFocusedChild(); if (null == view) { return super.getChildDrawingOrder(childCount, i); } int position = indexOfChild(view); if (position < 0) { return super.getChildDrawingOrder(childCount, i); } if (i == childCount - 1) { return position; } if (i == position) { return childCount - 1; } return super.getChildDrawingOrder(childCount, i); }别忘了还需要调用 setChildrenDrawingOrderEnabled(true) 开启自定义绘制顺序。
此时,焦点放大时,就不会被其他 View 遮挡。