晓我课堂

使用CameraX开发一款你自己的相机App

2021-12-01  本文已影响0人  丨逐风者丨

CameraX官方文档:https://developer.android.google.cn/training/camerax
CameraX官方Demo:https://github.com/android/camera-samples/tree/main/CameraXBasic

CameraX 概览

CameraX 是一个 Jetpack 支持库,旨在帮助您简化相机应用的开发工作。它提供一致且易用的 API 接口,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。

虽然 CameraX 利用了 camera2 的功能,但采取了一种具有生命周期感知能力且基于用例的更简单方式。它还解决了设备兼容性问题,因此您无需在代码库中添加设备专属代码。这些功能减少了将相机功能添加到应用时需要编写的代码量。

最后,借助 CameraX,开发者只需两行代码就能实现与预安装的相机应用相同的相机体验和功能。CameraX Extensions 提供了一些可选的插件,让您可以在支持的设备上添加各种特效,这些特效包括焦外成像(人像)、HDR、夜间和脸部照片修复。

>>今天我们就根据最新版CameraX官方文档手撸一个相机App<<

话不多说,先看效果图
照片列表
拍照页面
视频列表
录视频页面
写在前面

本文虽然是讲如何使用CameraX开发一款相机App,但这是基于Android Jetpack开发的,同时也会涉及很多新框架,这里先罗列一下涉及到的核心知识点:

1.Kotlin
2.ViewPager2
3.TabLayout与ViewPager2的联动
4.RxJava,RxAndroid,RxPermissions
5.DataViewBinding 和 MVVM
6.androidx.lifecycle
7.ActivityResultLauncher
8.ConstraintLayout
9.RecyclerView(如果你还在用ListView或者GridView,真是Out了,RecyclerView什么都能做,包括最新的ViewPager2也是用RecyclerView实现的)

第1步 添加依赖

创建Android项目并添加CameraX依赖,在app目录下的build.gradle文件内添加CameraX依赖,截止2021年12月01日,CameraX官方最新版本是1.1.0-alpha11

// CameraX core library using the camera2 implementation
def camerax_version = "1.1.0-alpha11"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// If you want to additionally use the CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
// If you want to additionally use the CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha31"
// If you want to additionally use the CameraX Extensions library
// implementation "androidx.camera:camera-extensions:1.0.0-alpha31"
第2步 权限声明

在AndroidManifest.xml中声明权限和硬件使用信息

<!-- 具备摄像头 -->
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.autofocus" />
<uses-feature android:name="android.hardware.camera.any" />

<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 录音权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- 读写外部存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
第3步 权限申请

这是一个相册相机App,进入首页就需要读取已拍摄的照片列表,这里需要获取外部存储读写权限,使用RxPermissions实现

    /**
     * 使用by lazy来对rxPermissions进行初始化, 
     * 由于我们这里不涉及多线程,所以加上LazyThreadSafetyMode.NONE
     */
    val rxPermissions: RxPermissions by lazy(LazyThreadSafetyMode.NONE) {
        RxPermissions(this)
    }

    @SuppressLint("CheckResult")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.lifecycleOwner = this
        setContentView(binding.root)

        rxPermissions.request(
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        ).subscribe {
            if (it) {// 授权成功,初始化ViewPager2
                initViewPager2()
            } else {
                // 授权失败,弹窗并提供退出按钮(后期可增加必要权限说明弹窗)
                showMessage("授权失败,无法读取相册。", withExit = true)
            }
        }
    }
第4步 启动相机

创建一个CameraXActivity,并启动这个Activity,由于进入相机后需要第一时间展示摄像头预览画面,所以我们选择在启动相机前进行权限申请。

    // 新的知识点:ActivityResultLauncher,用于取代startActivityForResult
    lateinit var cameraResultLauncher: ActivityResultLauncher<Intent>
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // ActivityResultLauncher必须在页面创建的时候初始化
        cameraResultLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
                if (it.resultCode == Activity.RESULT_OK) {
                    val intent: Intent? = it.data
                    val path = intent?.getStringExtra("path")
                    if (!TextUtils.isEmpty(path)) {
                        LogUtil.d("拍摄成功:$path")
                        notifyItemInserted(path!!)
                    }
                }
            }
        // 子类重写此方法即可
        onInit()
    }

    @SuppressLint("CheckResult")
    fun openCamera(type: Int) {
        rxPermissions.request(
            Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        ).subscribe {
            if (it) {
                // 授权成功
                LogUtil.d("授权成功,启动相机")
                val intent = Intent(requireContext(), CameraXActivity::class.java)
                intent.putExtra("type", type)// 这个type用于区分拍照还是摄像,后面会用到
                cameraResultLauncher.launch(intent)
            } else {
                // 授权失败
                showMessage("授权失败,无法启动相机。")
            }
        }
    }
