解决 Android 中 Camera 在 Preview 和图像输出时的 Rotation 问题

之前项目中拍照都是调用的 android.media.action.IMAGE_CAPTURE 这个系统组件实现,其实很多应用都是的,包括微信。因为这样省心,不用考虑数据处理和兼容性问题。

但是最近这个外包项目中因为要求连续拍照 16 张,在多次调用这个组件的时候,发现很多机型会报一个莫名其妙的错误(具体记不清了,反正不是 Java 层的什么 Exception ,应该是 native 层的异常)。

而且 crash 后不仅不进入 onActivityResult() 回调,还莫名其妙的把之前保存的照片数据都清空了。

没办法,只有自己用 Camera 去实现拍照,然后处理图片数据。

一来为了快速解决问题,二来也确实是很长时间没接触过这块内容,正好借此机会复习下。

首先当然是需要一个 SurfaceView ,然后获取其 ViewHolder ,并添加一个回调;

surfaceCreated() 方法里创建 Camera ,并设置当前 ViewHolder 为 PreviewDisplay;

surfaceDestroyed() 方法里销毁 Camera 对象,surfaceChanged() 方法先不管:

SurfaceHolder holder=surfaceView.getHolder();
holder.addCallback(new SurfaceHolder.Callback() {
	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
	}
	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		if(camera==null){
			camera=Camera.open();
		}
		try {
			camera.setPreviewDisplay(holder);
			camera.startPreview();
			camera.autoFocus(new AutoFocusCallback(){
				@Override
				public void onAutoFocus(boolean success, Camera camera) {
					if(success){
						Camera.Parameters params=camera.getParameters();
						params.setFlashMode(flashMode);
						params.setPictureFormat(ImageFormat.JPEG);
						Size size=params.getSupportedPreviewSizes().get(0);
						params.setPreviewSize(size.width, size.height);
						camera.setParameters(params);
					}
				}
			}); 
		} catch (Exception e) {
			camera.release();
		}
	}
	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		camera.stopPreview();
		camera.release();
		camera=null;
	}
});

然后就是拍照了,直接调用 CameratakePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) 方法;

这里需要传三个参数,第一个是按下快门后的回调,第二个是得到 RAW 数据后的回调,第三个是得到 JPEG 数据后的回调,一般情况下只需要传第三个即可:

camera.takePicture(null, null, new PictureCallback(){
	@Override
	public void onPictureTaken(byte[] data, Camera camera) {
		//TODO: Compress data to file.
	}
});

这方法是 API 1 里面的,API 5 重载了个新方法:

takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback postview, PictureCallback jpeg)

多了个 postviewPictureCallback 类型回调,具体用法还不清楚,以后再研究。

在这里预览的时候应该就能发现问题:方向不对,相差 90 度。

网上有人说得调用 camera.setDisplayOrientation(90) ,但是这样拍出来的图片也是翻转的,更关键的是不能随屏幕方向自动调整预览和输出图片的方向。

然后就想到了 Activity 有个 onConfigurationChanged() 的回调,可以用来处理屏幕翻转相关问题。当然前提是得在 AndroidManifest.xml 里面设置:

android:configChanges="orientation|screenSize"

然后在 onConfigurationChanged() 里面更新当前屏幕方向:

@Override
public void onConfigurationChanged(Configuration newConfig){
	super.onConfigurationChanged(newConfig);
	if(getApplication() instanceof RLApplication){
		((RLApplication)getApplication()).setDisplayInfo(RLSysUtil.getDisplayInfo(this));
	}
	displayRotation=getWindowManager().getDefaultDisplay().getRotation();
}

然后根据这个 displayRotationSurfaceView 回调的 surfaceChanged() 方法中去更新 Camera 的 Preview 参数和 Display 方向:

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
	Camera.Parameters params=camera.getParameters(); 
	params.setFlashMode(flashMode);
	params.setPictureFormat(ImageFormat.JPEG);
	switch(displayRotation){
	    case Surface.ROTATION_0:
		    params.setPreviewSize(height, width);
		    camera.setDisplayOrientation(90);
		    break;
	    case Surface.ROTATION_90:
		    params.setPreviewSize(width, height);
			camera.setDisplayOrientation(0);
		    break;
		case Surface.ROTATION_180:
			params.setPreviewSize(height, width);
			camera.setDisplayOrientation(270);
			break;
		case Surface.ROTATION_270:
			params.setPreviewSize(width, height);
			camera.setDisplayOrientation(180);
			break;
	}
	Size size=params.getSupportedPreviewSizes().get(0);
	params.setPreviewSize(size.width, size.height);
	camera.setParameters(params);
	camera.startPreview(); 
}

最后还需要根据这个 displayRotation 去在处理图像数据时翻转一下 Bitmap :

@Override
public void onPictureTaken(byte[] data, Camera camera) {
    Bitmap srcBmp, dstBmp;
    srcBmp=BitmapFactory.decodeByteArray(mData, 0, data.length);
	float degrees=0f;
	switch(displayRotation){
		case Surface.ROTATION_0:
			degrees=90f;
			break;
		case Surface.ROTATION_90:
			degrees=0f;
			break;
		case Surface.ROTATION_180:
			degrees=270f;
			break;
		case Surface.ROTATION_270:
			degrees=180f;
			break;
	}
	Matrix matrix=new Matrix();
	matrix.reset();
	matrix.postRotate(degrees);
	dstBmp=Bitmap.createBitmap(srcBmp, 0, 0, srcBmp.getWidth(), srcBmp.getHeight(), matrix, true);
	if(!srcBmp.isRecycled()){
		srcBmp.recycle();
	}
	//TODO: Compress bitmap to file
	if(!dstBmp.isRecycled()){
		dstBmp.recycle();
	}
}