1. 准备工作
在本 Codelab 中,您将学习如何使用 CameraX 创建相机应用,以显示取景器、拍照并分析相机的图像流。
为了实现此目标,我们将引入 CameraX 中的用例概念,您可在多个操作中应用此概念,包括显示取景器和实时分析画面帧等。
先决条件
- 基本的 Android 开发体验。
您需要执行的操作
- 了解如何添加 CameraX 依赖项。
- 了解如何在 Activity 中显示相机预览内容。(Preview 用例)
- 构建可以拍照并将照片保存在存储空间中的应用。(ImageCapture 用例)
- 了解如何实时分析相机中的画面帧。(ImageAnalysis 用例)
您需要用到的工具
- 一台 Android 设备,也可以使用 Android Studio 模拟器。我们建议使用搭载 Android 11 或更高版本的 AVD。
- 受支持的最低 API 级别为 21。
- Android Studio 3.6 或更高版本。
2. 创建项目
- 使用 Android Studio 菜单,新建项目并在收到系统提示时选择*"Empty Activity"* (空 Activity)。

- 下一步,将应用命名为"CameraX App"。确保将语言设置为 Kotlin、将最低 API 级别设为 21(对于 CameraX,这是所需的最低级别),并确保您使用 AndroidX 工件。

添加 Gradle 依赖项
- 打开 build.gradle(Module: app)文件并将 CameraX 依赖项添加到应用 Gradle 文件中的"依赖项"部分内:
def camerax_version = "1.0.0-beta07"
// 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-alpha14"
- CameraX 需要用到 Java 8 中的一些方法,因此我们需要对编译选项进行相应设置。 在 android块末尾,紧跟buildTypes的位置添加以下内容:
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
- 如果尚未添加此插件,请在最上方进行添加。
apply plugin: 'kotlin-android-extensions'
收到系统提示时,点击 Sync Now(立即同步),这样我们便做好了在应用中使用 CameraX 的准备。
创建取景器布局
使用以下代码替换默认布局:
- 打开 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>
设置 MainActivity.kt
- 将 MainActivity.kt中的代码替换为此内容。其中将包含输入语句、将会实例化的变量、将实现的函数以及常量。
系统已实现 onCreate(),可便于您检查相机权限、启动相机、为拍照按钮设置 onClickListener(),并实现 outputDirectory 和 cameraExecutor。即使您已实现了 onCreate(),相机也不会工作,直到您在文件中实施了这些方法为止。
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.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 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 imageCapture: ImageCapture? = 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)
       }
       // Set up the listener for take photo button
       camera_capture_button.setOnClickListener { takePhoto() }
       outputDirectory = getOutputDirectory()
       cameraExecutor = Executors.newSingleThreadExecutor()
   }
   private fun takePhoto() {}
   private fun startCamera() {}
   private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
       ContextCompat.checkSelfPermission(
           baseContext, it) == PackageManager.PERMISSION_GRANTED
   }
   private 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
   }
   override fun onDestroy() {
       super.onDestroy()
       cameraExecutor.shutdown()
   }
   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)
   }
}
- 运行代码。应用的界面应如下所示:

3. 请求相机权限
在应用打开相机前,其需要用户授予执行此操作的权限。在此步骤中,您将用到相机权限。
- 打开 AndroidManifest.xml并在application标记之前添加以下行。
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
添加用于确保设备配备有相机的 android.hardware.camera.any。指定 .any,用以表示相机可以是前置摄像头或后置摄像头。
- 将此代码复制到 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()
}
- 运行应用。
应用现在应该会请求相机使用权限:

4. 实现预览用例
在相机应用中,用户可借助取景器预览他们要拍摄的照片。您可以使用 CameraX Preview 类实现取景器功能。
如要使用 Preview,您首先需要定义配置,然后使用该配置创建用例的实例。所生成的实例是您要绑定到 CameraX 生命周期的内容。
- 将此代码复制到 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
       val preview = Preview.Builder()
          .build()
          .also {
              it.setSurfaceProvider(viewFinder.createSurfaceProvider())
          }
       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()
           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)
       } 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对象,在该对象上调用 build,从取景器中获取表面提供程序,然后在预览中进行设置。
val preview = Preview.Builder()
   .build()
   .also {
       it.setSurfaceProvider(viewFinder.createSurfaceProvider())
   }
- 创建  CameraSelector对象并选择DEFAULT_BACK_CAMERA。
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
- 创建 try块。在该块中,确保任何内容都未绑定到您的cameraProvider,然后将您的cameraSelector和预览对象绑定到cameraProvider。
try {
   cameraProvider.unbindAll()
   cameraProvider.bindToLifecycle(
       this, cameraSelector, preview)
} 
- 在少数情况下,此代码会失败,例如应用不再处于焦点中。将此代码放入 catch块中,以记录是否存在失败情况。
catch(exc: Exception) {
      Log.e(TAG, "Use case binding failed", exc)
}
- 运行应用。您应会看到相机预览!