第5步 拍照页面布局文件
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--实时镜头预览画面,不再需要SurfaceView或者TextureView-->
        <androidx.camera.view.PreviewView
            android:id="@+id/surfacePreview"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:background="@color/colorPrimary" />

        <View
            android:id="@+id/bgControl"
            android:layout_width="0dp"
            android:layout_height="144dp"
            android:background="@color/black_20p"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <!--切换前置/后置摄像头-->
        <com.zcs.app.camerax.widget.ScalableImageView
            android:id="@+id/btnSwitch"
            android:layout_width="54dp"
            android:layout_height="54dp"
            android:layout_margin="10dp"
            android:onClick="switchCamera"
            android:padding="10dp"
            android:src="@mipmap/ic_switch_camera"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <!--可缩放的白色大圆,点击变小并执行拍照-->
        <com.zcs.app.camerax.widget.ScalableImageView
            android:id="@+id/takePhoto"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:onClick="takePhoto"
            android:src="@drawable/bg_circle_white"
            app:layout_constraintBottom_toBottomOf="@id/bgControl"
            app:layout_constraintEnd_toEndOf="@id/bgControl"
            app:layout_constraintStart_toStartOf="@id/bgControl"
            app:layout_constraintTop_toTopOf="@id/bgControl" />

        <!--取消按钮-->
        <com.zcs.app.camerax.widget.ScalableImageView
            android:id="@+id/btnClose"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:onClick="closeCamera"
            android:padding="10dp"
            android:src="@mipmap/ic_arrow_down"
            app:layout_constraintBottom_toBottomOf="@id/bgControl"
            app:layout_constraintEnd_toStartOf="@id/takePhoto"
            app:layout_constraintStart_toStartOf="@id/bgControl"
            app:layout_constraintTop_toTopOf="@id/bgControl" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
第6步 拍照功能实现
    // 使用by lazy初始化摄像头预览Preview
    private val preview by lazy(LazyThreadSafetyMode.NONE) {
        Preview.Builder()
            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
            .build()
    }

    // 使用by lazy来初始化ImageCapture
    private val imageCapture by lazy(LazyThreadSafetyMode.NONE) {
        ImageCapture.Builder()
            // 设置照片比例,目前只有16:9和4:3
            .setTargetAspectRatio(AspectRatio.RATIO_16_9)
            // 设置预览方向,默认手机方向,可以不用修改
            // .setTargetRotation(binding.surfacePreview.display.rotation)
            // 设置照片压缩质量[1,100],值越大越清晰,默认值:95或100,根据模式而定
            .setJpegQuality(100)
            .build()
    }

    // Camerax实现拍照的核心
    private var cameraProvider: ProcessCameraProvider? = null
    // 默认使用后置摄像头
    var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA//当前相机

    // 页面初始化
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.lifecycleOwner = this
        setContentView(binding.root)
        // 启动相机
        startCamera()
    }
    /**
     * 打开相机
     */
    private fun startCamera() {
        if (cameraProvider == null) {
            val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
            cameraProviderFuture.addListener({
                cameraProvider = cameraProviderFuture.get()
                startPreview()
            }, ContextCompat.getMainExecutor(this))
        } else {
            startPreview()
        }
    }

    /**
     * 开启预览
     */
    private fun startPreview() {
        try {
            // 解除相机之前的所有绑定
            cameraProvider?.unbindAll()
            // 绑定前面用于预览和拍照的UseCase到相机上
            cameraProvider?.bindToLifecycle(this, cameraSelector, preview, imageCapture)
            // 设置用于预览的view
            preview.setSurfaceProvider(binding.surfacePreview.surfaceProvider)
        } catch (exc: Exception) {
            exc.printStackTrace()
            showMessage("相机启动失败,${exc.message}")
        }
    }

    /**
     * 切换前置/后置摄像头
     */
    fun switchCamera(v: View) {
        cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
            CameraSelector.DEFAULT_FRONT_CAMERA
        } else {
            CameraSelector.DEFAULT_BACK_CAMERA
        }
        // 切换摄像头后,需要重新开启预览
        startPreview()
    }

    /**
     * 拍照
     */
    fun takePhoto(v: View) {
        // 照片保存路径
        val imagePath = "${externalCacheDir?.absolutePath}/Pic_${System.currentTimeMillis()}.jpg"
        val file = File(imagePath)
        val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
        ImageCapture.OutputFileOptions.Builder(file)
        // 开始拍照
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onError(exc: ImageCaptureException) {
                    // 拍照失败
                    showMessage("Photo capture failed: ${exc.message}")
                }

                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    LogUtil.d("图片保存成功 -->${output.savedUri}")
                    LogUtil.d("图片保存成功 -->$imagePath")
                    // 拍照成功后,关闭相机并将照片绝对路径返回相册列表
                    val intent = Intent()
                    intent.putExtra("path", imagePath)
                    setResult(RESULT_OK, intent)
                    finish()
                }
            })
    }

    override fun onDestroy() {
        super.onDestroy()
        // 退出时记得关闭相机
        cameraProvider?.shutdown()
    }

