解决 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;
}
});
然后就是拍照了,直接调用 Camera
的 takePicture(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)
多了个 postview
的 PictureCallback
类型回调,具体用法还不清楚,以后再研究。
在这里预览的时候应该就能发现问题:方向不对,相差 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();
}
然后根据这个 displayRotation
在 SurfaceView
回调的 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();
}
}