5. 实现 ImageCapture 用例
其他用例的运行方式非常类似于 Preview。首先,定义用于对实际用例对象进行实例化的配置对象。为了拍摄照片,您需要采用 takePhoto() 方法,当用户按下 **Take photo(拍摄照片)**按钮时,系统会调用此方法。
将此代码复制到 takePhoto() 方法中。
下方的项目符号点将把您刚才复制的代码分为几个部分。
private fun takePhoto() {
   // Get a stable reference of the modifiable image capture use case
   val imageCapture = imageCapture ?: return
   // Create time-stamped 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()
   // Set up 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)
           }
       })
}
- 首先,获取对  ImageCapture用例的引用。如果用例为 null,则退出函数。如果您在设置拍摄图像之前点按拍照按钮,则这将为 null。如果没有return语句,则在用例为null的情况下,应用会崩溃。
val imageCapture = imageCapture ?: return
- 接下来,创建一个容纳图像的文件。添加时间戳,以避免文件名重复。
val photoFile = File(
   outputDirectory,
   SimpleDateFormat(FILENAME_FORMAT, Locale.US
   ).format(System.currentTimeMillis()) + ".jpg")
- 创建  OutputFileOptions对象。您可以在此对象中指定有关输出方式的设置。如果您希望将输出内容保存在刚创建的文件中,则添加您的photoFile。
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
- 对 imageCapture对象调用takePicture()。传入执行程序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)
}
- 使用 startCamera()方法并将此代码复制到用于预览的代码之下。
imageCapture = ImageCapture.Builder()
   .build()
- 最后,更新对 try块中对bindToLifecycle()的调用以加入新的用例:
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture)
此时,该方法将如下所示:
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
       val preview = Preview.Builder()
           .build()
           .also {
                 it.setSurfaceProvider(viewFinder.createSurfaceProvider())
           }
       imageCapture = ImageCapture.Builder()
           .build()
       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()
           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture)
       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }
   }, ContextCompat.getMainExecutor(this))
}
- 重新运行应用并按 Take Photo(拍照)。 屏幕上会出现一个消息框,其中会显示一条日志消息。

查看照片
- 检查日志语句。您将看到一条日志消息,其会通知您照片已拍摄成功。
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
- 复制用于存储照片的文件,并省略 file:// prefix。
/storage/emulated/0/Android/media/com.example.cameraxapp/CameraXApp/2020-04-24-15-13-25-746.jpg
- 在 Android Studio 终端中,运行以下命令:
adb shell
cp [INSERT THE FILE FROM STEP 2 HERE] /sdcard/Download/photo.jpg
- 运行此 ADB 命令,然后退出 shell:
adb pull /sdcard/Download/photo.jpg
- 您可以查看保存在当前文件夹的 photo.jpg 文件内的照片。
如果您需要实现简单的相机应用,则已达到目的。就是这么简单!如果您想要实现图像分析程序,请继续阅读!
6. 使用 ImageAnalysis 用例
如要让您的相机应用变得更加有趣,使用  ImageAnalysis 功能不妨是一种好方法。 通过此功能,您可以定义用于实现 ImageAnalysis.Analyzer 接口的自定义类,该接口将使用传入的相机帧来进行调用。您不必管理相机会话状态,甚至无须处理图像,只需将其绑定到应用的预期生命周期即可,就像其他 具有生命周期感知能力的组件一样。
- 添加此分析程序,将其作为 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 界面,我们只需在 ImageAnalysis, 中对 LuminosityAnalyzer  的实例进行实例化(类似于其他用例),并再次更新 startCamera() 函数,然后再调用 CameraX.bindToLifecycle() 即可:
- 在 startCamera()方法中,将此代码添加到imageCapture()代码下。
val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
- 更新 cameraProvider中的bindToLifecycle()调用,以加入imageAnalyzer。
cameraProvider.bindToLifecycle(
   this, cameraSelector, preview, imageCapture, imageAnalyzer)
完整方法现如下所示:
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
       val preview = Preview.Builder()
           .build()
           .also {
               it.setSurfaceProvider(viewFinder.createSurfaceProvider())
           }
       imageCapture = ImageCapture.Builder()
           .build()
       val imageAnalyzer = ImageAnalysis.Builder()
           .build()
           .also {
               it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
                   Log.d(TAG, "Average luminosity: $luma")
               })
           }
       // Select back camera as a default
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
       try {
           // Unbind use cases before rebinding
           cameraProvider.unbindAll()
           // Bind use cases to camera
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview, imageCapture, imageAnalyzer)
       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }
   }, ContextCompat.getMainExecutor(this))
}
- 现在运行应用!应用大约每秒会在 logcat 中生成一条类似于以下内容的消息。
D/CameraXApp: Average luminosity: ...
7. 恭喜!
您已成功地从零开始,在新的 Android 应用中实现了以下内容:
- 已将 CameraX 依赖项加入到您的项目中。
- 已显示相机取景器(使用 Preview用例)
- 已能够拍摄照片,并可将图像保存到存储空间(使用 ImageCapture 用例)
- 已实现对来自相机的画面帧进行实时分析(使用 ImageAnalysis 用例)