Android相机各个API拍照实践

Android相机各个API拍照实践

前言

好久没更新博客了,过了年基本就没怎么动了,不过还是做了一些东西,最近有时间觉得还是得写一写,不然过段时间就忘了,不划算。

最近把Android Camera的三种API一一试了下,实现了拍照和录像,和图片、bitmap相关的功能也练习了下,比如获取、保存、删除图片等。

下面就来记录下Android三种API的拍照实践。

目标

先来明确下目标,即要做到的效果:

  1. 能够使用Android三种API预览、拍照、显示结果: Camera1、Camera2、CameraX
  2. 三种API能够自由切换,互不干扰
  3. 能够拿到拍照结果,并对图片进行一些操作,如保存、删除等

还是比较简单的,下面就开干。

使用Camera1 API

接口封装

在一段摸索后,我先抽象了一个接口,用来统一三种API的行为:

kotlin 复制代码
import android.graphics.Bitmap
import androidx.activity.ComponentActivity
import androidx.core.util.Consumer

interface ICameraCaptureHelper<in T> {

    /**
     * 使用相机API开始预览
     *
     * @param activity 带lifecycle的activity,提供context,并且便于使用协程
     * @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
     */
    fun startPreview(
        activity: ComponentActivity,
        view: T
    )

    /**
     * 使用相机API拍照
     *
     * @param activity 带lifecycle的activity,提供context,并且便于使用协程
     * @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
     * @param callback 结果回调
     */
    fun takePhoto(
        activity: ComponentActivity,
        view: T,
        callback: Consumer<Bitmap>
    )

    /**
     * 释放资源
     */
    fun release()
}

其实就三个方法,预览、拍照、释放资源,这里我传入了ComponentActivity来获取context,也方便用它的lifecycleScope来使用协程。因为拍照预览可以用SurfaceView、TextureView、PreviewView这几个,我这直接设置成了泛型,看情况用吧。

封装好接口,我们就一步一步实现功能了。

Camera1预览

使用Camera1 API,必须先预览才能拍照,其他API倒没有要求。我这用了SurfaceView来预览,TextureView也可以,下面看下startPreview的写法:

kotlin 复制代码
/**
 * 使用Camera API进行预览
 *
 * @param activity 带lifecycle的activity,提供context,并且便于使用协程
 * @param view Camera API使用的 SurfaceView
 */
override fun startPreview(
    activity: ComponentActivity,
    view: SurfaceView
) {
    // IO协程中执行,
    activity.lifecycleScope.launch(Dispatchers.IO) {

        // 1、获取后置摄像头ID: 默认 Camera.CameraInfo.CAMERA_FACING_BACK
        val cameraId = getCameraId(mFacingType)

        // 2、获取相机实例
        if (mCamera == null) {
            mCamera = Camera.open(cameraId)
        }

        // 3、设置和屏幕方向一致
        setCameraDisplayOrientation(activity, mCamera!!, cameraId)

        // 4、设置相机参数
        setCameraParameters(mCamera!!)

        // 5、在startPreview前设置holder(有前提: surfaceCreated已完成)
        // 不要在surfaceCreated设置,不然有问题,使用工具类没法收到surfaceCreated回调
        mCamera!!.setPreviewDisplay(view.holder)

        // 6、设置SurfaceHolder回调
        view.holder.addCallback(mSurfaceCallback)

        // 7、开始预览
        mCamera!!.startPreview()
    }
}

主要就是这七步,首先要根据摄像头类型获得cameraId,再根据cameraId去打开摄像头,这时候摄像头的方向默认是横着的,还得改下摄像头方向,详细的代码后面会完整提供,现在大致看下流程,

参数设置特别要注意下,这里的width要比height更大,而且要选对尺寸,不然会造成各种意想不到的问题,后面几个API也是一样:

