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

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 遮盖了。

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

 

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

不过确实有一些设计效果,间距足够,也就不存在遮盖的现象,例如 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 步调整:

  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) 开启自定义绘制顺序。

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

转自:https://mp.weixin.qq.com/s/G3BKLbu1gjIIf8-qY6DFBg


延伸:

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,这样即可。

原文链接:https://blog.csdn.net/hua631150873/article/details/74989666

 

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android的Widget是指可以被放置在桌面或者其他应用中的小部件,比如常见的时钟、天气、日历等等。在Android中,Widget的布局可以使用XML文件来进行定义,与普通的布局定义类似,可以使用各种属性来设置Widget的样式和行为。 在Widget的XML布局文件中,可以使用View类中定义的许多属性来控制Widget的样式和行为,比如: 1. android:id:设置Widget的ID,可以在代码中通过findViewById()方法来获取对应的View对象。 2. android:layout_width、android:layout_height:设置Widget的宽度和高度,可以使用具体数值或者match_parent、wrap_content等特殊值。 3. android:layout_gravity:设置Widget在父布局中的对齐方式,比如center、left、right等等。 4. android:padding、android:paddingLeft、android:paddingRight等:设置Widget的内边距,用于控制Widget内部内容的显示位置。 5. android:background:设置Widget的背景颜色或者背景图片。 6. android:clickable、android:longClickable:设置Widget是否可以被点击或者长按。 7. android:focusable、android:focusableInTouchMode:设置Widget是否可以获取焦点,用于控制Widget是否可以响应键盘事件等。 8. android:visibility:设置Widget是否可见,可以使用值为visible、invisible、gone。 除了以上列出的属性之外,还有许多其他的属性可以用于控制Widget的样式和行为,具体可以查看Android官方文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值