Android: Camera相机开发详解(中) ——实现预览、拍照、保存照片等功能

android.jpg

前言

  • 在上一篇文章中给小伙伴们介绍了进行Camera开发需要了解的知识点,如果你还没有看过的话,建议先去看上一篇文章《Android: Camera相机开发详解(上) —— 知识储备》

  • 本篇文章会带着小伙伴们一步一步实现自己的Camera,并在实现的过程中验证上一篇中所讲解的结论


实现思路:

  1. 在xml布局中定义一个SurfaceView,用于预览相机采集的数据

  2. 给SurfaceHolder添加回调,在surfaceCreated(holder: SurfaceHolder?)回调中打开相机

  3. 成功打开相机后,设置相机参数。比如:对焦模式,预览大小,照片保存大小等等

  4. 设置相机预览时的旋转角度,然后调用startPreview()开始预览

  5. 调用takePicture方法拍照 或者 是在Camera的预览回调中 保存照片

  6. 对保存的照片进行旋转处理,使其为"自然方向"

  7. 关闭页面,释放相机资源

具体实现步骤:

一丶申请权限

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

二、在xml布局文件中定义一个SurfaceView

 <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

三、创建一个CameraHelper类

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       //保存图片的高

}

由于对Camera的操作等代码比较多,本着各司其职的原则,创建了一个CameraHelper类来处理Camera相关的操作,如果放在Activity中对Camera操作会使Activity臃肿复杂

CameraHelper的构造方法有两个,一个是Activity对象,一个是SurfaceView对象(就是xml文件里定义的SurfaceView)

四、给SurfaceView对象添加回调函数,并初始化相机

    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("打开相机失败!")
                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
    }

在CameraHelper的创建后调用init()方法。在init()方法中,我们首先对mSurfaceHolder添加了一个回调,这个回调会告诉我们SurfaceView中surface的变化(在上一篇上有讲解)

在surfaceCreated(holder: SurfaceHolder?) 回调中打开相机。因为相机开始预览的时候,如果SurfaceView中的surface还没有创建,就回抛出异常,所以我们在surface创建后再对相机进行操作

我们调用相机的open()方法打开一个摄像头,在打开摄像头之前判断一下手机是否支持我们将要打开的摄像头。

五、配置相机参数

    //配置相机参数
    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("相机初始化失败!")
        }
    }

       //获取与指定宽高相等或最接近的尺寸
    private fun getBestSize(targetWidth: Int, targetHeight: Int, sizeList: List<Camera.Size>): 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("系统支持的尺寸 : ${size.width} * ${size.height} ,    比例$supportedRatio")
        }

        for (size in sizeList) {
            if (size.width == targetHeight && size.height == targetWidth) {
                bestSize = size
                break
            }
            var supportedRatio = (size.width.toDouble() / size.height)
            if (Math.abs(supportedRatio - targetRatio) < minDiff) {
                minDiff = Math.abs(supportedRatio - targetRatio)
                bestSize = size
            }
        }
        log("目标尺寸 :$targetWidth * $targetHeight ,   比例  $targetRatio")
        log("最优尺寸 :${bestSize?.height} * ${bestSize?.width}")
        return bestSize
    }

我们对预览大小和保存图片大小进行设置,在设置的时候,我们应该获取到与指定宽高相等或最接近的尺寸,这样的话才能保证图片既不变形又能最接近我们指定的大小。

下面是vivo x9的后置摄像头支持的尺寸:

相机预览大小.png
保存图片的大小.png

六、开始预览

   //开始预览
    fun startPreview() {
        mCamera?.let {
            it.setPreviewDisplay(mSurfaceHolder)         //设置相机预览对象
          //  setCameraDisplayOrientation(mActivity)    //设置预览时相机旋转的角度
            it.startPreview()
        }
    }

调用startPreview()方法开始预览,我们先看一下预览效果:

设置角度前预览效果.jpg

我们可以看到,画面并不是"自然方向"而且被拉伸。这个在上一篇已经讲解过,下面通过setDisplayOrientation(int degree)方法,使其正常显示

    //设置预览旋转的角度
    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 -> screenDegree = 0
            Surface.ROTATION_90 -> screenDegree = 90
            Surface.ROTATION_180 -> screenDegree = 180
            Surface.ROTATION_270 -> 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("屏幕的旋转角度 : $rotation")
        log("setDisplayOrientation(result) : $mDisplayOrientation")
    }

设置后预览效果如下:

设置角度后预览效果.jpg

上一篇提到的相机的预览方向:

相机预览方向.png
后置摄像头预览旋转角度.png
前置摄像头预览旋转角度.png

通过日志我们看到,前后摄像头的预览旋转角度都是90
前置摄像头在进行角度旋转之前,图像会进行一个水平的镜像翻转,所以前置摄像头应该设置的旋转角度是 270 - 180 = 90