kotlin 复制代码
private fun setCameraParameters(camera: Camera) {
    val params = camera.parameters

    // 设置图像格式
    params.previewFormat = ImageFormat.NV21

    // 设置预览尺寸(注意相机方向是width>height)
    val previewSize = getOptimalPreviewSize(params.supportedPreviewSizes, 1920, 1080)
    params.setPreviewSize(previewSize.width, previewSize.height)

    // 设置图片尺寸
    params.setPictureSize(previewSize.width, previewSize.height)

    // 设置对焦模式
    params.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE

    // 设置闪光灯模式
    params.flashMode = Camera.Parameters.FLASH_MODE_AUTO

    // 设置场景模式
    params.sceneMode = Camera.Parameters.SCENE_MODE_AUTO

    // 应用参数设置
    camera.parameters = params
}

private fun getOptimalPreviewSize(sizes: List<Camera.Size>, w: Int, h: Int): Camera.Size {
    val targetRatio = w.toDouble() / h
    return sizes.minByOrNull { abs(it.width.toDouble() / it.height - targetRatio) } ?: sizes[0]
}

另外一个需要注意的就是mSurfaceCallback,正常使用的话,应该在mSurfaceCallback的surfaceCreated里面去open Camera的,不过我这写成了工具类,调用的时候收不到surfaceCreated,而是已经created了,所以不处理mSurfaceCallback,直接open,当然这里也要根据实际情况去处理。

再一个就是在startPreview前,一定要调用setPreviewDisplay,传入SurfaceView的holder,再通过mCamera去startPreview就可以预览了,预览的时候确保下SurfaceView处于VISIBLE状态,预览就完成了。

Camera1拍照

搞定预览后,拍照功能其实就完成的差不多了,通过mCamera去takePicture就行了

kotlin 复制代码
/**
 * 使用相机API拍照
 *
 * @param activity 带lifecycle的activity,提供context,并且便于使用协程
 * @param view SurfaceView
 * @param callback 结果回调
 */
override fun takePhoto(
    activity: ComponentActivity,
    view: SurfaceView,
    callback: Consumer<Bitmap>
) {
    // camera1 API需要先预览才能拍照
    if (mCamera == null) {
        throw IllegalStateException("camera not prepared!!!")
    }

    // IO协程中执行,
    activity.lifecycleScope.launch(Dispatchers.IO) {
        mCamera!!.takePicture(null, null) { data, _ ->

            // 处理拍照结果
            val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
            val cameraInfo = Camera.CameraInfo()
            Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, cameraInfo)
            val rotation = cameraInfo.orientation

            // 将结果投递到UI线程
            activity.lifecycleScope.launch(Dispatchers.Main) {
                callback.accept(rotateBitmap(bitmap, rotation))
            }
        }
    }
}

