CameraX使用入门

  • 什么是CameraX
  • 1. 在你开始之前
  • 2. 创建项目
  • 1)项目新建
  • 2)添加Gradle依赖
  • 3) 创建取景器布局
  • 4) 设置MainActivity.kt
  • 3. 请求相机权限
  • 4.实施预览用例
  • 5.实施ImageCapture用例
  • 6.实施ImageAnalysis用例
  • 7.恭喜!


博客创建时间:2020.06.09
博客更新时间:2021.06.27

以Android studio build=4.2.1,gradle=6.7.1,SdkVersion 30来分析讲解。如图文和网上其他资料不一致,可能是别的资料版本较低而已


这是一篇CameraX的官方使用入门译文,其原网页https://codelabs.developers.google.com/codelabs/camerax-getting-started/#5,当然它需要翻墙,我已帮你翻墙且翻译完成,请放心使用!

CameraX的使用源码Demo在此https://github.com/android/camera-samples/tree/master/CameraXBasic,可无需翻墙直接下载使用。

个人使用过后感觉比Camera和Camera2 更加简单易用,且兼容性更好,不会出现常见的聚焦失败等问题。

博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !

什么是CameraX

在 Android 应用中要实现 Camera 功能还是比较困难的,为了保证在各品牌手机设备上的兼容性、响应速度等体验细节,Camera 应用的开发者往往需要花很大的时间和精力进行测试,甚至需要手动在数百种不同设备上进行测试。CameraX 正是为解决这个痛点而诞生的。

CameraX 的优势

  • CameraX 和 Lifecycle 结合在一起,方便开发者管理生命周期。且相比较 camera2 减少了大量样板代码的使用。
  • CameraX 是基于 Camera2 API 实现的,兼容至 Android L (API 21),从而确保兼容到市面上绝大多数手机
  • 开发者可以通过扩展的形式使用和原生摄像头应用同样的功能(如:人像、夜间模式、HDR、滤镜、美颜)
  • Google 自己还打造了 CameraX 自动化测试实验室,对摄像头功能进行深度测试,确保能覆盖到更加广泛的设备。相当于 Google 帮我们把设备兼容测试工作给做了。

对于开发者来说,简单易用的 API、更少的模版代码、更强的兼容性,意味着更高的开发和测试效率。而丰富的扩展性则意味着开发者可以为用户们带来更多基于摄像头的光影体验。

1. 在你开始之前

在此代码实验室中,您将学习如何创建一个使用CameraX来显示取景器,拍照并分析来自相机的图像流的相机应用。
为此,我们将在CameraX中引入用例的概念,您可以将其用于各种相机操作,从显示取景器到实时分析帧。

先决条件

  • 基本的Android开发经验。

你会做什么

  • 了解如何添加CameraX依赖项。
  • 了解如何在活动中显示相机预览。(预览用例)
  • 构建一个可以拍照的应用程序并将其保存到存储中。(ImageCapture用例)
  • 了解如何实时分析相机中的帧。(ImageAnalysis用例)

你需要什么

  • Android设备。Android Studio的模拟器也可以使用,我们建议使用R及更高版本。图像分析用例不适用于低于R的任何对象。
  • 支持的最低API级别为21。
  • Android Studio 3.6或更高版本。

2. 创建项目

1)项目新建

  1. 使用Android Studio菜单,启动一个新项目,并在出现提示时选择清空活动。
  2. 接下来,将应用程序命名为“ CameraX App”。确保将语言设置为Kotlin,最低API级别为21(这是CameraX所需的最低要求),并且您使用的是AndroidX工件。

2)添加Gradle依赖

1… 打开build.gradle(Module: app)文件,并将CameraX依赖项添加到我们的应用Gradle文件中的“ 依赖项”部分中:

def camerax_version = "1.0.0-beta03"
// CameraX core library using camera2 implementation
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle Library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha10"
  1. CameraX需要Java 8中的某些方法,因此我们需要相应地设置编译选项。在该android块的末尾,紧接着buildTypes,添加以下内容:
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

出现提示时,点击立即同步,我们将准备在我们的应用程序中使用CameraX。

3) 创建取景器布局

