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
相关产品推荐
相关产品推荐

