写在前面 本文并不是基于Camera2的,所以想要了解Camera2的同学可以先散了。文题加了详记二字,因为相机整个打开的流程的确是比较复杂的,稍有疏忽可能就会引发一系列问题。我也是看了一下Android的文档才整理了这篇文章,想看原文的戳这 。不得不说,文档还是详细啊~
本文主要会涉及以下内容:
先放一下最终效果图吧,做的比较简单,各位不用担心:
主要功能就是拍照保存,多的也没啥了,项目地址在文末有。
使用流程 在详细的研究相机之前,首先熟悉一下使用相机的整个流程:
检测和访问相机:创建代码检测相机的存在和请求访问
创建预览类:创建继承自SurfaceView和实现SurfaceHolder接口的预览类。这个类展示来自相机的实时图片
创建一个预览布局:如果你有相机预览类,你就需要创建一个和用户交互的界面布局
为拍照或者录像设置监听:对用户的行为作出响应,你要为你的控件设置监听去开始拍照或者录像,比如你设置了一个拍照的按钮,用户点击之后就要开始拍照。监听用户行为只是其一,还有就是拍照的监听,这个放到后文讨论
捕获和保存文件:无论是拍照还是录像,都需要有保存的功能
释放相机:在不使用相机时候,你的应用一定要释放相机。
那么为什么一定要释放相机资源呢?因为相机硬件是一个共享临界资源,不仅你的应用会使用,其他的应用也会使用相机。所以在不用相机的时候,一定要释放相机,不然你自己的应用和后续其他要使用相机的应用都无法使用相机。
流程大致就是这样,接下来一步一步的跟进,看看这个相机到底特么的是怎么用的。
申请权限 Android 6.0之前 & targetSdkVersion < 23 只需要在清单文件中声明一下权限就行
1 2 <uses-permission android:name ="android.permission.CAMERA" /> <uses-feature android:name ="android.hardware.camera" />
上面那个uses-feature,文档中说如果声明了这个,在Google Play中会阻止没有相机的设备下载你的应用。国内的应用商店就不知道了= =。在Android 6.0之后且targetSdkVersion >= 23就需要申请相机权限了。我们可以在Activity的onResume中检测是否拥有相机权限,如果拥有权限就进行下一步的操作,如果没有就申请权限,并在回调中检测是否申请成功,成功的话就进行下一步操作。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override protected void onResume () { super .onResume(); if (ContextCompat.checkSelfPermission(this , Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this , new String[]{Manifest.permission.CAMERA}, 1 ); } else { startCameraPre(); } } @Override public void onRequestPermissionsResult (int requestCode, @NonNull String[] permissions, @NonNull int [] grantResults) { super .onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 1 && grantResults[0 ] == PackageManager.PERMISSION_GRANTED) { startCameraPre(); } else { Toast.makeText(this , "您已拒绝打开相机,想要使用此功能请手动打开相机权限" , Toast.LENGTH_SHORT).show(); } }
检测相机是否存在 在使用之前需要先检测一下设备是否有相机:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static boolean checkCameraHardware (Context context) { if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) { Log.i(TAG, "有相机" ); return true ; } else { Log.i(TAG, "没有相机" ); return false ; } }
代码比较简单,比较坑爹的是什么呢?有一些嵌入式设备的Android系统通过这个方法是无法获取到到底特么的是有没有相机的。获取的可能是错误的信息,我在我们的设备上用这个代码检测,明明没有相机也判断成有相机了。如果一定要判断……还是有办法的,直接Camera.open试一试,成功就说明有,失败就……就失败了呗。
访问相机 访问相机的代码比较简单,就是通过Camera.open方法拿到一个Camera的实例,需要注意的是这个open方法可能会引发异常,最好还是要try catch一下的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static Camera getCameraInstance () { Camera c; try { c = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); } catch (Exception e) { e.printStackTrace(); Log.e(TAG, "相机打开失败,正在使用或者不存在,或者,没有权限?" ); return null ; } return c; }
Android 2.3版本以后可以通过Camera.open(int)来打开指定的相机,我这里打开了后置摄像头。
创建预览类 预览类就是用来播放相机画面的类,预览类是继承自SurfaceView的。普通的View以及其子类都是共享同一个surface的,所有的绘制都必须在UI线程进行。而SurfaceView是一种比较特殊的view,他并不与其他view共享surface,而是在内部持有了一个独立的surface,SurfaceView负责管理这个surface的格式、尺寸以及显示位置。由于UI线程还要同事处理其他交互逻辑,因此对View的更新速度和帧率无法保证,而surfaceview由于持有一个独立的surface,因而可以在独立的线程中进行绘制,因此可以提供更高的帧率。自定义相机的预览图像由于对更新速度和帧率要求比较高,所以比较适合用surfaceview来显示。(关于surfaceview的介绍摘自Android自定义相机详细讲解 )
介绍了这么多,对SurfaceView大概有个了解就可以了,这个类和相机的声明周期息息相关,需要实现SurfaceHolder.Callback接口来接收View创建和销毁的事件。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 package com.xiasuhuei321.cameradieorme.camera;import android.content.Context;import android.hardware.Camera;import android.os.Environment;import android.os.Handler;import android.os.Looper;import android.text.format.DateFormat;import android.util.Log;import android.view.SurfaceHolder;import android.view.SurfaceView;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;public class CameraPreview extends SurfaceView implements SurfaceHolder .Callback , Camera .AutoFocusCallback , Camera .PictureCallback { public static final String TAG = "CameraPreview" ; public static final String DIRNAME = "MyCamera" ; private SurfaceHolder mHolder; private Camera mCamera; private boolean canTake = false ; private Context context; public CameraPreview (Context context) { super (context); this .context = context; mHolder = getHolder(); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); Log.i(TAG, "CameraPreview被创建 " + this .hashCode()); } public void setCamera (Camera mCamera) { this .mCamera = mCamera; mHolder.addCallback(this ); surfaceCreated(getHolder()); Log.i(TAG, "serCamera" + " release = " + CameraUtil.getInstance().isRelease()); } @Override public void surfaceCreated (SurfaceHolder holder) { Log.i(TAG, "surface view被创建" ); if (CameraUtil.getInstance().isRelease()) return ; try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); } catch (IOException e) { Log.d(TAG, "Error setting camera preview: " + e.getMessage()); } } @Override public void surfaceDestroyed (SurfaceHolder holder) { Log.i(TAG, "surface 被销毁 " ); holder.removeCallback(this ); mCamera.setPreviewCallback(null ); mCamera.stopPreview(); mCamera.lock(); CameraUtil.getInstance().releaseCamera(); mCamera = null ; } @Override public void surfaceChanged (SurfaceHolder holder, int format, int w, int h) { if (mHolder.getSurface() == null ) { return ; } try { mCamera.stopPreview(); } catch (Exception e) { } try { mCamera.setPreviewDisplay(mHolder); mCamera.startPreview(); } catch (Exception e) { Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } } public void takePhoto () { canTake = true ; mCamera.autoFocus(this ); } @Override public void onAutoFocus (boolean success, final Camera camera) { Log.i(TAG, "聚焦: " + canTake); if (canTake) { camera.takePicture(null , null , CameraPreview.this ); } canTake = false ; new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { @Override public void run () { mCamera.startPreview(); } }, 1000 ); } @Override public void onPictureTaken (final byte [] data, Camera camera) { Log.i(TAG, "onPictureTaken" ); new Thread(new Runnable() { @Override public void run () { saveToSd(data); } }).start(); } private void saveToSd (byte [] data) { long dateTaken = System.currentTimeMillis(); String fileName = DateFormat.format("yyyy-MM-dd kk.mm.ss" , dateTaken).toString() + ".jpg" ; FileOutputStream fos = null ; if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { String filePath = Environment.getExternalStorageDirectory() + File.separator + DIRNAME + File.separator + fileName; Log.i(TAG, "文件路径:" + filePath); File imgFile = new File(filePath); if (!imgFile.getParentFile().exists()) { imgFile.getParentFile().mkdirs(); } try { if (!imgFile.exists()) { imgFile.createNewFile(); } fos = new FileOutputStream(imgFile); fos.write(data); fos.flush(); insertIntoMediaPic(); } catch (Exception e) { } finally { try { if (fos != null ) { fos.close(); } } catch (Exception e) { e.printStackTrace(); } } } else { insertIntoMediaPic(); } } private void insertIntoMediaPic () { } }
这个类可以说是字字血泪,各位看的时候可以结合注释看……每一个我被坑过的地方我都详细的注释了出来。真的是都在代码里了。关于图片处理那一块我是没什么比较好的办法,内存所限,在拿到byte[] data 这个图片数据数组,我直接在转成Bitmap那一步就OOM了,后来看了一下我这里选取的是4160 * 2340的分辨率,直接写入文件一张图也有4~5M,这个时候的问题就是生成一个Bitmap需要申请很大的内存,而原来的data数组因为这个方法还没结束也无法释放(Java参数传递是引用拷贝传递,所以这时候依然有引用指向内存中的data对象,GC无法回收这块内存),所以就算后续你不额外申请内存,有方法在原有的Bitmap对象上进行操作,也是不行的,因为在生成的时候就OOM了。
创建预览布局 这里Android文档中的建议是在布局中放置一个FrameLayout作为相机预览类的父容器,我采用了这种做法,布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:layout_width ="fill_parent" android:layout_height ="fill_parent" android:orientation ="horizontal" > <FrameLayout android:id ="@+id/camera_preview" android:layout_width ="match_parent" android:layout_height ="match_parent" /> <ImageView android:id ="@+id/iv_take" android:layout_width ="80dp" android:layout_height ="80dp" android:layout_alignParentBottom ="true" android:layout_centerHorizontal ="true" android:layout_marginBottom ="16dp" android:src ="@drawable/takephoto" /> </RelativeLayout >
布局比较简单,就一个FrameLayout和一个ImageView,点击ImageView开始拍照。
接下来看一下Activity的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 package com.xiasuhuei321.cameradieorme.camera;import android.Manifest;import android.animation.ObjectAnimator;import android.content.pm.PackageManager;import android.hardware.Camera;import android.os.Bundle;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.support.v4.app.ActivityCompat;import android.support.v4.content.ContextCompat;import android.support.v7.app.AppCompatActivity;import android.view.MotionEvent;import android.view.View;import android.view.Window;import android.view.WindowManager;import android.widget.FrameLayout;import android.widget.Toast;import com.xiasuhuei321.cameradieorme.R;public class CameraActivity extends AppCompatActivity { private Camera camera; private FrameLayout preview; private CameraPreview mPreview; @Override protected void onCreate (@Nullable Bundle savedInstanceState) { super .onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_camera); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); View iv_take = findViewById(R.id.iv_take); final ObjectAnimator scaleX = ObjectAnimator.ofFloat(iv_take, "scaleX" , 1f , 0.8f ); final ObjectAnimator scaleY = ObjectAnimator.ofFloat(iv_take, "scaleY" , 1f , 0.8f ); iv_take.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch (View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: v.setScaleX(0.9f ); v.setScaleY(0.9f ); scaleX.start(); scaleY.start(); break ; case MotionEvent.ACTION_UP: v.setScaleX(1f ); v.setScaleY(1f ); scaleX.reverse(); scaleY.reverse(); break ; } return false ; } }); iv_take.setOnClickListener(new View.OnClickListener() { @Override public void onClick (View v) { mPreview.takePhoto(); } }); mPreview = new CameraPreview(this ); CameraUtil.getInstance().init(this ); } @Override protected void onResume () { super .onResume(); if (ContextCompat.checkSelfPermission(this , Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this , new String[]{Manifest.permission.CAMERA}, 1 ); } else { startCameraPre(); } } @Override public void onRequestPermissionsResult (int requestCode, @NonNull String[] permissions, @NonNull int [] grantResults) { super .onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 1 && grantResults[0 ] == PackageManager.PERMISSION_GRANTED) { startCameraPre(); } else { Toast.makeText(this , "您已拒绝打开相机,想要使用此功能请手动打开相机权限" , Toast.LENGTH_SHORT).show(); } } private void startCameraPre () { if (CameraUtil.checkCameraHardware(this )) { camera = CameraUtil.getInstance().getCameraInstance(); } mPreview.setCamera(camera); preview = (FrameLayout) findViewById(R.id.camera_preview); if (preview.getChildCount() == 0 ) preview.addView(mPreview); } }
在开始的时候写了两个属性动画,用户在点击的时候有点交互的感觉(貌似并没有什么luan用)。在onResume中检查是否拥有权限打开相机,因为6.0以上需要动态申请啊,蛋疼。拥有权限或者用户给了权限就执行startCameraPre方法,这个方法通过我自己写的CameraUtil获取并初始化了一个Camera实例。并且最后判断FrameLayout中是否有子View,如果没有就将我们自己的相机预览类添加进去。这样打开相机和拍照的整个流程就完成了,当然了,最后还得贴一下CameraUtil代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 package com.xiasuhuei321.cameradieorme.camera;import android.content.Context;import android.content.pm.PackageManager;import android.graphics.PixelFormat;import android.graphics.Point;import android.hardware.Camera;import android.util.Log;import android.view.WindowManager;import java.util.List;public class CameraUtil { public static final String TAG = "CameraUtil" ; private Camera camera; private int cameraId; private int mScreenWidth; private int mScreenHeight; private boolean release = false ; private Camera.Parameters params; private CameraUtil () { } private static class CameraUtilHolder { private static CameraUtil instance = new CameraUtil(); } public static CameraUtil getInstance () { return CameraUtilHolder.instance; } public void init (Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Point p = new Point(); wm.getDefaultDisplay().getSize(p); mScreenWidth = p.x; mScreenHeight = p.y; } public static boolean checkCameraHardware (Context context) { if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) { return true ; } else { return false ; } } public Camera getCameraInstance () { if (camera != null ) { Log.i(TAG, "camera已经打开过,返回前一个值" ); return camera; } try { camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); cameraId = Camera.CameraInfo.CAMERA_FACING_BACK; } catch (Exception e) { e.printStackTrace(); Log.i(TAG, "相机打开失败,正在使用或者不存在,或者,没有权限?" ); return null ; } initParam(); release = false ; return camera; } public void initParam () { if (camera == null ) { return ; } if (params != null ) { camera.setParameters(params); } else { camera.setParameters(generateDefaultParams(camera)); } } public void setParams (Camera.Parameters params) { this .params = params; } public Camera.Parameters generateDefaultParams (Camera camera) { Camera.Parameters parameters = camera.getParameters(); if (parameters.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); } camera.cancelAutoFocus(); parameters.setPictureFormat(PixelFormat.JPEG); parameters.setJpegQuality(100 ); if (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK) { camera.setDisplayOrientation(90 ); parameters.setRotation(90 ); } else if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { camera.setDisplayOrientation(270 ); parameters.setRotation(180 ); } List<Camera.Size> picSizeList = parameters.getSupportedPictureSizes(); for (Camera.Size size : picSizeList) { Log.i(TAG, "pictureSizeList size.width=" + size.width + " size.height=" + size.height); } Camera.Size picSize = getProperSize(picSizeList, ((float ) mScreenHeight / mScreenWidth)); parameters.setPictureSize(picSize.width, picSize.height); List<Camera.Size> previewSizeList = parameters.getSupportedPreviewSizes(); for (Camera.Size size : previewSizeList) { Log.i(TAG, "previewSizeList size.width=" + size.width + " size.height=" + size.height); } Camera.Size preSize = getProperSize(previewSizeList, ((float ) mScreenHeight) / mScreenWidth); Log.i(TAG, "final size is: " + picSize.width + " " + picSize.height); if (null != preSize) { Log.i(TAG, "preSize.width=" + preSize.width + " preSize.height=" + preSize.height); parameters.setPreviewSize(preSize.width, preSize.height); } return parameters; } private Camera.Size getProperSize (List<Camera.Size> pictureSizeList, float screenRatio) { Log.i(TAG, "screenRatio=" + screenRatio); Camera.Size result = null ; for (Camera.Size size : pictureSizeList) { float currentRatio = ((float ) size.width) / size.height; if (currentRatio - screenRatio == 0 ) { result = size; break ; } } if (null == result) { for (Camera.Size size : pictureSizeList) { float curRatio = ((float ) size.width) / size.height; if (curRatio == 4f / 3 ) { result = size; break ; } } } return result; } public void releaseCamera () { if (camera != null ) { camera.release(); } camera = null ; release = true ; } public boolean isRelease () { return release; } }
这里需要注意的就是生成相机参数那一块了,Android中的相机默认是横向的,我们平时用的时候肯定不是那么用的,所以通过 camera.setDisplayOrientation(90)
旋转90度调整一下。不过设置了这个之后,如果不设置parameters.setRotation(90)
那么保存的图片方向也不对,设置了这个之后就可以了。不过我看网上很多都是采用自己生成Bitmap然后自己旋转……如果parameters.setRotation(90)
这种方式可以完成的话,最好不要采用自己处理的方式了,内存开销太大了。关于资源的释放啊什么的,都在预览类里面注释写好了= = ,这里就不再赘述了。
小结 最开始研究相机是因为项目里一个用到相机三方总是报错,在有空研究了一下相机之后,添了一行代码,测试到现在还算比较稳定,没有出现崩溃了,有的时候真的是,一行代码能改变的东西却是非常多的。跑题了跑题了,现在突然感觉相机可以玩的东西很多……以后这个demo可能会继续完善
代码地址:https://github.com/ForgetAll/CameraDieOrMe
参考资料