让我们用替换默认布局

  1. 打开activity_main布局文件,并将其替换为此代码。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/camera_capture_button"
       android:layout_width="100dp"
       android:layout_height="100dp"
       android:layout_marginBottom="50dp"
       android:scaleType="fitCenter"
       android:text="Take Photo"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintBottom_toBottomOf="parent"
       android:elevation="2dp" />

   <androidx.camera.view.PreviewView
       android:id="@+id/viewFinder"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

4) 设置MainActivity.kt

  1. 用替换中的代码MainActivity.kt。它包括导入语句,将要实例化的变量,将要实现的函数和常量。
    onCreate()已经实现了您检查相机权限,启动相机,onClickListener()为照片按钮设置和并实现outputDirectory和的功能cameraExecutor。即使onCreate()已为您实现,相机也无法工作,直到您实现文件中的方法。
package com.example.cameraxapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.concurrent.Executors
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
typealias LumaListener = (luma: Double) -> Unit

class MainActivity : AppCompatActivity() {
   private var preview: Preview? = null
   private var imageCapture: ImageCapture? = null
   private var imageAnalyzer: ImageAnalysis? = null
   private var camera: Camera? = null

   private lateinit var outputDirectory: File
   private lateinit var cameraExecutor: ExecutorService

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       // Request camera permissions
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           ActivityCompat.requestPermissions(
               this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
       }

       // Setup the listener for take photo button
       camera_capture_button.setOnClickListener { takePhoto() }

       outputDirectory = getOutputDirectory()

       cameraExecutor = Executors.newSingleThreadExecutor()
   }

   private fun startCamera() {
       // TODO
   }

   private fun takePhoto() {
       // TODO
   }

   private fun allPermissionsGranted() = false

   fun getOutputDirectory(): File {
       val mediaDir = externalMediaDirs.firstOrNull()?.let {
           File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
       return if (mediaDir != null && mediaDir.exists())
           mediaDir else filesDir
   }

   companion object {
       private const val TAG = "CameraXBasic"
       private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
       private const val REQUEST_CODE_PERMISSIONS = 10
       private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
   }
}
  1. 运行代码,它应如下所示:
  2. Android 相机 run 程序报错 debug 正常 android camerax_CameraX


3. 请求相机权限

在应用程序打开相机之前,需要获得用户的许可才能这样做。在此步骤中,您将实现相机权限。

  1. 打开AndroidManifest.xml并在application标签之前添加这些行。
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

添加操作android.hardware.camera.any可确保该设备具有摄像机。指定.any意味着它可以是前置摄像头或后置摄像头。

如果您android.hardware.camera不使用.any,则如果您的设备没有后置摄像头(例如大多数Chromebook),则无法使用。第二行添加了访问该摄像机的权限。

  1. 将此代码复制到MainActivity.kt.
    下面的项目符号中,将分解您刚刚复制的代码。
override fun onRequestPermissionsResult(
   requestCode: Int, permissions: Array<String>, grantResults:
   IntArray) {
   if (requestCode == REQUEST_CODE_PERMISSIONS) {
       if (allPermissionsGranted()) {
           startCamera()
       } else {
           Toast.makeText(this,
               "Permissions not granted by the user.",
               Toast.LENGTH_SHORT).show()
           finish()
       }
   }
}
  • 检查请求代码是否正确;如果不忽略它。
if (requestCode == REQUEST_CODE_PERMISSIONS) {

}
  • 如果授予了权限,请致电startCamera()。
if (allPermissionsGranted()) {
   startCamera()
}
  • 如果未授予权限,请举杯以通知用户未授予权限。
else {
   Toast.makeText(this,
       "Permissions not granted by the user.",
       Toast.LENGTH_SHORT).show()
   finish()
}
  1. 将此allPermissionsGranted()方法替换为:
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {       
   ContextCompat.checkSelfPermission(
   baseContext, it) == PackageManager.PERMISSION_GRANTED
}
  1. 运行应用程序。
    现在,它应该征得使用相机的许可:
  2. Android 相机 run 程序报错 debug 正常 android camerax_CameraX_02


4.实施预览用例

在相机应用程序中,取景器用于让用户预览将要拍摄的照片。您可以使用CameraX Preview类实现取景器。

