文章

自定义控件技巧

自定义控件技巧

自定义控件技巧

自定义 ViewGroup

自定义 ViewGroup 需要实现相关的 LayoutParams 方法

onMeasure 多次调用

onMeasure 可能会调用多次,具体看父布局;如果存在 onMeasure 变量赋值的,需要在 onMeasure 清空

setWillNotDraw 设置的条件

默认:

  • ViewGroup 默认调用 setWillNotDraw(true) 以跳过绘制优化(因其通常作为容器,不自身绘制内容)。需手动禁用此优化。
  • 继承自 View,默认未设置 WILL_NOT_DRAW 标记,可直接重写 onDraw(),无需调用 setWillNotDraw(false)

设置了 background,默认会添加上 ~PFLAG_SKIP_DRAW 标记,需要绘制

1
2
3
4
5
6
7
8
9
10
// View.java
public void setBackgroundDrawable(Drawable background) {
	if (background != null) {
		if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {  	
	        mPrivateFlags &= ~PFLAG_SKIP_DRAW;  
	        requestLayout = true;  
	    }  
	}
	// ...
}

setWillNotDraw 行为:

1
2
3
4
5
6
7
8
是否需要自定义绘制内容
├──   setWillNotDraw(false)
├──   
    ├── 是否设置了背景/前景
       ├──   无需操作系统自动处理
       └──   保持默认 setWillNotDraw(true)
    └── 是否继承自修改过 willNotDraw 的父类
        └── 检查父类逻辑必要时显式配置

getChildDrawingOrder 自定义绘制顺序

作用: 在 Android 自定义 ViewGroup 时,getChildDrawingOrder() 方法用于控制子 View 的绘制顺序。默认情况下,子 View 按照添加顺序绘制(后添加的 View 覆盖在先添加的 View 上),但通过重写此方法,可以实现自定义的绘制顺序(如反向绘制、按特定条件排序等)。

前提: 设置了 setChildrenDrawingOrderEnabled(boolean enable)

核心代码:

1
2
3
4
5
6
7
8
9
10
11
public class CustomLayout extends ViewGroup {
    public CustomLayout(Context context) {
        super(context);
        setChildrenDrawingOrderEnabled(true); // 必须启用
    }
    @Override
	protected int getChildDrawingOrder(int childCount, int drawingPosition) {
	    // 默认实现:return drawingPosition;
	    // 自定义逻辑:返回实际需要绘制的子 View 索引
	}
}

示例:

  • 默认正序,按添加的顺序
  • 倒序绘制
1
2
3
4
5
@Override
protected int getChildDrawingOrder(int childCount, int drawingPosition) {
    // 反向绘制:最后一个子 View 先绘制,第一个子 View 最后绘制
    return childCount - 1 - drawingPosition;
}
  • 动态置顶某个子 View
1
2
3
4
5
6
7
8
9
10
11
12
private int mTopChildIndex = 0; // 需要置顶的子 View 索引

@Override
protected int getChildDrawingOrder(int childCount, int drawingPosition) {
    if (drawingPosition == childCount - 1) {
        return mTopChildIndex; // 最后一个绘制的子 View(显示在最上层)
    } else if (drawingPosition >= mTopChildIndex) {
        return drawingPosition + 1; // 跳过已处理的置顶子 View
    } else {
        return drawingPosition;
    }
}
  • 按 Z 轴值排序
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected int getChildDrawingOrder(int childCount, int drawingPosition) {
    // 假设子 View 实现了 Z 轴接口,按 Z 轴值从小到大排序
    List<View> sortedChildren = new ArrayList<>();
    for (int i = 0; i < childCount; i++) {
        sortedChildren.add(getChildAt(i));
    }
    Collections.sort(sortedChildren, (v1, v2) -> 
        Float.compare(v1.getZ(), v2.getZ()));

    // 返回排序后的索引
    return indexOfChild(sortedChildren.get(drawingPosition));
}

疑惑:

  • 子 View 的触摸事件顺序是否受影响? 不影响。触摸事件的分发顺序由 ViewGroup 的 onInterceptTouchEvent 和子 View 的添加顺序决定,与绘制顺序无关。

  • 如何实现点击子 View 后置顶?

    1. 在点击事件中更新 mTopChildIndex
    2. 调用 invalidate() 触发重绘。
    3. 在 getChildDrawingOrder() 中动态调整顺序。

layout RTL 适配封装

状态保存

  • onSaveInstanceState
  • onRestoreInstanceState

其他

内存抖动

onMeasure, onLayout, onDraw 调用非常频繁,不要在这些方法中 new 对象,否则容易出现内存抖动

高效自定义 View

本文由作者按照 CC BY 4.0 进行授权