CameraX的使用 | 青训营笔记

154 阅读3分钟

CameraX的使用

这是我参与「第四届青训营 」笔记创作活动的第3天

CameraX 是一个 Jetpack 支持库,目的是简化Camera的开发工作,它是基于Camera2 API的基础,向后兼容至 Android 5.0(API 级别 21)。 它有以下几个特性:

  • 易用性

    CameraX 引入了多个用例,使您可以专注于需要完成的任务,而无需花时间处理不同设备之间的细微差别;如:预览用例,图片分析用例,图片拍摄用例等。

  • 新的相机体验

    CameraX 有一个名为 Extensions 的可选插件,开发者可以通过扩展的形式使用和原生摄像头应用同样的功能(如:人像、夜间模式、HDR、滤镜、美颜)

  • 生命周期管理

    CameraX 和 Lifecycle 结合在一起,方便开发者管理生命周期。且相比较 camera2 减少了大量样板代码的使用

  • 确保各设备间的一致性

    Google 自己还打造了 CameraX 自动化测试实验室,对摄像头功能进行深度测试,确保能覆盖到更加广泛的设备。相当于 Google 帮我们把设备兼容测试工作给做了。

导入依赖

首先我们来导入依赖:最新版本请看CameraX | Android 开发者 | Android Developers (google.cn)

//核心库必须依赖
implementation("androidx.camera:camera-core:1.2.0-alpha04")
implementation("androidx.camera:camera-camera2:1.2.0-alpha04")
//如果想另外使用 CameraX Lifecycle 库
implementation("androidx.camera:camera-lifecycle:1.2.0-alpha04")
//如果使用cameraView,previewView
implementation("androidx.camera:camera-view:1.2.0-alpha04")
​
implementation("androidx.camera:camera-extensions:1.2.0-alpha04")
//如果想拍摄video
implementation "androidx.camera:camera-video:1.2.0-alpha04"

添加权限

记得要在AndroidManifest.xml文件中添加对应的权限

<uses-feature android:name="android.hardware.camera.any" /><!-- 使用uses-feature指定需要相机资源 -->
<uses-feature android:name="android.hardware.camera.autofocus" /> <!-- 需要自动聚焦 -->
<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"/><!-- 存储图片或者视频 -->
​
//如果安卓的版本大于11,访问本地文件需要添加这行代码在application中
android:requestLegacyExternalStorage="true"

动态授权

在Activity中添加

companion object {
    const val REQUEST_CODE_PERMISSIONS = 10
    private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
    private val REQUIRED_PERMISSIONS = mutableListOf(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO,
        Manifest.permission.WRITE_EXTERNAL_STORAGE
    ).toTypedArray()
}
​
private lateinit var cameraSelector : CameraSelector
private lateinit var preview: Preview//预览对象
private lateinit var cameraProvider: ProcessCameraProvider//相机信息
private lateinit var camera: Camera//相机对象
private var imageCapture: ImageCapture? = null
private lateinit var cameraExecutor: ExecutorService
​
​
​
private fun initPermission(){
    if (allPermissionsGranted()) {
        startCamera()
    } else {
        ActivityCompat.requestPermissions(
            this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
        )
    }
    cameraExecutor = Executors.newSingleThreadExecutor()
}
​
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
    ContextCompat.checkSelfPermission(
        baseContext, it) == PackageManager.PERMISSION_GRANTED
}
​
override fun onRequestPermissionsResult(
    requestCode: Int, permissions: Array<String>, grantResults:
    IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == REQUEST_CODE_PERMISSIONS) {
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            Toast.makeText(this,"授权失败!",Toast.LENGTH_SHORT).show()
            finish()
        }
    }
}

编写相机界面

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black">
​
    //相机预览界面
    <androidx.camera.view.PreviewView
        android:id="@+id/camera_preview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="@id/guideline"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