要使用Preview,您首先需要定义一个配置,然后使用它来创建用例的实例。生成的实例是您需要绑定到CameraX生命周期的实例。

  1. 将此代码复制到startCamera()函数中。
    下面的要点将分解您刚刚复制的代码。
private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener(Runnable {
       // Used to bind the lifecycle of cameras to the lifecycle owner
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // Preview
       preview = Preview.Builder()
           .build()

       // Select back camera
       val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()

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

           // Bind use cases to camera
           camera = cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)
           preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo))
       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }

   }, ContextCompat.getMainExecutor(this))
}
  • 创建的实例ProcessCameraProvider。这用于将摄像机的生命周期绑定到生命周期所有者。由于CameraX具有生命周期感知功能,因此您不必担心打开和关闭相机。
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  • 侦听器添加到中cameraProviderFuture。添加一个Runnable作为一个参数,稍后我们将对其进行填充。添加作为第二个参数,这将返回一个在主线程上运行的。ContextCompat.getMainExecutor()Executor
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
  • 在中Runnable,添加ProcessCameraProvider,用于将相机的生命周期绑定到应用程序进程中的LifecycleOwner。
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
  • 初始化您的Preview对象。
preview = Preview.Builder().build()
  • 一个CameraSelector对象,然后使用该 CameraSelector.Builder.requireLensFacing方法传递您喜欢的镜头。
val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
  • 创建一个try块。在该块内,确保没有任何内容绑定到cameraProvider,然后将cameraSelector和预览对象绑定到cameraProvider。将viewFinder的Surface提供程序附加到preview用例。
try {
   cameraProvider.unbindAll()
   camera = cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(camera?.cameraInfo))
}
  • 此代码有几种可能会失败的方式,例如如果应用程序不再受到关注。将此代码包装在一个catch块中以记录是否失败。
catch(exc: Exception) {
      Log.e(TAG, "Use case binding failed", exc)
}
  1. 运行该应用程序,您应该会看到相机预览!
  2. Android 相机 run 程序报错 debug 正常 android camerax_android_03


5.实施ImageCapture用例

与相比,其他用例的工作方式非常相似Preview。首先,必须定义一个用于实例化实际用例对象的配置对象。要捕获照片,您需要实现takePhoto()方法,当按下捕获按钮时会调用该方法。

将此代码复制到takePhoto()方法中。

下面的要点将分解您刚刚复制的代码。

private fun takePhoto() {
   // Get a stable reference of the modifiable image capture use case
   val imageCapture = imageCapture ?: return

   // Create timestamped output file to hold the image
   val photoFile = File(
       outputDirectory,
       SimpleDateFormat(FILENAME_FORMAT, Locale.US
       ).format(System.currentTimeMillis()) + ".jpg")

   // Create output options object which contains file + metadata
   val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

   // Setup image capture listener which is triggered after photo has
   // been taken
   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) {
               val savedUri = Uri.fromFile(photoFile)
               val msg = "Photo capture succeeded: $savedUri"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       })
}
  1. 首先,获得ImageCapture用例的参考。如果用例为空,则返回该函数。如果在设置图像捕获之前点击照片按钮,则此属性为null。如果没有return声明,则该应用程序将崩溃null。
val imageCapture = imageCapture ?: return
  1. 接下来,创建一个文件来保存图像。添加时间戳,以便文件名是唯一的。
val photoFile = File(
   outputDirectory,
   SimpleDateFormat(FILENAME_FORMAT, Locale.US
   ).format(System.currentTimeMillis()) + ".jpg")
  • 创建一个OutputFileOptions对象。您可以在此对象中指定有关输出效果的内容。您希望将输出保存在我们刚刚创建的文件中,因此添加您的photoFile。
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
  • 调用takePicture()的上imageCapture对象。传入outputOptions,执行程序和保存图像的回调。接下来,您将填写回调。
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) {
   val savedUri = Uri.fromFile(photoFile)
   val msg = "Photo capture succeeded: $savedUri"
   Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
   Log.d(TAG, msg)
}
  1. 转到startCamera()方法,然后将此代码复制到代码下以进行预览。
