绘制流程小细节,如何修改 View绘制的顺序?

1、概述

今天我们来聊聊 View 绘制流程的一个小细节,自定义绘制顺序。

View 的三大流程:测量、布局、绘制,我想大家应该都烂熟于心。而在绘制阶段,ViewGroup 不光要绘制自身,还需循环绘制其一众子 View,这个绘制策略默认为顺序绘制,即 [0 ~ childCount)。

这个默认的策略,有办法调整吗?

例如修改成 (childCount ~ 0],或是修成某个 View 最后绘制。同时又有什么场景需要我们做这样的修改?

需要注意的是,绘制顺序会影响覆盖顺序,同时也会影响 View 的事件分发,这些都是关联影响的,可谓是牵一发而动全身。

今天就来聊聊这个问题。

2、TV App 的 Item 处理

修改 View 的绘制顺序,在日常开发中,基本用不到。众多手机端 App 的 UI 设计,大部分采用扁平化的设计思想,除非是一些很特别的自定义 View,多数情况下,我们无需考虑 View 的默认绘制顺序。

这也很好理解,正常情况下,ViewGroup 中后添加的 View,视觉上就是应该覆盖在之前的 View 之上。

但是有一个场景的设计,很特别,那就是 Android TV App。

在 TV 的设计上,因为需要遥控器按键控制,为了更丰富的视觉体验,是需要额外处理 View 对焦点状态的变化的。

例如:获取焦点的 ItemView 整个高亮,放大再加个阴影,都是很常见的设计。

那么这就带来一个问题,正常我们使用 RecyclerView 实现的列表效果,当 Item 之间的间距过小时,单个 Item 被放大就会出现遮盖的效果。



例如上图所示,一个很常见的焦点放大高亮的设计,但却被后面的 View 遮盖了。

这样的情况,如何解决呢?

拍脑袋想,既然是间距太小了,那我们就拉大间距就好了。修改一个属性解决一个需求,设计师哭晕在工位上。

不过确实有一些设计效果,间距足够,也就不存在遮盖的现象,例如 Bilibili TV 端的部分页面。



但是我们不能只靠改间距解决问题,多数情况下,设计师留给我们的间距并不多。大部分 TV App 是这样的。



既然逃不掉,那就研究一下如何解决。

3、修改绘制顺序原理

修改绘制顺序,其实很简单,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 的取值索引

4、实例

最后,我们写个 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 遮挡。

这是我所整理出来的一份资料,有需求的可以点击我的github获取哦。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342