这三个方法都是用于触发视图更新的,但它们的应用场景和触发的"更新级别"完全不同。
- invalidate() : "重绘 "。意思是"我当前的内容变了(比如颜色、文字、位置等),需要重新画一遍"。它只触发
onDraw()方法。 - postInvalidate() : "在非UI线程中安全地重绘 "。功能和
invalidate()完全一样,但它可以在非UI线程(子线程)中调用。 - requestLayout() : "重新测量和布局 "。意思是"我的尺寸可能变了,或者子视图的结构变了,整个布局需要重新计算"。它会触发完整的
measure()->layout()流程,可能也会触发onDraw()。
详细对比
| 特性 | invalidate() |
postInvalidate() |
requestLayout() |
|---|---|---|---|
| 核心作用 | 标记视图的局部区域为脏区 ,请求重绘。 | 在非UI线程 中安全地请求重绘。 | 请求重新布局整个视图树。 |
| 触发方法 | onDraw(Canvas) |
onDraw(Canvas) |
onMeasure(), onLayout() (以及可能的 onDraw()) |
| 调用线程 | 必须 在UI主线程中调用。 | 可以在任何线程(主线程或子线程)中调用。 | 必须 在UI主线程中调用。 |
| 性能开销 | 较小。只影响自身视图的绘制。 | 较小 。同 invalidate()。 |
较大。会从根视图开始,可能遍历整个视图树,重新测量和布局所有相关视图。 |
| 使用场景 | 内容改变但视图的大小和位置不变 时。 例如: - 改变背景色、文字颜色 - 在 onTouchEvent 中移动一个子视图 - 自定义View时动态改变绘制内容 |
在子线程中更新UI,例如: - 在 AsyncTask 的 doInBackground 中更新进度 - 在子线程中进行计算,并实时反馈到UI上 |
视图的边界(尺寸)可能发生变化 时。 例如: - 给 TextView 设置新的文字,导致其宽高改变 - 动态添加或移除子View - 在自定义View中改变了 LayoutParams |
深入解析与示例
1. invalidate()
当你只改变了视图的内容 ,而它的尺寸和位置 没有变化时,使用 invalidate()。
工作流程 :
invalidate() -> dispatchDraw() -> onDraw()
示例:
java
public class CustomView extends View {
private int mCircleColor = Color.RED;
// 在UI线程中改变颜色并重绘
public void changeColor() {
mCircleColor = Color.BLUE;
invalidate(); // 触发onDraw,视图会变成蓝色
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(mCircleColor);
canvas.drawCircle(100, 100, 50, paint);
}
}
2. postInvalidate()
invalidate() 的线程安全版本。其内部实现是向主线程的Handler发送了一个消息,最终在主线程中调用了 invalidate()。
示例:
java
public class CustomView extends View {
private int mProgress = 0;
// 在子线程中更新进度
public void startUpdateInBackground() {
new Thread(new Runnable() {
@Override
public void run() {
while (mProgress < 100) {
mProgress++;
// 不能在子线程直接调用invalidate(),否则会崩溃
// invalidate(); // 错误!
postInvalidate(); // 正确!安全地在主线程触发重绘
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制一个根据mProgress变化的进度条...
}
}
3. requestLayout()
当你认为当前视图的尺寸或布局已经不再满足要求,需要重新计算时,调用此方法。它会从ViewRootImpl开始,执行一个完整的遍历(Traversal)。
工作流程 :
requestLayout() -> onMeasure() -> onLayout() -> (可能) onDraw()
为什么可能触发 onDraw()?
因为重新布局后,视图的位置和大小可能发生了变化,系统认为你需要重新绘制以适应新的布局。
示例:
java
public class MyTextView extends TextView {
public void setTextAndResize(String text) {
setText(text);
// 设置新文字后,这个TextView所需的宽度和高度可能变了。
// 我们需要告诉父布局:"我的尺寸可能变了,请重新测量和摆放我"。
requestLayout();
}
// 或者在自定义View中,你重写了onMeasure,并且条件发生了变化
private boolean mIsWideMode = false;
public void setWideMode(boolean isWide) {
if (mIsWideMode != isWide) {
mIsWideMode = isWide;
// 测量逻辑改变了,必须请求重新布局
requestLayout();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mIsWideMode) {
// 宽模式的测量逻辑
setMeasuredDimension(500, 100);
} else {
// 窄模式的测量逻辑
setMeasuredDimension(200, 100);
}
}
}
如何选择?
-
只涉及视觉表现变化(颜色、位置、透明度等)?
- 是 -> 使用
invalidate()。 - 如果在子线程 中 -> 使用
postInvalidate()。
- 是 -> 使用
-
视图的尺寸或布局结构发生了变化(宽高、边距、子视图数量等)?
- 是 -> 使用
requestLayout()。
- 是 -> 使用
-
不确定该用哪个?
- 先想想你的改变是否影响了视图的尺寸 。如果影响了,用
requestLayout();如果没影响,只用invalidate()。滥用requestLayout()会导致不必要的性能损耗。
- 先想想你的改变是否影响了视图的尺寸 。如果影响了,用
组合使用
在某些复杂情况下,你甚至可能需要同时调用两者。
java
// 例如,一个自定义View,它既改变了内部状态(需要重绘),又改变了自己的尺寸(需要重新布局)
public void complexChange() {
changeInternalState(); // 改变状态
requestLayout(); // 请求重新布局(这会隐式包含重绘)
// 或者,如果你确定invalidate()是必要的,也可以显式调用,但通常requestLayout()就够了。
// invalidate();
}
总结一下,理解这三个方法的区别,关键在于理解Android视图系统的工作流程:测量(Measure) -> 布局(Layout) -> 绘制(Draw) 。requestLayout() 触发了前两步(可能包括第三步),而 invalidate()/postInvalidate() 只触发了第三步。