imageCapture = ImageCapture.Builder()
   .build()

这显示了在方法中粘贴代码的位置:

private fun startCamera() {
       ... 

       preview = Preview.Builder()
           .build()
       // Paste image capture code here!

       val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
       ... 
}
  1. 最后,bindToLifecycle()在try块中更新对的调用以包括新的用例:
camera = cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture)
  1. 重新运行该应用程序,然后按拍照。
    您将在屏幕上看到烤面包和日志中的消息。
  2. Android 相机 run 程序报错 debug 正常 android camerax_CameraX_04


查看照片
6. 检查日志语句,您还将看到一条日志,宣布照片捕获成功。

2020-04-24 15:13:26.146 11981-11981/com.example.cameraxapp D/CameraXBasic: Photo capture succeeded: file:///storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
  1. 复制文件的存储位置,省略file:// prefix。
/storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
  1. 在Android Studio终端中,运行以下命令:
adb shell
cp [INSERT THE FILE FROM STEP 2 HERE] /sdcard/Download/photo.jpg
  1. 运行此ADB命令,然后退出外壳程序:
adb pull /sdcard/Download/photo.jpg
  1. 您可以查看保存在当前文件夹中名为photo.jpg的文件中的照片。
6.实施ImageAnalysis用例

如果您运行的是Q或更低版本,则同时实现预览,图像捕获和图像分析将不适用于Android Studio的设备模拟器。我们建议使用真实的设备来测试代码实验室的这一部分。

使用该ImageAnalysis功能可以使您的相机应用程序更加有趣。它允许您定义实现该ImageAnalysis.Analyzer接口的自定义类,该类将与传入的摄像机帧一起调用。您无需担心管理相机会话状态甚至图像的问题。绑定到我们的应用程序所需的生命周期就足够了,就像其他可感知生命周期的组件一样。

  1. 将此分析器添加为中的内部类MainActivity.kt。
    该分析仪记录图像的平均亮度。要创建分析器,您需要analyze在实现该ImageAnalysis.Analyzer接口的类中重写该函数。
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {

   private fun ByteBuffer.toByteArray(): ByteArray {
       rewind()    // Rewind the buffer to zero
       val data = ByteArray(remaining())
       get(data)   // Copy the buffer into a byte array
       return data // Return the byte array
   }

   override fun analyze(image: ImageProxy) {

       val buffer = image.planes[0].buffer
       val data = buffer.toByteArray()
       val pixels = data.map { it.toInt() and 0xFF }
       val luma = pixels.average()

       listener(luma)

       image.close()
   }
}

随着我们的类实现ImageAnalysis.Analyzer接口,我们需要做的就是实例化的一个实例,LuminosityAnalyzer在ImageAnalysis像所有其他用例和更新startCamera()函数再次调用之前CameraX.bindToLifecycle():

  1. 在startCamera()方法中,将此代码添加到代码下imageCapture()。
imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }

这显示了在方法中粘贴代码的位置:

private fun startCamera() {
       ... 

       imageCapture = ImageCapture.Builder()
   .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
   .build()

       // Paste image analyzer code here!


       val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()

       ... 
}
  1. 更新上的bindToLifecycle()呼叫cameraProvider以包含imageAnalyzer。
camera = cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, imageAnalyzer)
  1. 立即运行该应用程序!它将大约每秒钟在logcat中产生一条与此类似的消息。
D/CameraXApp: Average luminosity: ...
7.恭喜!

您已成功从头开始将以下内容实现到新的Android应用中:

  • 项目中包含的CameraX依赖项。
  • 显示了相机取景器(使用“预览”用例)
  • 实施照片捕获,将图像保存到存储中(使用ImageCapture用例)
  • 实时执行来自摄像机的帧分析(使用ImageAnalysis用例)

相关链接

  1. Android 今日头条屏幕适配详细使用攻略
  2. Lottie动画 轻松使用

扩展链接:

  1. Android CameraX 使用入门
  2. Android studio 最全必用插件
  3. Android 史上最新最全的ADB及命令百科,没有之一

博客书写不易,您的点赞收藏是我前进的动力,千万别忘记点赞、 收藏 ^ _ ^ !