private fun rotateBitmap(bitmap: Bitmap, degrees: Int): Bitmap {
    val matrix = Matrix()
    matrix.postRotate(degrees.toFloat())
    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

这里传出的bitmap,并且调整了下方向,没什么好说的。

Camera1释放资源

只会写拍照,没什么好说的,要能正确释放资源,才是一个好程序员,下面看下释放资源:

kotlin 复制代码
/**
 * 释放资源
 */
override fun release() {
    mCamera?.stopPreview()
    mCamera?.release()
    mCamera = null
}

完整代码

上面讲了个大概,下面提供完整代码,加了个takePhotoNoFeeling无感拍照和continuePreview,使用Camera1拍照后,需要调用startPreview继续预览才能再拍照,需要注意下。 (代码有点长,还是去GitHub看吧)

Camera1CaptureHelper

使用Camera2 API

Camera1被标记过时了,Google推荐使用CameraX,CameraX其实用的也是Camera2,只不过Camera2比较难用,但是学习嘛,总得试试,就仿照上面Camera1的写法,下面看下Camera2的使用。

Camera2预览

Camera1中我们用了SurfaceView,这里就来用下TextureView,关于两者区别可以查下资料,这里不详叙,下面看预览代码:

kotlin 复制代码
/**
 * 使用Camera2 API进行预览
 *
 * @param activity 带lifecycle的activity,提供context,并且便于使用协程
 * @param view Camera API2使用的 TextureView(当然也能用SurfaceView)
 */
override fun startPreview(
    activity: ComponentActivity,
    view: TextureView
) {
    // 持有TextureView的弱引用,便于释放资源
    mTextureViewRef = WeakReference(view)

    // IO协程中执行,
    activity.lifecycleScope.launch(Dispatchers.IO) {

        // 1、获取CameraManager
        val cameraManager = ContextCompat.getSystemService(activity, CameraManager::class.java)
            ?: throw IllegalStateException("get cameraManager fail")

        // 2、获取摄像头mCameraId、摄像头信息mCameraCharacteristics
        // 默认 CameraCharacteristics.LENS_FACING_BACK
        chooseCameraIdByFacing(mFacingType, cameraManager)

        // 3、开启相机
        mCameraDevice = openCamera(cameraManager)

        // 4、设置如何读取图片的ImageReader
        mImageReader = getImageReader()

        // 5.创建Capture Session
        val surface = getSurface(view)
        mSession = startCaptureSession(mutableListOf(
            // 注意一定要传入使用到的surface,不然会闪退
            surface,
            mImageReader!!.surface
        ),  mCameraDevice!!)

        // 6.设置textureView回调
        view.surfaceTextureListener = mTextureViewCallback

        // 7.开始预览,预览和拍照都用request实现
        preview(surface)
    }
}

其实吧,和Camera1类似,都要选择相机得到mCameraId,再开启相机,只不过Camera2预览的时候,要要创建Session对话,还要把要输出的surface全部传进去,而且图片要通过ImageReader去读取,这么一搞真就复杂多了。

这里要注意下创建的session,要把预览及ImageReader的surface都传进去,不然就出错了。

kotlin 复制代码
// 5.创建Capture Session
val surface = getSurface(view)
mSession = startCaptureSession(mutableListOf(
    // 注意一定要传入使用到的surface,不然会闪退
    surface,
    mImageReader!!.surface
),  mCameraDevice!!)

另外,这两者的尺寸也要匹配下,我这两个都是用的最大尺寸:

kotlin 复制代码
val map = info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    ?: throw IllegalStateException("Cannot get available preview/video sizes")
val largest = map.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.width * it.height }
mLargestSize = largest ?: throw IllegalStateException("Cannot get largest preview size")

和mSurfaceCallback类似,Camera2的openCamera也应该在mTextureViewCallback的onSurfaceTextureAvailable中调用,只不过我这写成工具类,收不到这个回调,直接就用了。

因为这里有很多回调,我这用了协程和suspend方法,不是必须,只不过能让代码结构更清晰。

Camera2的preview是通过发送重复的请求实现的,其实可以持有previewRequestBuilder,在过程中修改配置,达到想要的效果:

kotlin 复制代码
private fun preview(surface: Surface) {
    // 通过模板创建RequestBuilder
    // CaptureRequest还可以配置很多其他信息,例如图像格式、图像分辨率、传感器控制、闪光灯控制、
    // 3A(自动对焦-AF、自动曝光-AE和自动白平衡-AWB)控制等
    val previewRequestBuilder =
        mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)

    // 设置预览画面
    previewRequestBuilder.addTarget(surface)

    mPreviewRequest = previewRequestBuilder.build()
    mSession!!.setRepeatingRequest(mPreviewRequest!!, null, mHandler)
}

Camera2拍照

看到上面Camera2的预览就挺麻烦了,结果Camera2的拍照也比Camera1来的麻烦:

kotlin 复制代码
/**
 * 使用相机API拍照
 *
 * @param activity 带lifecycle的activity,提供context,并且便于使用协程
 * @param view Camera2 API使用的 TextureView(当然也能用SurfaceView)
 * @param callback 结果回调
 */
