You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android激活相机后绘图应用遇空指针及宽高异常求助

解决Android绘图应用相机拍照后Canvas相关问题

首先,咱们先理清问题的根源:你遇到的两个核心问题——NullPointerException和"width and height must be >0",本质都是Activity重建时Canvas/Bitmap状态丢失,以及错误地试图序列化Canvas导致的。Canvas本身是一个绘制工具,不是数据载体,真正要保存的是它背后的Bitmap(所有绘制内容都存在这里)。下面一步步给你解决:

一、放弃Canvas序列化,转而保存绘制用的Bitmap

Canvas无法被直接序列化,你的SerializableCanvas包装类其实没抓住核心——我们需要保存的是承载绘制内容的Bitmap,而不是Canvas对象。因为Bitmap太大不能直接存到Bundle里,我们可以把它保存到应用的私有文件,然后在onSaveInstanceState里记录文件路径,重建时再加载。

1. 在DrawingView中添加Bitmap的保存/加载方法

// DrawingView内部
private Bitmap drawBitmap; // 这是你用来绘制的核心Bitmap,Canvas绑定它:drawCanvas = new Canvas(drawBitmap);

// 保存Bitmap到应用私有文件
private String saveBitmapToFile(Context context) throws IOException {
    File tempFile = new File(context.getFilesDir(), "temp_drawing.png");
    try (FileOutputStream fos = new FileOutputStream(tempFile)) {
        drawBitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
    }
    return tempFile.getAbsolutePath();
}

// 从文件加载Bitmap
private Bitmap loadBitmapFromFile(String filePath) {
    return BitmapFactory.decodeFile(filePath);
}

2. 在MainActivity中处理状态保存与恢复

// MainActivity内部
private DrawingView drawingView;

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    try {
        // 保存Bitmap的文件路径到Bundle
        String bitmapPath = drawingView.saveBitmapToFile(this);
        outState.putString("SAVED_DRAWING_PATH", bitmapPath);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    String savedPath = savedInstanceState.getString("SAVED_DRAWING_PATH");
    if (savedPath != null) {
        Bitmap savedBitmap = BitmapFactory.decodeFile(savedPath);
        drawingView.restoreSavedBitmap(savedBitmap);
    }
}

然后在DrawingView中添加restoreSavedBitmap方法,先暂存Bitmap,等View准备好再绘制:

private Bitmap pendingBitmap; // 暂存恢复的Bitmap,等待View测量完成

public void restoreSavedBitmap(Bitmap bitmap) {
    this.pendingBitmap = bitmap;
    invalidate(); // 触发View重绘,进而触发onMeasure
}

二、解决"width and height must be >0"错误

这个错误是因为你在View还没完成测量(宽高为0)时,就调用了drawCanvas.getWidth()/getHeight()。我们需要等View测量完成后再初始化Bitmap和绘制内容。

1. 在DrawingView的onMeasure中处理初始化与恢复

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int viewWidth = getMeasuredWidth();
    int viewHeight = getMeasuredHeight();

    // 如果Bitmap还没初始化,或者尺寸和View不匹配,重新创建
    if (drawBitmap == null || drawBitmap.getWidth() != viewWidth || drawBitmap.getHeight() != viewHeight) {
        drawBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
        drawCanvas = new Canvas(drawBitmap);

        // 恢复之前保存的绘制内容
        if (pendingBitmap != null) {
            drawCanvas.drawBitmap(pendingBitmap, 0, 0, canvasPaint);
            pendingBitmap.recycle(); // 释放临时Bitmap内存
            pendingBitmap = null;
        }

        // 如果有等待绘制的拍照图片,一起处理
        if (pendingCameraImage != null) {
            drawCameraImage(pendingCameraImage);
            pendingCameraImage = null;
        }
    }
}

// 抽离绘制相机图片的方法,确保宽高有效
private void drawCameraImage(Bitmap image) {
    Bitmap resizedImage = resize(image, drawCanvas.getWidth(), drawCanvas.getHeight());
    drawCanvas.drawBitmap(resizedImage, 0, 0, canvasPaint);
    invalidate();
}

2. 修改drawImage方法,避免宽高为0时操作

private Bitmap pendingCameraImage;

public void drawImage(Bitmap image) {
    // 如果Canvas还没准备好(宽高为0),暂存图片
    if (drawCanvas == null || drawCanvas.getWidth() <= 0 || drawCanvas.getHeight() <= 0) {
        pendingCameraImage = image;
        invalidate();
        return;
    }
    drawCameraImage(image);
}

三、优化相机拍照逻辑,获取完整照片

你原来的相机代码只能获取缩略图,不仅质量差,还容易出现状态问题。我们可以指定照片保存路径,获取完整的拍照图片:

1. 相机启动与文件创建

// MainActivity内部
private File cameraPhotoFile;
private static final int CAMERA_INTENT_CALLED = 1001;

private void launchCamera() {
    Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePhotoIntent.resolveActivity(getPackageManager()) == null) {
        return;
    }

    // 创建临时文件保存拍照结果
    try {
        cameraPhotoFile = createTempPhotoFile();
    } catch (IOException e) {
        e.printStackTrace();
        return;
    }

    // 使用FileProvider获取Uri(适配Android 7.0+)
    Uri photoUri = FileProvider.getUriForFile(this, "com.your.app.package.fileprovider", cameraPhotoFile);
    takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
    startActivityForResult(takePhotoIntent, CAMERA_INTENT_CALLED);
}

private File createTempPhotoFile() throws IOException {
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
    String fileName = "CAMERA_PHOTO_" + timeStamp;
    return File.createTempFile(fileName, ".jpg", getExternalFilesDir(Environment.DIRECTORY_PICTURES));
}

2. 处理相机返回结果

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == CAMERA_INTENT_CALLED && resultCode == RESULT_OK) {
        // 从临时文件加载完整Bitmap
        Bitmap cameraBitmap = BitmapFactory.decodeFile(cameraPhotoFile.getAbsolutePath());
        drawingView.drawImage(cameraBitmap);
    }
}

3. 配置FileProvider(必须)

在AndroidManifest.xml中添加:

<application>
    ...
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.your.app.package.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

然后在res/xml目录下创建file_paths.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="camera_photos" path="Pictures" />
</paths>

最后再强调几个关键注意点

  • 永远不要序列化Canvas:Canvas是临时绘制工具,所有绘制数据都存在Bitmap里,保存Bitmap才是正确思路。
  • Bitmap内存管理:用完的临时Bitmap记得调用recycle()释放内存,避免OOM。
  • View生命周期:所有涉及View宽高的操作,必须等onMeasure完成后再执行,否则会出现宽高为0的错误。

内容的提问来源于stack exchange,提问作者marvin421

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 04:19:24