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的拍照功能实践了下,并编写成工具类方便使用。

相关推荐
LSL666_1 小时前
5 Repository 层接口
android·运维·elasticsearch·jenkins·repository
alexhilton5 小时前
在Jetpack Compose中创建CRT屏幕效果
android·kotlin·android jetpack
2501_940094027 小时前
emu系列模拟器最新汉化版 安卓版 怀旧游戏模拟器全集附可运行游戏ROM
android·游戏·安卓·模拟器
下位子7 小时前
『OpenGL学习滤镜相机』- Day9: CameraX 基础集成
android·opengl
参宿四南河三9 小时前
Android Compose SideEffect(副作用)实例加倍详解
android·app
火柴就是我9 小时前
mmkv的 mmap 的理解
android
没有了遇见9 小时前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong10 小时前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强10 小时前
如何简单 hack agp 执行过程中的某个类
android
沐怡旸10 小时前
【底层机制】 Android ION内存分配器深度解析
android·面试