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 用例)