​
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent="0.8" />
​
    //相机拍照按钮
    <ImageView
        android:id="@+id/camera_click"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:src="@drawable/ic_camera_click"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/guideline" />
​
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_end="20dp"/>
​
    //前后置相机切换
    <ImageView
        android:id="@+id/camera_switch"
        android:layout_width="34dp"
        android:layout_height="34dp"
        android:layout_marginTop="30dp"
        android:src="@drawable/ic_camera_switch"
        app:layout_constraintEnd_toStartOf="@+id/guideline1"
        app:layout_constraintTop_toTopOf="parent"
        android:elevation="1dp"/>
​
    //闪光灯模式切换
    <ImageView
        android:id="@+id/camera_flash"
        android:layout_width="34dp"
        android:layout_height="34dp"
        android:src="@drawable/ic_camera_flash_close"
        app:layout_constraintEnd_toStartOf="@+id/guideline1"
        app:layout_constraintTop_toBottomOf="@+id/camera_switch"
        android:layout_marginTop="20dp"
        android:elevation="1dp"/>
​
    //返回键
    <ImageView
        android:id="@+id/camera_close"
        android:layout_width="34dp"
        android:layout_height="34dp"
        android:src="@drawable/ic_camera_close"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/camera_switch"
        android:layout_marginStart="20dp"
        android:elevation="1dp"/>
​
    //相册以及拍照完成后预览图
    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/camera_album"
        android:layout_width="50dp"
        android:layout_height="50dp"
        app:layout_constraintBottom_toBottomOf="@+id/camera_click"
        app:layout_constraintEnd_toStartOf="@+id/guideline1"
        app:layout_constraintStart_toEndOf="@+id/camera_click"
        app:layout_constraintTop_toTopOf="@+id/camera_click"
        app:civ_border_width="1dp"
        android:src="@color/black"
        app:civ_border_color="@color/white"/>
​
</androidx.constraintlayout.widget.ConstraintLayout>

启动相机

private fun startCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
​
    cameraProviderFuture.addListener({
        // 将相机的生命周期绑定到应用进程
        cameraProvider = cameraProviderFuture.get()
        //预览配置
        preview = Preview.Builder().build()
        .also {
            //提供previewView预览控件
            it.setSurfaceProvider(cameraPreview.surfaceProvider)
        }
        imageCapture = ImageCapture.Builder().build()
        imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF//设置默认闪光灯模式
​
        cameraSelector = CameraSelector.Builder()
        .requireLensFacing(CameraSelector.LENS_FACING_BACK) //设置默认后置摄像头
        .build()
​
        try {
            bindPreview()
        } catch(exc: Exception) {
            Log.e(TAG, "Use case binding failed", exc)
        }
​
    }, ContextCompat.getMainExecutor(this))
}
​
private fun bindPreview(){
    cameraProvider.unbindAll()//防止之前绑定过,先解绑所有用例
    camera = cameraProvider.bindToLifecycle(
        this,
        cameraSelector,
        imageCapture,
        preview)
    //绑定用例
}

这样子就能够从预览界面中看到相机拍摄的画面了

拍照

private fun takePhoto() {
    val imageCapture = imageCapture ?: return
​
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
        }
    }
​
    // 创建包含文件 + 数据的输出对象,拍照完成后会保存在相册中
    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues)
        .build()
​
    // 设置图片捕获监听器,拍照后触发
    imageCapture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(this),
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
            }
​
            override fun onImageSaved(output: ImageCapture.OutputFileResults){
                Glide.with(cameraAlbum).load(output.savedUri).into(cameraAlbum)//拍照完后显示在图片预览中
            }
        }
    )
}

切换前后置摄像头

cameraSelector = 
if (cameraSelector.lensFacing == CameraSelector.LENS_FACING_BACK){
    CameraSelector.Builder()
                   .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
                   .build()
    }else{
            CameraSelector.Builder()
             .requireLensFacing(CameraSelector.LENS_FACING_BACK)
             .build()
}
//改变配置时需要重新绑定
bindPreview()