七、进行拍照

拍照的话有两种方式:

  • 调用takePicture(ShutterCallback shutter, PictureCallback raw,
    PictureCallback jpeg) 方法

  • 在相机的预览回调里直接保存

1.调用takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) 拍照
    //拍摄照片
    fun takePic() {
        mCamera?.let {
            it.takePicture({}, null, { data, _ ->
                it.startPreview()
                savePic(data)  //保存图片
            })
        }
    }

   //保存照片
    private fun savePic(data: ByteArray?) {
        thread {
            try {
                val temp = System.currentTimeMillis()
                val picFile = FileUtil.createCameraFile()
                if (picFile != null && data != null) {
                   val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
                   Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close()
                    runOnUiThread {
                        toast("图片已保存! ${picFile.absolutePath}")
                        log("图片已保存! 耗时:${System.currentTimeMillis() - temp}    路径:  ${picFile.absolutePath}")
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
                runOnUiThread {
                    toast("保存图片失败!")
                }
            }
        }
    }

takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg)方法有3个参数,而且这3个参数都是抽象接口:

  • 第一个是点击拍照时的回调。
    如果传null,则没有任何效果
    如果写一个空实现,则在点击拍照时会有"咔擦"声

  • 第二个和第三个参数类型一样,PictureCallback 有一个抽象方法
    void onPictureTaken(byte[] data, Camera camera)
    data就是点击拍照后相机返回的照片的byte数组,用该数组创建一个bitmap保存下来,就得到了拍摄的照片

2.在相机的预览回调里直接保存
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
    savePic(data)   //保存照片   
}

注意:实际上这个回调方法会一直一直的调用,如果要保存一张照片的话应该加个字段进行控制,此处只是做演示

在保存图片的时候,我们需要开启一个子线程来进行操作,通过日志输出可以看到保存图片所用时间和保存路径:

保存图片.png

八、调整保存照片的方向

与预览时方向类似,照片在保存时也有一个方向。我们先看一下在上一步中保存的照片是什么样的:

后置摄像头:


后置摄像头拍摄照片.png

前置摄像头:


前置摄像头拍摄照片.png

下面我们在保存图片的时候,对照片进行旋转处理,保存照片的方法应该如下:

   private fun savePic(data: ByteArray?) {
        thread {
            try {
                val temp = System.currentTimeMillis()
                val picFile = FileUtil.createCameraFile()
                if (picFile != null && 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("图片已保存! ${picFile.absolutePath}")
                        log("图片已保存! 耗时:${System.currentTimeMillis() - temp}    路径:  ${picFile.absolutePath}")
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
                runOnUiThread {
                    toast("保存图片失败!")
                }
            }
        }
    }


//图片工具类
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)
    }
|

然后我们在进行一次拍照:

后置摄像头:


后置摄像头拍摄照片旋转后.png

前置摄像头:


前置摄像头拍摄照片旋转后.png

对比一下上一篇文章所讲的相机保存照片的方向:

图六、采集的图像方向.png

关于前置摄像头所拍摄照片,需要注意的是,由于在setDisplayOrientation()设置相机预览方向的时候系统默认做了一个水平镜面的翻转,所以我们通过前置摄像头保存来的照片并不是和预览时看到的一样,两者是水平镜像关系。所以,一般情况下我们不仅仅需要对前置摄像头做旋转,还应该做一个水平方向的镜面翻转处理。

在上面保存图片的方法中判断如果是前置摄像头的话,代码修改如下:

BitmapUtils.mirror(BitmapUtils.rotate(rawBitmap, 270f)) //旋转270,然后水平镜面翻转

这样的话,就能保证所拍摄照片与在预览时所呈现的画面是一模一样的,如下图:

前置摄像头预览与保存一致.png

注:如果有小伙伴对这点还不太理解的话,墙裂建议自己用前置摄像头自拍一张,然后在对比保存的照片与预览时手机里显示的画面,就很容易理解了
不是我不愿意自己自拍来给小伙们演示,长相实在是有点惨,所以大家还是自己亲自验证吧o(╥﹏╥)o

九、释放相机资源

在Activity销毁前或者是关闭相机时,应当释放当前相机资源

   //释放相机
    fun releaseCamera() {
        if (mCamera != null) {
            mCamera?.stopPreview()
            mCamera?.setPreviewCallback(null)
            mCamera?.release()
            mCamera = null
        }
    }

完整效果如下:

拍照效果图.gif

总结

本篇文章主要给小伙伴们介绍了实现Camera拍照功能的流程及步骤,并且用实际效果验证了上一篇文章中所讲解的理论
下一篇文章将会给小伙伴们介绍如何实现人脸检测功能,敬请期待~~

完整代码

https://github.com/smashinggit/Study

注:此工程包含多个module,本文所用代码均在camerademo文件夹下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容