深入浅出Android系列之从ViewToBitmap延伸到View的绘制全过程

一、前言:

最近在开发过程中遇到一个棘手的问题:当我们试图将应用中的某个视图(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 的绘制流程起始于 DecorViewDecorViewActivity 中所有 View 的顶级父容器,其本质是一个 FrameLayoutDecorView 的创建流程如下:

Activity 被启动时,最终会调用 ActivityThreadhandleLaunchActivity 方法:

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 的内部状态,而这些状态可能已经在原始布局中被依赖。

解决方案:

  1. 避免重复调用 :如果目标仅是生成图片,可以直接调用 draw() 方法。

  2. 备份和恢复状态 :在调用 measure()layout() 之前,保存 View 的状态,在操作后恢复。

  3. 使用专用的离屏绘制方法 :比如通过 View.setDrawingCacheEnabled(true) 提取 Bitmap。

  4. 使用 view .drawToBitmap() 方法也可以直接生成 view 的图片

原文地址:mp.weixin.qq.com/s/nG1YboB3N...

相关推荐
二流小码农7 分钟前
鸿蒙开发:DevEcoStudio中的代码提取
android·ios·harmonyos
江湖有缘26 分钟前
使用obsutil工具在OBS上完成基本的数据存取【玩转华为云】
android·java·华为云
移动开发者1号2 小时前
Android 多 BaseUrl 动态切换策略(结合 ServiceManager 实现)
android·kotlin
移动开发者1号2 小时前
Kotlin实现文件上传进度监听:RequestBody封装详解
android·kotlin
AJi5 小时前
Android音视频框架探索(三):系统播放器MediaPlayer的创建流程
android·ffmpeg·音视频开发
柿蒂6 小时前
WorkManager 任务链详解:优雅处理云相册上传队列
android
alexhilton6 小时前
使用用例(Use Case)以让Android代码更简洁
android·kotlin·android jetpack
峥嵘life6 小时前
Android xml的Preference设置visibility=“gone“ 无效分析解决
android·xml
用户2018792831677 小时前
通俗故事:驱动二进制文件在AOSP中的角色
android
穷人小水滴7 小时前
在 Termux 中签名 apk 文件
android·linux·apk