0
点赞
收藏
分享

微信扫一扫

Android控制View绘制顺序的关键方法——setChildrenDrawingOrderEnabled

Brose 2022-08-02 阅读 103


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 被放大就会出现遮盖的效果。

Android控制View绘制顺序的关键方法——setChildrenDrawingOrderEnabled_ide

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

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

 

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

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

Android控制View绘制顺序的关键方法——setChildrenDrawingOrderEnabled_自定义_02

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

Android控制View绘制顺序的关键方法——setChildrenDrawingOrderEnabled_取值_03

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

三、修改绘制顺序原理

修改绘制顺序,其实很简单,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 步调整:

  1. 调用​​setChildrenDrawingOrderEnable(true)​​ 开启自定义绘制顺序
  2. 重写​​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)​​ 开启自定义绘制顺序。

Android控制View绘制顺序的关键方法——setChildrenDrawingOrderEnabled_自定义_04

此时,焦点放大时,就不会被其他 View 遮挡。


延伸:

ViewGroup及其子类如果要想指定子View的绘制顺序只需两步:

1. setChildrenDrawingOrderEnabled(true) 开启自定义子View的绘制顺序;

2. 用setZ(float),自定义Z值,值越大越优先绘制;

重写getChildDrawingOrder,让gridview倒序绘制item

最近要实现一个效果,gridview每个item加上动画,发现每个item都会被后面的item挡住,重写viewgroup的这个方法可实现倒叙绘制item,让后面的item绘制在前面item的底部。

public class MyGridView extends GridView {
public MyGridView(Context context, AttributeSet attrs) {//构造函数
super(context, attrs);
setChildrenDrawingOrderEnabled(true);
}

@Override
protected int getChildDrawingOrder(int childCount, int i) {
return childCount - i - 1;//倒序
}
}

GridView 在TV上解决item放大时候,被其他item遮挡,单纯使用bringToFront无法解决的问题

做过TV上使用GridView,对item进行放大的时候,会被后面或者其他item遮挡的问题,那么这个问题一般怎么解决呢?

其实当我们遇到这样子的情况,使用bringToFront是无法解决问题的。

其实我们要做的就是,要改变GridView对子view的绘制顺序,要将选中的item项绘制显示在顶层,所以要改变GridView的子View绘制顺序;

/**
*
* @author zhanghuagang 2017.7.6
*
*/
public class CommonGridView extends GridView {
private View mLastView = null;
private int mSelectedPosition;
/**
*
* @author zhanghuagang 2017.7.6
*
*/
public CommonGridView(Context context) {
this(context, null);
}

public CommonGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setChildrenDrawingOrderEnabled(true);
setSmoothScrollbarEnabled(true);
}

public CommonGridView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

@Override
protected void setChildrenDrawingOrderEnabled(boolean enabled) {
super.setChildrenDrawingOrderEnabled(enabled);
}

public int getSelectedPosition() {
return mSelectedPosition;
}

public void setSelectedPosition(int mSelectedPosition) {
this.mSelectedPosition = mSelectedPosition;
}

@Override
public void draw(Canvas canvas) {
super.draw(canvas);
}

private void zoomInView(View v){
AnimatorSet animSet = new AnimatorSet();
float[] values = new float[] { 1.0f ,1.18f };
animSet.playTogether(ObjectAnimator.ofFloat(v, "scaleX", values),
ObjectAnimator.ofFloat(v, "scaleY", values));
animSet.setDuration(100).start();
}

private void zoomOutView(View v){
AnimatorSet animSet = new AnimatorSet();
float[] values = new float[] { 1.18f ,1.0f };
animSet.playTogether(ObjectAnimator.ofFloat(v, "scaleX", values),
ObjectAnimator.ofFloat(v, "scaleY", values));
animSet.setDuration(100).start();
}



public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if(view!=null)
zoomInView(view);
if (view != mLastView && mLastView!=null) {
zoomOutView(mLastView);
}
mLastView=view;
}




/**
* 此方法用来完美觉得item放大 ,绘制顺序出现问题的
*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {
if (this.getSelectedItemPosition() != -1) {
if (i + this.getFirstVisiblePosition() == this.getSelectedItemPosition()) {// 这是原本要在最后一个刷新的item
return childCount - 1;
}
if (i == childCount - 1) {// 这是最后一个需要刷新的item
return this.getSelectedItemPosition() - this.getFirstVisiblePosition();
}
}
return i;
}


}

首先我们是自定义view,在构造方法中将是否可以改变绘制顺序设置为true,改为可以。
setChildrenDrawingOrderEnabled(true);
然后,覆盖一下关键方法。
getChildDrawingOrder方法,在这个中实现改变绘制顺序的逻辑,那么我们既然要在放大的时候,不被其他item遮挡,那么就必须在他选中的时候,将他绘制顺序放在最后,大改这个方法的实现逻辑如下。

/**
* 此方法用来完美解决item放大 ,绘制顺序出现问题的
*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {
if (this.getSelectedItemPosition() != -1) {
if (i + this.getFirstVisiblePosition() == this.getSelectedItemPosition()) {// 这是原本要在最后一个刷新的item
return childCount - 1;
}
if (i == childCount - 1) {// 这是最后一个需要刷新的item
return this.getSelectedItemPosition() - this.getFirstVisiblePosition();
}
}
return i;
}

第一行代码是判断,当前选中的item的position是否有效。
代码逻辑很好理解,如果当前选中的子view是可见的,那么就将其设置为最后一个子view,来绘制,如果选中的是最后一个view,就返回他的真是有效的position,这样即可。


举报

相关推荐

0 条评论