override fun takePhoto (
    activity: ComponentActivity,
    view: TextureView,
    callback: Consumer<Bitmap>
) {
    // IO协程中执行,
    activity.lifecycleScope.launch(Dispatchers.IO) {

        // 1、创建拍照的请求
        val captureRequestBuilder =
            mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)

        // 2、设置参数
        captureRequestBuilder.addTarget(mImageReader!!.surface)
        captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)

        // 3、设置拍照方向
        val rotation = activity.windowManager.defaultDisplay.rotation
        captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
            getJpegOrientation(mCameraCharacteristics!!, rotation))

        mCaptureRequest = captureRequestBuilder.build()

        // 4、拍照
        // mSession?.stopRepeating() // 这行代码只是为了防止重复请求
        // mSession?.abortCaptures() // 这行代码只是为了防止重复请求
        mSession!!.capture(mCaptureRequest!!, object : CameraCaptureSession.CaptureCallback() {
            override fun onCaptureCompleted(
                session: CameraCaptureSession,
                request: CaptureRequest,
                result: TotalCaptureResult
            ) {
                // 图片已捕获
                // 可选步骤,根据需要进行处理
            }
        }, mHandler)

        // 5、设置图片回调,拿到结果
        setImageReaderCallback(callback)
    }
}

private fun setImageReaderCallback(callback: Consumer<Bitmap>) {
    mImageReader?.setOnImageAvailableListener({
        val image = mImageReader!!.acquireNextImage()
        image?.use {
            val planes = it.planes
            if (planes.isNotEmpty()) {
                val buffer = planes[0].buffer
                val data = ByteArray(buffer.remaining())
                buffer.get(data)

                // 转成bitmap
                val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)

                // 传递结果,mHandler应该是在UI线程了
                callback.accept(bitmap)
            }
        }
    }, mHandler)
}

这里需要用captureRequestBuilder创建拍照请求,设置好参数,最后通过mSession去拍照,这里能拿到TotalCaptureResult,里面有很多数据,只不过我只想拿bitmap,所以要去mImageReader获取。

Camera2释放资源

Camera2涉及的东西更多,释放资源也更复杂些,需要注意下。

kotlin 复制代码
override fun release() {
    // 从 SurfaceTexture 中移除 SurfaceTextureListener
    mTextureViewRef?.get()?.surfaceTextureListener = null
    // 需要关闭这三个
    mCameraDevice?.close()
    mSession?.close()
    mImageReader?.close()
    mHandler.removeCallbacksAndMessages(null)
}

完整代码

需要注意Android 5.0以后版本才能使用Camera2 API。 Camera2CaptureHelper

使用CameraX API

Camera1 API简单但是过时了,Camera2 API功能强大,使用起来却十分复杂,还好Google在JetPack中提供了CameraX,方便我们使用Camera的相关功能,下面就俩看看吧。

引入CameraX库

CameraX作为JetPack的库,还是需要我们引入库的,我这用的version catalog管理依赖,实际都差不多,就下面几个库(算是把拍照也引入了):

toml 复制代码
# cameraX
camerax = "1.1.0-beta01"

# cameraX
camerax = { module = "androidx.camera:camera-core", version.ref = "camerax"}
camerax_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax"}
camerax_video = { module = "androidx.camera:camera-video", version.ref = "camerax"}
camerax_lifecycler = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax"}
camerax_view = { module = "androidx.camera:camera-view", version.ref = "camerax"}
camerax_extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax"}

这里的cameraX版本并没有用最新的,我试了1.2和1.3版本,需要升级比较高的gradle版本,想想还是算了。

在dependence里面添加上依赖,就能开始写代码了。

kotlin 复制代码
dependencies {
    //。。。

    // CameraX 相关依赖
    implementation(libs.camerax)
    implementation(libs.camerax.camera2)
    implementation(libs.camerax.lifecycler)
    implementation(libs.camerax.video)
    implementation(libs.camerax.view)
    implementation(libs.camerax.extensions)
}

CameraX预览

前面我们分别使用了SurfaceView和TextureView进行预览,而在CameraX里面提供了更好的PreviewView来预览,下面看下CameraX如何预览:

kotlin 复制代码
/**
 * 使用CameraX API进行预览
 *
 * @param activity 带lifecycle的activity,提供context,并且便于使用协程
 * @param view Camera API使用的 PreviewView
 */
override fun startPreview(
    activity: ComponentActivity,
    view: PreviewView
) {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
    cameraProviderFuture.addListener({
        // 用于将相机的生命周期绑定到生命周期所有者
        // 消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力
        mCameraProvider = cameraProviderFuture.get()

        // 预览
        val preview = Preview.Builder()
            .build()
            .also {
                it.setSurfaceProvider(view.surfaceProvider)
            }

        // 拍照的使用场景
        imageCapture = ImageCapture.Builder()
            .build()

        // 选择摄像头,省去了去判断摄像头ID
        // 默认 CameraSelector.DEFAULT_BACK_CAMERA
        val cameraSelector = mSelector

        try {
            // Unbind use cases before rebinding
            mCameraProvider!!.unbindAll()

            // 将相机绑定到 lifecycleOwner,就不用手动关闭了
            mCameraProvider!!.bindToLifecycle(
                activity, cameraSelector, preview, imageCapture)

        } catch(exc: Exception) {
            Log.e("TAG", "Use case binding failed", exc)
        }

        // 回调代码在主线程处理
    }, ContextCompat.getMainExecutor(activity))
}

果然看起来就舒服多了,和Camera2比起来,简单太多了,就是获取了一个mCameraProvider,设置下preview,然后绑定到activity的生命周期就能预览了,根本不需要怎么解释。

如果要拍照,创建个imageCapture,在bindToLifecycle最后加上就行了,这里还帮我们搞定了异步线程问题。

CameraX拍照

CameraX的预览很简单,拍照就更简单了:

kotlin 复制代码
/**
 * 使用相机API拍照
 *
 * @param activity 带lifecycle的activity,提供context,并且便于使用协程
 * @param view Camerax API使用的 PreviewView
 * @param callback 结果回调
 */
override fun takePhoto (
    activity: ComponentActivity,
    view: PreviewView,
    callback: Consumer<Bitmap>
) {
    // Get a stable reference of the modifiable image capture use case
    val imageCapture = imageCapture ?: return

    // 直接拍照拿bitmap,存文件可以用 OutputFileOptions
    imageCapture.takePicture(
        ContextCompat.getMainExecutor(activity),
        object : ImageCapture.OnImageCapturedCallback() {
            override fun onCaptureSuccess(image: ImageProxy) {
                // 转换为 Bitmap,并传递结果
                callback.accept(imageProxyToBitmap(image))
                image.close()
            }

            override fun onError(exc: ImageCaptureException) {
                // 处理拍摄过程中的异常
                Log.e("TAG", "Photo capture failed: ${exc.message}", exc)
            }
        }
    )
}

private fun imageProxyToBitmap(image: ImageProxy): Bitmap {
    val planeProxy = image.planes[0]
    val buffer = planeProxy.buffer

    val bytes = ByteArray(buffer.remaining())
    buffer.get(bytes)

    return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}

直接通过imageCapture的takePicture拍照,拿到image对象就能获取bitmap了,so easy!

CameraX释放资源

CameraX不用了直接解除生命周期的绑定就行了。

kotlin 复制代码
/**
 * 释放资源
 */
override fun release() {
    // 取消绑定生命周期观察者
    mCameraProvider?.unbindAll()
}

完整代码

CameraXCaptureHelper

使用Demo

我这写了个例子,用了这三个API,还加上了系统拍照、系统选取、系统分享、保存相册等,有兴趣可以参考下:

Demo地址:

TakePhotoFragment

小结

花了点时间,把Android相机中Camera1、Camera2、CameraX三个API的拍照功能实践了下,并编写成工具类方便使用。

相关推荐
拭心11 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王13 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡13 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道14 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库15 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道15 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe15 小时前
Android Hook - 动态加载so库
android
居居飒16 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He19 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗19 小时前
Android笔试面试题AI答之Android基础(1)
android