Android 自定义 View
- 三种类型:组合现有控件、继承现有控件和直接继承 View 或 ViewGroup
基本流程
- 1 继承 View 或其子类
- 2 构造方法:至少实现 3 个构造方法,可选通过 TypedArray 获取 attrs.xml 中的自定义属性
- 3 重写 onMeasure 方法:测量 View 的大小,通常必须处理 wrap_content 模式的尺寸计算(否则默认填充父布局)
- 4 重写 onSizeChanged(可选),确定 View 的大小
- 5 重写 onLayout(仅 ViewGroup):布局子 View,为每个子 View 调用 View#layout 方法
- 6 重写 onDraw 方法:绘制 View 的内容
- 7 处理触摸事件:重写 onTouchEvent 方法并按需返回 true 消费事件
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
MeasureSpec 测量规格
- View.MeasureSpec 封装了 Mode 和 Size,Mode 分为 EXACTLY、AT_MOST 和 UNSPECIFIED
- EXACTLY 父控件精确指定子控件的尺寸大小,match_parent 或精确值
- AT_MOST 父控件指定子控件最大尺寸的上限,wrap_content
- UNSPECIFIED 父控件不限制子控件尺寸大小,通常在 ScrollView 中使用
int measureSpec = MeasureSpec.makeMeasureSpec(int size, int mode)
Measure 测量流程
- View 测量:ViewRootImpl#performMeasure -> View#measure -> View#onMeasure -> View#setMeasuredDimension
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
- ViewGroup 测量:ViewRootImpl#performMeasure -> View#measure(比如 AbsoluteLayout)-> View#onMeasure(AbsoluteLayout 重写) -> ViewGroup#measureChildren 遍历测量子控件 -> ViewGroup#measureChild -> View#measure(子控件)-> View#onMeasure -> View#setMeasuredDimension
- 先测量子控件的尺寸大小,最后测量自身的尺寸大小
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
Layout 布局流程
- View 布局:ViewRootImpl#performLayout -> View#layout -> View#onMeasure(可能会调用)-> View#onLayout
- 确认控件的坐标位置
- 先确定自身的位置,然后确定子控件位置
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
- ViewGroup 布局:ViewRootImpl#performLayout -> ViewGroup#layout -> View#layout(super.layout)-> View#onMeasure(可能会调用)-> View#onLayout
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
mLayoutCalledWhileSuppressed = true;
}
}
Draw 绘制流程
- ViewRootImpl#performDraw -> View#draw -> View#drawBackground -> View#onDraw -> View#dispatchDraw -> onDrawForeground
protected void onDraw(@NonNull Canvas canvas) {
}
onSizeChanged
- 确定 View 的大小,在 View 的大小发生改变后才会执行(屏幕旋转导致 View 所在的布局空间尺寸变化等场景),可以根据新的宽高重新调整绘制的一些布局、重新初始化与尺寸相关的资源等操作
- 既然 onMeasure 已经测量过 View 的大小了,那为啥还要确定一次?:因为 measure 测量是 View 自身的尺寸大小,而 View 能展现的大小还受父控件的影响