至此,简单的拍照流程已经结束,你可以拿着这个图片的绝对路径为所欲为了。

第7步 视频录制
    // 使用by lazy来初始化VideoCapture
    private val videoCapture by lazy(LazyThreadSafetyMode.NONE) {
        VideoCapture.Builder()//录像用例配置
            // 设置视频比特率,720P 大概是2000Kbps  1080P 大概是6000Kbps
            // 如果这里不设置的话,摄像头像素很高的手机,拍出来的视频文件超大
            .setBitRate(1024 * 1024 * 2)
            // 设置音频比特率,可以使用系统默认
            // .setAudioBitRate(1024)
            // 设置视频帧率-高于30帧/秒,视频格式会过大。低于25帧/秒,视频会出现卡屏现象。
            .setVideoFrameRate(30)
            // 设置输出视频比例
            .setTargetResolution(Size(720, 1280))
            // 设置高宽比 不能和setTargetResolution共存
            // .setTargetAspectRatio(CustomCameraConfig.mAspectRatio)
            // 设置旋转角度,默认根据手机方向决定,非必要无需修改
            // .setTargetRotation(binding.surfacePreview.display.rotation)
            .build()
    }

    /**
     * 开启预览
     */
    private fun startPreview() {
        try {
            // 解除相机之前的所有绑定
            cameraProvider?.unbindAll()
            // 绑定前面用于预览和拍照的UseCase到相机上
            cameraProvider?.bindToLifecycle(
                this, cameraSelector, preview, imageCapture, videoCapture
            )
            // 设置用于预览的view
            preview.setSurfaceProvider(binding.surfacePreview.surfaceProvider)
        } catch (exc: Exception) {
            exc.printStackTrace()
            showMessage("相机启动失败,${exc.message}")
        }
    }

    /**
     * 开始视频录制
     */
    fun startRecord(v: View) {
        // 视频保存路径
        val videoPath = "${externalCacheDir?.absolutePath}/V_${System.currentTimeMillis()}.mp4"
        val file = File(videoPath)
        val outputOptions = VideoCapture.OutputFileOptions.Builder(file).build()
        // 开始录制
        videoCapture.startRecording(outputOptions,
            ContextCompat.getMainExecutor(this),
            object : VideoCapture.OnVideoSavedCallback {
                override fun onVideoSaved(output: VideoCapture.OutputFileResults) {
                    LogUtil.d("视频保存成功 -->${output.savedUri}")
                    LogUtil.d("视频保存成功 -->$videoPath")
                    // 关闭视频录制,并将绝对路径返回
                    val intent = Intent()
                    intent.putExtra("path", videoPath)
                    setResult(RESULT_OK, intent)
                    finish()
                }

                override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                    // 保存失败的回调,可能在开始或结束录制时被调用
                    showMessage("录像失败,$message")
                }
            }
        )
    }

至此,简单的视频拍摄流程已经结束,你可以拿着这个视频的绝对路径为所欲为了。

写在最后

文中只展示了部分核心代码,完整代码请查阅:
GitHub:https://github.com/ZengCS/MyCameraX
Gitee:https://gitee.com/ZengCS/MyCameraX
后期会继续维护,迭代

工程源码截图
上一篇下一篇

猜你喜欢

热点阅读