一、前言:
最近在开发过程中遇到一个棘手的问题:当我们试图将应用中的某个视图(View)生成图片时,生成的图片中的布局总是出现错乱。经过仔细排查,发现代码逻辑本身并没有问题,问题似乎出在生成图片的具体操作上。
让我们先来看看代码
scss
view.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))view.layout(0, 0, view.measuredWidth, view.measuredHeight)val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)val canvas = Canvas(bitmap)view.draw(canvas)
这段代码的逻辑很简单:手动执行了 measure()、layout() 和 draw() 方法,然后通过自定义画布生成了 Bitmap。
在测试中发现,问题出在 measure() 和 layout() 的调用上。具体来说,这两个方法的先后执行导致布局的宽度和高度发生了变化。比如,原本宽度为 match_content 的 TextView,调用后变成了文字宽度。如果有其他布局依赖这个 TextView 的位置,就会引发错乱。
尝试逐步注释代码后发现:
- 仅执行
draw()
方法:正常执行,且布局没有问题。 - 注释掉
measure()
或layout()
中的一个:代码同样可以正常运行。
因此问题显然与 measure()
和 layout()
的联合调用有关。
这让我开始思考 View 的绘制流程。
二、View 的绘制流程:
1、从 DecorView 开始绘制
View 的绘制流程起始于 DecorView
。DecorView
是 Activity
中所有 View 的顶级父容器,其本质是一个 FrameLayout
。DecorView
的创建流程如下:
当 Activity
被启动时,最终会调用 ActivityThread
的 handleLaunchActivity
方法:
java
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) { .... Activity a = performLaunchActivity(r, customIntent); if (a != null) { r.createdConfig = new Configuration(mConfiguration); Bundle oldState = r.state; handleResumeActivity(r.tolen, false, r.isForward, !r.activity..mFinished && !r.startsNotResumed); }} final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { unscheduleGcIdler(); mSomeActivitiesChanged = true; ActivityClientRecord r = performResumeActivity(token, clearHide); if (r != null) { final Activity a = r.activity; ... if (r.window == null &&& !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); // 得到了WindowManager,WindowManager是一个接口 ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); } } }}
这里涉及到两个重要的概念:ViewRoot 和 DecorView。ViewRoot 是一个管理类,负责协调 View 的测量、布局和绘制。DecorView 则是 Activity 的根视图,所有其他的 View 都依附于它。当 Activity 创建时,系统会创建一个 ViewRootImpl 对象(ViewRoot 的具体实现),将 DecorView 附加到窗口上,并由 ViewRootImpl 来管理整个视图树。
2、绘制流程详解
1. Measure
measure() 方法的主要作用是计算 View 的宽高,它分为以下两个步骤:
- 生成测量规格(MeasureSpec)。
- 根据规格测量宽高。
arduino
public static class MeasureSpec { public static final int UNSPECIFIED = 0 << 30; public static final int EXACTLY = 1 << 30; public static final int AT_MOST = 2 << 30;
public static int makeMeasureSpec(int size, int mode) { return (size & ~MODE_MASK) | (mode & MODE_MASK); }}
Measure 流程
measure()
是一个 final
方法,无法被重写。实际的测量逻辑是在 onMeasure()
方法中完成。
arduino
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... onMeasure(widthMeasureSpec, heightMeasureSpec); ...} protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasureDimension(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 = sepcSize; break; } return result;}
如果 View 没有重写 onMeasure
,则默认调用 getDefaultSize()
来设置宽高。
2. Layout(布局)
layout()
方法负责确定 View 的位置。View 的布局过程会调用 onLayout()
方法,该方法对于 ViewGroup
来说尤其重要,因为它负责对子 View 的布局。
arduino
public void layout(int l, int t, int r, int b) { setFrame(l, t, r, b); onLayout(changed, l, t, r, b);}
3. Draw(绘制)
draw()
方法用于完成 View 的绘制工作。对于 View
,它通常会直接调用 onDraw()
方法;而对于 ViewGroup
,它会先绘制子 View。
css
public void draw(Canvas canvas) {
...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
...
// Step 2, save the canvas' layers
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
...
if (drawTop) {
...
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
...
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
...
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
...
canvas.drawRect(right - length, top, right, bottom, p);
}
...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
三、问题分析与解决
在最开始 Bug 中,measure()
和 layout()
的多次调用会导致某些 View 的宽高被重新计算,从而影响布局。根本原因是这些方法会修改 View 的内部状态,而这些状态可能已经在原始布局中被依赖。
解决方案:
-
避免重复调用 :如果目标仅是生成图片,可以直接调用
draw()
方法。 -
备份和恢复状态 :在调用
measure()
和layout()
之前,保存 View 的状态,在操作后恢复。 -
使用专用的离屏绘制方法 :比如通过
View.setDrawingCacheEnabled(true)
提取 Bitmap。 -
使用 view .drawToBitmap() 方法也可以直接生成 view 的图片