切换闪光灯模式

when(imageCapture?.flashMode){
    ImageCapture.FLASH_MODE_AUTO ->{
        imageCapture!!.flashMode = ImageCapture.FLASH_MODE_ON
        cameraFlash.setImageResource(R.drawable.ic_camera_flash_on)
    }
    ImageCapture.FLASH_MODE_OFF ->{
        imageCapture!!.flashMode = ImageCapture.FLASH_MODE_AUTO
        cameraFlash.setImageResource(R.drawable.ic_camera_flash_auto)
    }
    ImageCapture.FLASH_MODE_ON ->{
        imageCapture!!.flashMode = ImageCapture.FLASH_MODE_OFF
        cameraFlash.setImageResource(R.drawable.ic_camera_flash_close)
    }
    else->{}
}

实现聚焦显示

//设置触碰监听器
cameraPreview.setOnTouchListener { _, motionEvent ->
            if (motionEvent.action == MotionEvent.ACTION_UP){
                if (isOneClick){
                    val point = cameraPreview.meteringPointFactory
                        .createPoint(motionEvent.x, motionEvent.y)
                    val action = FocusMeteringAction.Builder(point,FLAG_AF)
                        .setAutoCancelDuration(3,TimeUnit.SECONDS)
                        .build()
                    showTapView(motionEvent.rawX.toInt(), motionEvent.rawY.toInt())
                    camera.cameraControl.startFocusAndMetering(action)
                }
                isOneClick = false
                isZoomOver = false
            }
            if (motionEvent.pointerCount == 1){
                if(!isZoomOver) isOneClick = true
            }else{
                //实现双指放大缩小
            }
            true
        }
​
​
//显示聚焦图标
private fun showTapView(x: Int, y: Int) {
    val size = resources.getDimension(R.dimen.DP70).toInt()
    val popupWindow = PopupWindow(size, size)
    val imageView = ImageView(this)
    imageView.setImageResource(R.drawable.ic_focus)
    popupWindow.animationStyle = R.style.camera_focus_anim_style//自己实现的聚焦动画
    popupWindow.contentView = imageView
    popupWindow.showAsDropDown(cameraPreview, x-size/2, y)
    cameraPreview.postDelayed({ popupWindow.dismiss() }, 1000)
    cameraPreview.playSoundEffect(SoundEffectConstants.CLICK)
}

实现双指放大缩小

在上面的聚焦显示的代码里添加

isOneClick = false
when (motionEvent.action and MotionEvent.ACTION_MASK) {
    MotionEvent.ACTION_POINTER_DOWN -> oldDist = getFingerSpacing(motionEvent)
    MotionEvent.ACTION_MOVE -> {
    val newDist: Float = getFingerSpacing(motionEvent)
    if (newDist > oldDist) handleZoom(true)
        else if (newDist < oldDist) handleZoom(false)
        oldDist = newDist
        isZoomOver = true
    }
}
​
//计算手指的间距
private fun getFingerSpacing(event: MotionEvent): Float {
    val x = event.getX(0) - event.getX(1)
    val y = event.getY(0) - event.getY(1)
    return sqrt((x * x + y * y).toDouble()).toFloat()
}
​
//根据双指移动时进行缩放
private fun handleZoom(isZoomIn : Boolean) {
    if (camera.cameraInfo.zoomState.value != null){
        val currentZoom = camera.cameraInfo.zoomState.value!!.zoomRatio
        val maxZoom = camera.cameraInfo.zoomState.value!!.maxZoomRatio
        if (isZoomIn && currentZoom < maxZoom) camera.cameraControl.setZoomRatio(currentZoom + 0.08F)//双指移动时缩放的速度
        else if (currentZoom > 0) camera.cameraControl.setZoomRatio(currentZoom - 0.08F)
    }
}