1 自定义View分类
1.1. 自定义View
在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View
1.2. 自定义ViewGroup
自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout
2 自定义View的绘制流程
- 自定义View主要是实现 onMeasure + onDraw
- 自定义ViewGroup主要是实现onMeasure + onLayout
1.自定义View的开发
在 Android 中,自定义 View 的三个核心流程是 测量(Measure) 、布局(Layout) 和 绘制(Draw) 。以下是每个流程的详细解析:
1. 测量(Measure)
测量阶段的主要目的是计算 View 的宽高,结果存储在 getMeasuredWidth() 和 getMeasuredHeight() 中。
核心方法
-
onMeasure(int widthMeasureSpec, int heightMeasureSpec):- 父视图通过
MeasureSpec将宽高限制传递给子视图。 - 子视图根据这些限制自行计算尺寸,并通过
setMeasuredDimension()方法设置自身的宽高。
- 父视图通过
MeasureSpec 介绍
MeasureSpec 是父容器传递给子视图的宽高规格,包含两部分信息:
-
模式(Mode) :
UNSPECIFIED:父容器不对视图的大小做任何限制(少见)。EXACTLY:父容器指定了确切的宽高,子视图必须遵守。AT_MOST:父容器指定了一个最大宽高,子视图不能超过这个范围。
-
尺寸(Size) :模式对应的具体数值。
自定义 View 示例
java
复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 100; // 默认宽度
int height = 100; // 默认高度
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize; // 父容器指定确切宽度
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(width, widthSize); // 限制最大宽度
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
setMeasuredDimension(width, height);
}
2. 布局(Layout)
布局阶段的主要任务是确定 View 的位置(left、top、right、bottom),即视图在父容器中的具体位置。
核心方法
-
onLayout(boolean changed, int left, int top, int right, int bottom):- 通过参数指定 View 在屏幕中的位置。
- 在 ViewGroup 中,会循环对子视图调用
layout()方法。
自定义 ViewGroup 示例
java
复制代码
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childCount = getChildCount();
int curLeft = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(curLeft, 0, curLeft + childWidth, childHeight);
curLeft += childWidth;
}
}
3. 绘制(Draw)
绘制阶段负责将 View 绘制到屏幕上,通常用来自定义内容的呈现。
核心方法
-
onDraw(Canvas canvas):-
使用 Canvas 对象绘制内容。
-
常用 API:
drawRect():绘制矩形。drawCircle():绘制圆形。drawText():绘制文本。drawBitmap():绘制位图。
-
自定义 View 示例
java
复制代码
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
// 绘制一个红色的圆
canvas.drawCircle(getWidth() / 2, getHeight() / 2, Math.min(getWidth(), getHeight()) / 2, paint);
}
三者的联系
- 测量阶段:确定宽高,用于后续布局和绘制。
- 布局阶段:确定位置,决定子视图如何排列。
- 绘制阶段:根据宽高和位置,最终渲染内容。
注意点
- 重写 onMeasure() 时必须调用
setMeasuredDimension(),否则会抛出异常。 - onLayout() 只在 ViewGroup 中重写,普通 View 不需要。
- onDraw() 不要在方法中创建对象或执行耗时操作,避免性能问题。
通过以上三个流程的配合,可以高效地实现自定义 View 的功能并满足复杂布局和绘制需求。
2.Paint常用方法
3.Canvas绘制
3.1 Canvas基本图形的绘制
3.2Canvas的变换操作
3.3Canvas的保存与回滚
3.4屏幕显示与Canvas的关系
3.5文字绘制
3.6圆形头像绘制
3.7 Matrix矩阵操作
- 平移矩阵推导
- 缩放矩阵推导
- 旋转矩阵的推导
- 操作矩阵
4.View矩阵的原理
5自定义drawable
3 View的层级结构
4 面试题
4.1 LayoutParams 是什么?与MeasureSpec有关系吗?
1. LayoutParams
定义
LayoutParams是 Android 中用于定义视图布局规则的一个类,属于 View 的父容器(ViewGroup)的一个内部类。- 它描述了视图在其父容器中的 宽高、位置和边距 等信息。
作用
- 每个
ViewGroup都会定义自己的LayoutParams子类(如LinearLayout.LayoutParams,RelativeLayout.LayoutParams),用来解析子视图在布局中的排列规则。 LayoutParams在布局过程中会被ViewGroup使用来布局子视图。
常见参数
width和height:定义视图的宽和高,常见值包括match_parent,wrap_content, 或具体尺寸(如 100dp)。margin:用于定义视图之间的间距。gravity(在某些布局中使用):子视图对齐方式。
代码示例
java
复制代码
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(16, 16, 16, 16);
view.setLayoutParams(params);
2. MeasureSpec
定义
MeasureSpec是 Android 中用来描述视图测量要求的一个工具类。- 它是一种 压缩的整数值,将测量模式和尺寸合并在一起,用来告诉子视图如何测量自己。
作用
MeasureSpec是布局过程中View的onMeasure()方法的关键参数。- 它确定了视图的 测量模式(Mode) 和 测量大小(Size) 。
测量模式
UNSPECIFIED:父容器对视图的大小没有任何限制,视图可以是任意大小。EXACTLY:父容器指定了精确的大小,视图必须匹配这个尺寸。AT_MOST:父容器指定了一个最大值,视图的大小不能超过这个值。
代码示例
java
复制代码
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
3. LayoutParams 与 MeasureSpec 的关系
虽然两者是 Android 布局过程中不同阶段的工具,但它们在布局测量和排版的过程中紧密关联:
布局阶段
-
获取 LayoutParams
- 父容器通过子视图的
LayoutParams获取布局约束信息(如宽、高、margin)。
- 父容器通过子视图的
-
生成 MeasureSpec
- 父容器根据自己的尺寸、子视图的
LayoutParams(如wrap_content或match_parent),为子视图生成对应的MeasureSpec。
- 父容器根据自己的尺寸、子视图的
-
调用 onMeasure
- 父容器将生成的
MeasureSpec传递给子视图,子视图根据MeasureSpec计算自己的大小。
- 父容器将生成的
流程图
rust
复制代码
父容器 --> 获取子视图的 LayoutParams --> 根据父容器尺寸和 LayoutParams 生成 MeasureSpec --> 传递 MeasureSpec 给子视图 --> 子视图测量自己
4. 总结
-
LayoutParams是父容器用来描述子视图布局规则的类,负责 布局阶段。 -
MeasureSpec是父容器用来告知子视图测量要求的工具,负责 测量阶段。 -
两者的联系在于:
- 父容器通过
LayoutParams理解子视图的布局规则。 - 父容器基于
LayoutParams和自己的约束条件,生成MeasureSpec传递给子视图完成测量。
- 父容器通过
4.2 为什么要measure
在 Android 中,measure 是布局过程中的第一步,其作用是测量视图的尺寸。视图的尺寸直接决定了它在屏幕上的布局效果,因此测量是整个视图系统工作的基础。下面详细分析为什么需要 measure:
1. 为什么要进行测量?
- 动态布局: Android 应用需要适配多种屏幕尺寸和分辨率,视图的大小不能提前固定,必须根据父容器和子视图的需求动态计算。
- 层级结构复杂: Android 中的视图层级可能非常复杂,父容器和子视图需要协作来确定每个视图的尺寸。例如,
LinearLayout中每个子视图的宽高可能由其他子视图决定。 - 资源约束: 在布局过程中,需要确保每个视图的大小符合父容器的空间限制,从而避免越界或浪费屏幕空间。
2. Measure 的目的是什么?
-
确定每个视图的大小: 每个视图都需要通过测量确定自己的宽度和高度,才能决定如何绘制内容。
-
满足布局规则: 不同的父容器(如
LinearLayout,ConstraintLayout)有不同的布局规则,这些规则影响子视图的测量结果。例如:wrap_content:根据内容决定大小。match_parent:占满父容器的剩余空间。
-
为后续布局和绘制服务: 测量阶段确定了视图的大小,接下来的布局阶段会根据测量结果,决定视图的位置,最后在绘制阶段进行渲染。
3. Measure 的工作原理
测量通过调用视图的 measure() 方法触发,主要依赖以下两点:
-
父容器传递的测量要求(MeasureSpec)
- 包括测量模式和尺寸限制。
-
视图的 LayoutParams
- 定义视图的布局规则(如
wrap_content,match_parent)。
- 定义视图的布局规则(如
父容器根据自身尺寸和子视图的 LayoutParams 生成 MeasureSpec,再递归调用子视图的 measure() 方法,直到整棵视图树的测量完成。
4. 如果不进行测量会怎样?
- 如果没有测量,视图的尺寸将无法确定,Android 系统就不知道视图占据的空间和位置。
- 最终,视图无法正确绘制,用户将看不到期望的布局效果。
5. 总结:为什么要 Measure?
measure 的本质是解决 动态布局 和 屏幕适配 问题:
- 确定视图的大小,以适配不同的屏幕尺寸和分辨率。
- 实现复杂的视图层级和布局规则。
- 为布局阶段和绘制阶段提供必要的尺寸信息。
measure 是 Android 渲染管线中的关键步骤,没有测量,后续的布局和绘制工作都无法完成。
4.3 Android两种坐标系
4.4 getMeasureWidth与getWidth的区别
在 Android 中,getMeasuredWidth() 和 getWidth() 都是用于获取视图宽度的,但它们有明显的区别,尤其是在视图的生命周期中:
1. getMeasuredWidth()
-
定义:表示视图经过 测量阶段 后得到的宽度。
-
来源:该值是
measure()方法根据父容器提供的MeasureSpec计算出的值。 -
特点:
- 仅在测量阶段之后才有意义。
- 可能与最终视图的实际宽度不同(视图的布局可能会调整测量值)。
- 在视图的 onMeasure() 方法中可以获取此值。
-
使用场景:
- 用于了解测量阶段的结果,常用于自定义视图中,分析布局计算的宽度。
2. getWidth()
-
定义:表示视图在 布局阶段 完成后,实际分配的宽度。
-
来源:该值是在布局阶段后,由
layout()方法确定的宽度。 -
特点:
- 是视图在屏幕上显示的实际宽度。
- 只有在视图布局完成后,
getWidth()才能返回有效值。 - 如果在布局完成之前调用,可能返回
0。
-
使用场景:
- 用于实际的渲染或操作,确保视图已经布局好,并已在屏幕上占据指定的宽度。
3. 核心区别
| 特性 | getMeasuredWidth() | getWidth() |
|---|---|---|
| 阶段 | 测量阶段后(measure) | 布局阶段后(layout) |
| 含义 | 测量的宽度 | 实际绘制的宽度 |
| 修改可能性 | 可被布局阶段调整 | 已经固定,不可再修改 |
| 调用时机 | 测量完成后即可调用 | 布局完成后才能获取有效值 |
| 返回值稳定性 | 可能与最终宽度不同 | 是最终用于绘制的宽度 |
4. 示例
假设一个视图的 LayoutParams 是 wrap_content,测量阶段会根据内容计算出宽度:
java
复制代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = getMeasuredWidth(); // 测量的宽度
}
在布局阶段,可能会根据父容器的约束调整宽度:
java
复制代码
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int width = getWidth(); // 实际分配的宽度
}
5. 注意事项
- 如果只需要知道视图的最终显示宽度,请使用
getWidth()。 - 如果需要在自定义视图中了解测量结果,并在布局阶段前调整子视图布局,请使用
getMeasuredWidth()。 - 在视图未完成测量或布局之前,调用这两个方法都会返回
0。