android智能终端SDK

移动支付产品接入文档


Camera相机开发

<p>摄像头作为Android系统的标准功能,这里摘抄简书上的一个例子来简单介绍</p> <p>[Android: Camera相机开发详解](<a href="https://www.jianshu.com/p/e20a2ad6ad9a">https://www.jianshu.com/p/e20a2ad6ad9a</a> &quot;Android: Camera相机开发详解&quot;)</p> <p>实现思路:</p> <p>在xml布局中定义一个SurfaceView,用于预览相机采集的数据 给SurfaceHolder添加回调,在surfaceCreated(holder: SurfaceHolder?)回调中打开相机 成功打开相机后,设置相机参数。比如:对焦模式,预览大小,照片保存大小等等 设置相机预览时的旋转角度,然后调用startPreview()开始预览 调用takePicture方法拍照 或者 是在Camera的预览回调中 保存照片 对保存的照片进行旋转处理,使其为&quot;自然方向&quot; 关闭页面,释放相机资源</p> <p>具体实现步骤: 一丶申请权限</p> <pre><code class="language-java">&amp;lt;uses-permission android:name=&amp;quot;android.permission.CAMERA&amp;quot; /&amp;amp; &amp;lt;uses-permission android:name=&amp;quot;android.permission.WRITE_EXTERNAL_STORAGE&amp;quot; /&amp;amp;</code></pre> <p>二、在xml布局文件中定义一个SurfaceView</p> <pre><code class="language-java"> &amp;lt;SurfaceView android:id=&amp;quot;@+id/surfaceView&amp;quot; android:layout_width=&amp;quot;match_parent&amp;quot; android:layout_height=&amp;quot;match_parent&amp;quot; /&amp;amp;</code></pre> <p>三、创建一个CameraHelper类</p> <pre><code class="language-java">class CameraHelper(activity: Activity, surfaceView: SurfaceView) : Camera.PreviewCallback { private var mCamera: Camera? = null //Camera对象 private lateinit var mParameters: Camera.Parameters //Camera对象的参数 private var mSurfaceView: SurfaceView = surfaceView //用于预览的SurfaceView对象 var mSurfaceHolder: SurfaceHolder //SurfaceHolder对象 private var mActivity: Activity = activity private var mCallBack: CallBack? = null //自定义的回调 var mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK //摄像头方向 var mDisplayOrientation: Int = 0 //预览旋转的角度 private var picWidth = 2160 //保存图片的宽 private var picHeight = 3840 //保存图片的高 }</code></pre> <p>由于对Camera的操作等代码比较多,本着各司其职的原则,创建了一个CameraHelper类来处理Camera相关的操作,如果放在Activity中对Camera操作会使Activity臃肿复杂 CameraHelper的构造方法有两个,一个是Activity对象,一个是SurfaceView对象(就是xml文件里定义的SurfaceView) 四、给SurfaceView对象添加回调函数,并初始化相机</p> <pre><code class="language-java"> private fun init() { mSurfaceHolder.addCallback(object : SurfaceHolder.Callback { override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) { } override fun surfaceDestroyed(holder: SurfaceHolder?) { releaseCamera() //释放相机资源 } override fun surfaceCreated(holder: SurfaceHolder?) { //surface创建 if (mCamera == null) { openCamera(mCameraFacing) //打开相机 } startPreview() //开始预览 } }) } //打开相机 private fun openCamera(cameraFacing: Int = Camera.CameraInfo.CAMERA_FACING_BACK): Boolean { var supportCameraFacing = supportCameraFacing(cameraFacing) //判断手机是否支持前置/后置摄像头 if (supportCameraFacing) { try { mCamera = Camera.open(cameraFacing) initParameters(mCamera!!) //初始化相机配置信息 mCamera?.setPreviewCallback(this) } catch (e: Exception) { e.printStackTrace() toast(&amp;quot;打开相机失败!&amp;quot;) return false } } return supportCameraFacing } //判断是否支持某个相机 private fun supportCameraFacing(cameraFacing: Int): Boolean { var info = Camera.CameraInfo() for (i in 0 until Camera.getNumberOfCameras()) { Camera.getCameraInfo(i, info) if (info.facing == cameraFacing) return true } return false }</code></pre> <p>在CameraHelper的创建后调用init()方法。在init()方法中,我们首先对mSurfaceHolder添加了一个回调,这个回调会告诉我们SurfaceView中surface的变化(在上一篇上有讲解) 在surfaceCreated(holder: SurfaceHolder?) 回调中打开相机。因为相机开始预览的时候,如果SurfaceView中的surface还没有创建,就回抛出异常,所以我们在surface创建后再对相机进行操作 我们调用相机的open()方法打开一个摄像头,在打开摄像头之前判断一下手机是否支持我们将要打开的摄像头。 五、配置相机参数</p> <pre><code class="language-java">//配置相机参数 private fun initParameters(camera: Camera) { try { mParameters = camera.parameters mParameters.previewFormat = ImageFormat.NV21 //设置预览图片的格式 //获取与指定宽高相等或最接近的尺寸 //设置预览尺寸 val bestPreviewSize = getBestSize(mSurfaceView.width, mSurfaceView.height, mParameters.supportedPreviewSizes) bestPreviewSize?.let { mParameters.setPreviewSize(it.width, it.height) } //设置保存图片尺寸 val bestPicSize = getBestSize(picWidth, picHeight, mParameters.supportedPictureSizes) bestPicSize?.let { mParameters.setPictureSize(it.width, it.height) } //对焦模式 if (isSupportFocus(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) mParameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE camera.parameters = mParameters } catch (e: Exception) { e.printStackTrace() toast(&amp;quot;相机初始化失败!&amp;quot;) } } //获取与指定宽高相等或最接近的尺寸 private fun getBestSize(targetWidth: Int, targetHeight: Int, sizeList: List&amp;lt;Camera.Size&amp;amp;): Camera.Size? { var bestSize: Camera.Size? = null var targetRatio = (targetHeight.toDouble() / targetWidth) //目标大小的宽高比 var minDiff = targetRatio for (size in sizeList) { var supportedRatio = (size.width.toDouble() / size.height) log(&amp;quot;系统支持的尺寸 : ${size.width} * ${size.height} , 比例$supportedRatio&amp;quot;) } for (size in sizeList) { if (size.width == targetHeight &amp;amp;amp;&amp;amp;amp; size.height == targetWidth) { bestSize = size break } var supportedRatio = (size.width.toDouble() / size.height) if (Math.abs(supportedRatio - targetRatio) &amp;lt; minDiff) { minDiff = Math.abs(supportedRatio - targetRatio) bestSize = size } } log(&amp;quot;目标尺寸 :$targetWidth * $targetHeight , 比例 $targetRatio&amp;quot;) log(&amp;quot;最优尺寸 :${bestSize?.height} * ${bestSize?.width}&amp;quot;) return bestSize }</code></pre> <p>我们对预览大小和保存图片大小进行设置,在设置的时候,我们应该获取到与指定宽高相等或最接近的尺寸,这样的话才能保证图片既不变形又能最接近我们指定的大小。</p> <p>六、开始预览</p> <pre><code class="language-java">//开始预览 fun startPreview() { mCamera?.let { it.setPreviewDisplay(mSurfaceHolder) //设置相机预览对象 // setCameraDisplayOrientation(mActivity) //设置预览时相机旋转的角度 it.startPreview() } }</code></pre> <p>调用startPreview()方法开始预览,我们先看一下预览效果: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce9823e27c6.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=ae78028607e45a78ce29103669830422&amp;amp;file=file.png" alt="" /></p> <p>我们可以看到,画面并不是&quot;自然方向&quot;而且被拉伸。这个在上一篇已经讲解过,下面通过setDisplayOrientation(int degree)方法,使其正常显示</p> <pre><code class="language-java">//设置预览旋转的角度 private fun setCameraDisplayOrientation(activity: Activity) { var info = Camera.CameraInfo() Camera.getCameraInfo(mCameraFacing, info) val rotation = activity.windowManager.defaultDisplay.rotation var screenDegree = 0 when (rotation) { Surface.ROTATION_0 -&amp;amp; screenDegree = 0 Surface.ROTATION_90 -&amp;amp; screenDegree = 90 Surface.ROTATION_180 -&amp;amp; screenDegree = 180 Surface.ROTATION_270 -&amp;amp; screenDegree = 270 } if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { mDisplayOrientation = (info.orientation + screenDegree) % 360 mDisplayOrientation = (360 - mDisplayOrientation) % 360 // compensate the mirror } else { mDisplayOrientation = (info.orientation - screenDegree + 360) % 360 } mCamera?.setDisplayOrientation(mDisplayOrientation) log(&amp;quot;屏幕的旋转角度 : $rotation&amp;quot;) log(&amp;quot;setDisplayOrientation(result) : $mDisplayOrientation&amp;quot;) }</code></pre> <p>设置后预览效果如下: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce981ba940a.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=94acff17640c24205f6ede4db1016cf9&amp;amp;file=file.png" alt="" /></p> <p>上一篇提到的相机的预览方向: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce9815b4aff.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=2172076a57540ebd9b58e13ba55a2750&amp;amp;file=file.png" alt="" /></p> <p><img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce980fcb90f.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=8b7bfcfc0ade5bd821aa7f0cb8abdb9a&amp;amp;file=file.png" alt="" /></p> <p><img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce980986e45.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=b1bfb0b515e72831827f340d847c58a6&amp;amp;file=file.png" alt="" /></p> <p>通过日志我们看到,前后摄像头的预览旋转角度都是90 前置摄像头在进行角度旋转之前,图像会进行一个水平的镜像翻转,所以前置摄像头应该设置的旋转角度是 270 - 180 = 90</p> <p>七、进行拍照 拍照的话有两种方式:</p> <p>调用takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) 方法 在相机的预览回调里直接保存</p> <p>1.调用takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) 拍照</p> <pre><code class="language-java">//拍摄照片 fun takePic() { mCamera?.let { it.takePicture({}, null, { data, _ -&amp;amp; it.startPreview() savePic(data) //保存图片 }) } } //保存照片 private fun savePic(data: ByteArray?) { thread { try { val temp = System.currentTimeMillis() val picFile = FileUtil.createCameraFile() if (picFile != null &amp;amp;amp;&amp;amp;amp; data != null) { val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size) Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close() runOnUiThread { toast(&amp;quot;图片已保存! ${picFile.absolutePath}&amp;quot;) log(&amp;quot;图片已保存! 耗时:${System.currentTimeMillis() - temp} 路径: ${picFile.absolutePath}&amp;quot;) } } } catch (e: Exception) { e.printStackTrace() runOnUiThread { toast(&amp;quot;保存图片失败!&amp;quot;) } } } }</code></pre> <p>takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg)方法有3个参数,而且这3个参数都是抽象接口:</p> <p>第一个是点击拍照时的回调。 如果传null,则没有任何效果 如果写一个空实现,则在点击拍照时会有&quot;咔擦&quot;声 第二个和第三个参数类型一样,PictureCallback 有一个抽象方法 void onPictureTaken(byte[] data, Camera camera) data就是点击拍照后相机返回的照片的byte数组,用该数组创建一个bitmap保存下来,就得到了拍摄的照片</p> <p>2.在相机的预览回调里直接保存</p> <pre><code class="language-java">override fun onPreviewFrame(data: ByteArray?, camera: Camera?) { savePic(data) //保存照片 }</code></pre> <p>注意:实际上这个回调方法会一直一直的调用,如果要保存一张照片的话应该加个字段进行控制,此处只是做演示</p> <p>在保存图片的时候,我们需要开启一个子线程来进行操作,通过日志输出可以看到保存图片所用时间和保存路径: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce97fb99f19.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=371334383691bcd0b3c24002861530f8&amp;amp;file=file.png" alt="" /></p> <p>八、调整保存照片的方向 与预览时方向类似,照片在保存时也有一个方向。我们先看一下在上一步中保存的照片是什么样的: 后置摄像头: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce97f51c0c6.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=98e2549091e7b2090c9251bbc246b671&amp;amp;file=file.png" alt="" /></p> <p>前置摄像头: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce97ed7f380.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=05932470f0c8b88f8cc97326889b42e5&amp;amp;file=file.png" alt="" /></p> <p>下面我们在保存图片的时候,对照片进行旋转处理,保存照片的方法应该如下:</p> <pre><code class="language-java">private fun savePic(data: ByteArray?) { thread { try { val temp = System.currentTimeMillis() val picFile = FileUtil.createCameraFile() if (picFile != null &amp;amp;amp;&amp;amp;amp; data != null) { val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size) val resultBitmap = if (mCameraHelper.mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) BitmapUtils.rotate(rawBitmap, 270f) //前置摄像头旋转270° else BitmapUtils.rotate(rawBitmap, 90f) //后置摄像头旋转90° Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close() runOnUiThread { toast(&amp;quot;图片已保存! ${picFile.absolutePath}&amp;quot;) log(&amp;quot;图片已保存! 耗时:${System.currentTimeMillis() - temp} 路径: ${picFile.absolutePath}&amp;quot;) } } } catch (e: Exception) { e.printStackTrace() runOnUiThread { toast(&amp;quot;保存图片失败!&amp;quot;) } } } } //图片工具类 object BitmapUtils { //水平镜像翻转 fun mirror(rawBitmap: Bitmap): Bitmap { var matrix = Matrix() matrix.postScale(-1f, 1f) return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true) } //旋转 fun rotate(rawBitmap: Bitmap, degree: Float): Bitmap { var matrix = Matrix() matrix.postRotate(degree) return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true) }</code></pre> <p>然后我们在进行一次拍照: 后置摄像头: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce97da684cf.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=0f1ab75641da05a83182734623be48dd&amp;amp;file=file.png" alt="" /></p> <p>前置摄像头: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce97d5014ba.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=6129a5c9f8aed2a7f9de1311fad2dc1e&amp;amp;file=file.png" alt="" /></p> <p>对比一下上一篇文章所讲的相机保存照片的方向: <img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce97cb2b3de.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=65d7b68785520a1660d75860b0963a81&amp;amp;file=file.png" alt="" /></p> <p>关于前置摄像头所拍摄照片,需要注意的是,由于在setDisplayOrientation()设置相机预览方向的时候系统默认做了一个水平镜面的翻转,所以我们通过前置摄像头保存来的照片并不是和预览时看到的一样,两者是水平镜像关系。所以,一般情况下我们不仅仅需要对前置摄像头做旋转,还应该做一个水平方向的镜面翻转处理。</p> <p>在上面保存图片的方法中判断如果是前置摄像头的话,代码修改如下: BitmapUtils.mirror(BitmapUtils.rotate(rawBitmap, 270f)) //旋转270,然后水平镜面翻转</p> <p>这样的话,就能保证所拍摄照片与在预览时所呈现的画面是一模一样的,如下图:</p> <p><img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce97c45160c.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=4f060de35451ef11f659b4a46f3c0ec3&amp;amp;file=file.png" alt="" /></p> <p>注:如果有小伙伴对这点还不太理解的话,墙裂建议自己用前置摄像头自拍一张,然后在对比保存的照片与预览时手机里显示的画面,就很容易理解了 不是我不愿意自己自拍来给小伙们演示,长相实在是有点惨,所以大家还是自己亲自验证吧o(╥﹏╥)o</p> <p>九、释放相机资源 在Activity销毁前或者是关闭相机时,应当释放当前相机资源</p> <pre><code class="language-java">//释放相机 releaseCamera() { if (mCamera != null) { mCamera?.stopPreview() mCamera?.setPreviewCallback(null) mCamera?.release() mCamera = null } }</code></pre> <p>完整效果如下:</p> <p><img src="http://doc.szzkc.com/Public/Uploads/2019-05-05/5cce963f5c36e.png" alt="" /> <img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=11de8bdc2f044e79a1ba7e80c907791d&amp;amp;file=file.png" alt="" /></p> <p>总结 本篇文章主要给小伙伴们介绍了实现Camera拍照功能的流程及步骤,并且用实际效果验证了上一篇文章中所讲解的理论 下一篇文章将会给小伙伴们介绍如何实现人脸检测功能,敬请期待~~ 完整代码 <a href="https://github.com/smashinggit/Study">https://github.com/smashinggit/Study</a></p>

页面列表

ITEM_HTML