如果您的应用程序使用原始的 Camera
类(“Camera1”),该类已从 Android 5.0(API 级别 21) 开始弃用,我们强烈建议您更新到现代的 Android 相机 API。Android 提供了 CameraX(一个标准化、健壮的 Jetpack 相机 API)和 Camera2(一个低级框架 API)。对于绝大多数情况,我们建议将您的应用程序迁移到 CameraX。以下是原因:
- 易于使用:CameraX 处理低级细节,这样您就可以减少从头开始构建相机体验的时间,而将更多精力放在使您的应用程序与众不同方面。
- CameraX 为您处理碎片化:CameraX 降低了长期维护成本和设备特定代码,为用户带来更高质量的体验。有关更多信息,请查看我们的 通过 CameraX 提升设备兼容性 博客文章。
- 高级功能:CameraX 经过精心设计,可将高级功能轻松地集成到您的应用程序中。例如,您可以使用 CameraX 扩展 轻松地将散景、人脸修饰、HDR(高动态范围)和弱光亮化夜间拍摄模式应用于您的照片。
- 可更新性:Android 每年都会向 CameraX 发布新功能和错误修复。通过迁移到 CameraX,您的应用程序将获得最新的 Android 相机技术 随着每次 CameraX 版本发布,而不仅仅是在每年的 Android 版本发布时。
在本指南中,您将找到相机应用程序的常见场景。每个场景都包含 Camera1 实现和 CameraX 实现,以供比较。
在迁移时,有时您需要额外的灵活性才能与现有代码库集成。本指南中的所有 CameraX 代码都有 CameraController
实现(如果您希望使用 CameraX 的最简单方法,则非常适合)以及 CameraProvider
实现(如果您需要更多灵活性,则非常适合)。为了帮助您确定哪种方式最适合您,以下列出了每种方式的优点
CameraController |
CameraProvider |
需要很少的设置代码 | 允许更多控制 |
允许 CameraX 处理更多设置过程,这意味着点触对焦和捏合缩放等功能会自动运行 | 由于应用程序开发人员处理设置,因此有更多机会自定义配置,例如启用输出图像旋转或在 ImageAnalysis 中设置输出图像格式 |
要求 PreviewView 用于相机预览,允许 CameraX 提供无缝的端到端集成,就像我们的 ML Kit 集成一样,它可以将 ML 模型结果坐标(如人脸边界框)直接映射到预览坐标 |
能够使用自定义 `Surface` 用于相机预览,允许更大的灵活性,例如使用您现有的 `Surface` 代码,这可能是您应用程序其他部分的输入 |
如果您在尝试迁移时遇到问题,请在 CameraX 讨论组 中与我们联系。
迁移之前
将 CameraX 与 Camera1 使用情况进行比较
尽管代码可能看起来不同,但 Camera1 和 CameraX 中的基本概念非常相似。CameraX 将常见的相机功能抽象为用例,因此,在 Camera1 中留给开发人员的许多任务会由 CameraX 自动处理。CameraX 中有四个 UseCase
,您可以将其用于各种相机任务:Preview
、ImageCapture
、VideoCapture
和 ImageAnalysis
。
CameraX 为开发人员处理低级细节的一个示例是 ViewPort
,它在活动 UseCase
之间共享。这确保所有 UseCase
都看到完全相同的像素。在 Camera1 中,您必须自己管理这些细节,并且考虑到设备相机传感器和屏幕纵横比的变化,确保预览与拍摄的照片和视频匹配可能很棘手。
另一个示例是 CameraX 自动处理 Lifecycle
回调,方法是将回调传递给它时执行 Lifecycle
实例。这意味着 CameraX 在整个 Android 活动生命周期 中处理您的应用程序与相机的连接,包括以下情况:当您的应用程序进入后台时关闭相机;当屏幕不再需要显示时移除相机预览;以及当另一个活动(如传入视频通话)优先获取前台时暂停相机预览。
最后,CameraX 处理旋转和缩放,而无需您额外编写任何代码。在方向解锁的 Activity
的情况下,每次设备旋转时都会完成 UseCase
设置,因为系统会在方向更改时销毁并重新创建 Activity
。这会导致 UseCases
默认情况下每次将其目标旋转设置为与显示器的方向匹配。详细了解 CameraX 中的旋转。
在深入了解细节之前,以下是 CameraX 的 UseCase
的高级概述以及 Camera1 应用程序如何相关。(CameraX 概念以 蓝色 表示,而 Camera1 概念以 绿色 表示。)
CameraX |
|||
CameraController / CameraProvider 配置 | |||
↓ | ↓ | ↓ | ↓ |
预览 | ImageCapture | VideoCapture | ImageAnalysis |
⁞ | ⁞ | ⁞ | ⁞ |
管理预览 Surface 并将其设置在相机上 | 设置 PictureCallback 并调用相机上的 takePicture() | 以特定顺序管理相机和 MediaRecorder 配置 | 基于预览 Surface 构建的自定义分析代码 |
↑ | ↑ | ↑ | ↑ |
设备特定代码 | |||
↑ | |||
设备旋转和缩放管理 | |||
↑ | |||
相机会话管理(相机选择、生命周期管理) | |||
Camera1 |
CameraX 中的兼容性和性能
CameraX 支持运行 Android 5.0(API 级别 21) 及更高版本的设备。这代表了超过 98% 的现有 Android 设备。CameraX 旨在自动处理设备之间的差异,减少应用程序中需要编写设备特定代码的数量。此外,我们在 CameraX 测试实验室 中对 Android 5.0 之后的每个 Android 版本上的 150 多台物理设备进行了测试。您可以查看 当前在测试实验室中的设备 的完整列表。
CameraX 使用 Executor
来驱动相机堆栈。如果您应用程序有特定的线程要求,您可以 在 CameraX 上设置您自己的执行器。如果未设置,CameraX 会创建和使用优化的默认内部 Executor
。CameraX 所基于的许多平台 API 需要与硬件进行阻塞的进程间通信(IPC),而硬件有时可能需要数百毫秒才能响应。因此,CameraX 仅从后台线程调用这些 API,这可以确保主线程不被阻塞,并且 UI 保持流畅。详细了解线程。
如果您的应用程序的目标市场包括低端设备,CameraX 提供了一种使用 相机限制器 来缩短设置时间的方法。由于连接到硬件组件的过程可能需要相当长的时间,尤其是在低端设备上,因此您可以指定应用程序所需的相机集。CameraX 只会在设置过程中连接到这些相机。例如,如果应用程序只使用后置摄像头,它可以使用 DEFAULT_BACK_CAMERA
设置此配置,然后 CameraX 避免初始化前置摄像头,以减少延迟。
Android 开发概念
本指南假设您对 Android 开发有一定的了解。除了基础知识之外,以下是一些在深入了解以下代码之前需要了解的概念
- 视图绑定 为您的 XML 布局文件生成绑定类,允许您轻松地 在活动中引用视图,就像以下几个代码段中所做的那样。在 视图绑定和
findViewById()
(以前引用视图的方式)之间存在一些差异,但在下面的代码中,您可以将视图绑定行替换为类似的findViewById()
调用。 - 异步协程 是在 Kotlin 1.3 中添加的一种并发设计模式,可用于处理返回
ListenableFuture
的 CameraX 方法。从 1.1.0 版本开始,Jetpack 并发 库使这变得更加容易。要向您的应用程序添加异步协程
迁移常见场景
本节说明如何将常见场景从 Camera1 迁移到 CameraX。每个场景都包含 Camera1 实现、CameraX CameraProvider
实现和 CameraX CameraController
实现。
选择相机
在您的相机应用程序中,您可能想要提供的第一个功能之一就是选择不同的相机。
Camera1
在 Camera1 中,您可以使用 Camera.open()
并无参数来打开第一个后置摄像头,或者可以传入您要打开的相机的整数 ID。以下是如何实现此功能的示例
// Camera1: select a camera from id. // Note: opening the camera is a non-trivial task, and it shouldn't be // called from the main thread, unlike CameraX calls, which can be // on the main thread since CameraX kicks off background threads // internally as needed. private fun safeCameraOpen(id: Int): Boolean { return try { releaseCameraAndPreview() camera = Camera.open(id) true } catch (e: Exception) { Log.e(TAG, "failed to open camera", e) false } } private fun releaseCameraAndPreview() { preview?.setCamera(null) camera?.release() camera = null }
CameraX:CameraController
在 CameraX 中,相机选择由 CameraSelector
类处理。CameraX 使使用默认相机变得容易。您可以指定需要默认前置摄像头还是默认后置摄像头。此外,CameraX 的 CameraControl
对象让您可以轻松地 设置应用程序的缩放级别,因此,如果您的应用程序在支持 逻辑相机 的设备上运行,那么它将切换到正确的镜头。
以下是使用 CameraController
的默认后置相机的 CameraX 代码
// CameraX: select a camera with CameraController var cameraController = LifecycleCameraController(baseContext) val selector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK).build() cameraController.cameraSelector = selector
CameraX:CameraProvider
以下是如何使用 CameraProvider
选择默认前置相机的示例(前置或后置摄像头都可以与 CameraController
或 CameraProvider
一起使用)
// CameraX: select a camera with CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the "Android development concepts" // section above. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Set up UseCases (more on UseCases in later scenarios) var useCases:Array= ... // Set the cameraSelector to use the default front-facing (selfie) // camera. val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA try { // Unbind UseCases before rebinding. cameraProvider.unbindAll() // Bind UseCases to camera. This function returns a camera // object which can be used to perform operations like zoom, // flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, useCases) } catch(exc: Exception) { Log.e(TAG, "UseCase binding failed", exc) } }) ... // Call startCamera in the setup flow of your app, such as in onViewCreated. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ... lifecycleScope.launch { startCamera() } }
如果您希望控制选择哪个相机,这也是可能的。如果您使用 CameraProvider
,则可以通过调用 getAvailableCameraInfos()
来实现,该方法将为您提供 CameraInfo
对象,用于检查某些相机属性,如 isFocusMeteringSupported()
。然后,您可以将其转换为 CameraSelector
,以便像上面的示例一样使用 CameraInfo.getCameraSelector()
方法。
您可以使用 Camera2CameraInfo
类获取有关每个相机的更多详细信息。使用您想要获取的相机数据的键调用 getCameraCharacteristic()
。检查 CameraCharacteristics
类以获取您可以查询的所有键的列表。
以下是一个使用自定义 checkFocalLength()
函数的示例,您可以自己定义这个函数。
// CameraX: get a cameraSelector for first camera that matches the criteria // defined in checkFocalLength(). val cameraInfo = cameraProvider.getAvailableCameraInfos() .first { cameraInfo -> val focalLengths = Camera2CameraInfo.from(cameraInfo) .getCameraCharacteristic( CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS ) return checkFocalLength(focalLengths) } val cameraSelector = cameraInfo.getCameraSelector()
显示预览
大多数相机应用程序都需要在某个时候在屏幕上显示相机馈送。使用 Camera1,您需要正确管理生命周期回调,还需要确定预览的旋转和缩放。
此外,在 Camera1 中,您需要决定使用 TextureView
还是 SurfaceView
作为预览表面。这两种选择都有权衡,在任何情况下,Camera1 都要求您正确处理旋转和缩放。另一方面,CameraX 的 PreviewView
对 TextureView
和 SurfaceView
都有底层实现。CameraX 根据设备类型和应用程序运行的 Android 版本等因素决定最佳实现。如果任一实现兼容,则可以使用 PreviewView.ImplementationMode
声明您的首选。 COMPATIBLE
选项使用 TextureView
用于预览,PERFORMANCE
值使用 SurfaceView
(如果可能)。
Camera1
要显示预览,您需要编写自己的 Preview
类,该类具有 android.view.SurfaceHolder.Callback
接口的实现,该接口用于将图像数据从相机硬件传递到应用程序。然后,在您开始实时图像预览之前,Preview
类必须传递给 Camera
对象。
// Camera1: set up a camera preview. class Preview( context: Context, private val camera: Camera ) : SurfaceView(context), SurfaceHolder.Callback { private val holder: SurfaceHolder = holder.apply { addCallback(this@Preview) setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS) } override fun surfaceCreated(holder: SurfaceHolder) { // The Surface has been created, now tell the camera // where to draw the preview. camera.apply { try { setPreviewDisplay(holder) startPreview() } catch (e: IOException) { Log.d(TAG, "error setting camera preview", e) } } } override fun surfaceDestroyed(holder: SurfaceHolder) { // Take care of releasing the Camera preview in your activity. } override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) { // If your preview can change or rotate, take care of those // events here. Make sure to stop the preview before resizing // or reformatting it. if (holder.surface == null) { return // The preview surface does not exist. } // Stop preview before making changes. try { camera.stopPreview() } catch (e: Exception) { // Tried to stop a non-existent preview; nothing to do. } // Set preview size and make any resize, rotate or // reformatting changes here. // Start preview with new settings. camera.apply { try { setPreviewDisplay(holder) startPreview() } catch (e: Exception) { Log.d(TAG, "error starting camera preview", e) } } } } class CameraActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding private var camera: Camera? = null private var preview: Preview? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) // Create an instance of Camera. camera = getCameraInstance() preview = camera?.let { // Create the Preview view. Preview(this, it) } // Set the Preview view as the content of the activity. val cameraPreview: FrameLayout = viewBinding.cameraPreview cameraPreview.addView(preview) } }
CameraX:CameraController
在 CameraX 中,您作为开发人员需要管理的东西要少得多。如果您使用 CameraController
,那么您也必须使用 PreviewView
。这意味着 Preview
UseCase
是隐含的,这使得设置的工作量大大减少。
// CameraX: set up a camera preview with a CameraController. class MainActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) // Create the CameraController and set it on the previewView. var cameraController = LifecycleCameraController(baseContext) cameraController.bindToLifecycle(this) val previewView: PreviewView = viewBinding.cameraPreview previewView.controller = cameraController } }
CameraX:CameraProvider
使用 CameraX 的 CameraProvider
,您不必使用 PreviewView
,但它仍然比 Camera1 大大简化了预览设置。出于演示目的,此示例使用 PreviewView
,但如果您有更复杂的需求,可以编写一个自定义 SurfaceProvider
传递给 setSurfaceProvider()
。
在这里,Preview
UseCase
不会像使用 CameraController
那样隐式,因此您需要进行设置。
// CameraX: set up a camera preview with a CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the "Android development concepts" // section above. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Create Preview UseCase. val preview = Preview.Builder() .build() .also { it.setSurfaceProvider( viewBinding.viewFinder.surfaceProvider ) } // Select default back camera. val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { // Unbind UseCases before rebinding. cameraProvider.unbindAll() // Bind UseCases to camera. This function returns a camera // object which can be used to perform operations like zoom, // flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, useCases) } catch(exc: Exception) { Log.e(TAG, "UseCase binding failed", exc) } }) ... // Call startCamera() in the setup flow of your app, such as in onViewCreated. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ... lifecycleScope.launch { startCamera() } }
点触对焦
当相机预览出现在屏幕上时,常见的控制方法是在用户点击预览时设置焦点点。
Camera1
要实现 Camera1 中的点触对焦,您必须计算最佳焦点 Area
以指示 Camera
应该尝试对焦的位置。此 Area
将传递给 setFocusAreas()
。此外,您必须在 Camera
上设置兼容的对焦模式。只有当当前对焦模式为 FOCUS_MODE_AUTO
、FOCUS_MODE_MACRO
、FOCUS_MODE_CONTINUOUS_VIDEO
或 FOCUS_MODE_CONTINUOUS_PICTURE
时,焦点区域才会起作用。
每个 Area
都是一个具有指定权重的矩形。权重是 1 到 1000 之间的数值,它用于在设置多个焦点区域时优先考虑焦点区域。此示例仅使用一个区域,因此权重值无关紧要。矩形的坐标范围为 -1000 到 1000。左上角点为 (-1000, -1000)。右下角点为 (1000, 1000)。方向相对于传感器方向,即传感器看到的内容。方向不受 Camera.setDisplayOrientation()
的旋转或镜像的影响,因此您需要将触摸事件坐标转换为传感器坐标。
// Camera1: implement tap-to-focus. class TapToFocusHandler : Camera.AutoFocusCallback { private fun handleFocus(event: MotionEvent) { val camera = camera ?: return val parameters = try { camera.getParameters() } catch (e: RuntimeException) { return } // Cancel previous auto-focus function, if one was in progress. camera.cancelAutoFocus() // Create focus Area. val rect = calculateFocusAreaCoordinates(event.x, event.y) val weight = 1 // This value's not important since there's only 1 Area. val focusArea = Camera.Area(rect, weight) // Set the focus parameters. parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO) parameters.setFocusAreas(listOf(focusArea)) // Set the parameters back on the camera and initiate auto-focus. camera.setParameters(parameters) camera.autoFocus(this) } private fun calculateFocusAreaCoordinates(x: Int, y: Int) { // Define the size of the Area to be returned. This value // should be optimized for your app. val focusAreaSize = 100 // You must define functions to rotate and scale the x and y values to // be values between 0 and 1, where (0, 0) is the upper left-hand side // of the preview, and (1, 1) is the lower right-hand side. val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000 val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000 // Calculate the values for left, top, right, and bottom of the Rect to // be returned. If the Rect would extend beyond the allowed values of // (-1000, -1000, 1000, 1000), then crop the values to fit inside of // that boundary. val left = max(normalizedX - (focusAreaSize / 2), -1000) val top = max(normalizedY - (focusAreaSize / 2), -1000) val right = min(left + focusAreaSize, 1000) val bottom = min(top + focusAreaSize, 1000) return Rect(left, top, left + focusAreaSize, top + focusAreaSize) } override fun onAutoFocus(focused: Boolean, camera: Camera) { if (!focused) { Log.d(TAG, "tap-to-focus failed") } } }
CameraX:CameraController
CameraController
侦听 PreviewView
的触摸事件以自动处理点触对焦。您可以使用 setTapToFocusEnabled()
启用和禁用点触对焦,并使用相应的 getter isTapToFocusEnabled()
检查值。
getTapToFocusState()
方法返回一个 LiveData
对象,用于跟踪 CameraController
上焦点状态的变化。
// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView, // with handlers you can define for focused, not focused, and failed states. val tapToFocusStateObserver = Observer{ state -> when (state) { CameraController.TAP_TO_FOCUS_NOT_STARTED -> Log.d(TAG, "tap-to-focus init") CameraController.TAP_TO_FOCUS_STARTED -> Log.d(TAG, "tap-to-focus started") CameraController.TAP_TO_FOCUS_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focus successful)") CameraController.TAP_TO_FOCUS_NOT_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focused unsuccessful)") CameraController.TAP_TO_FOCUS_FAILED -> Log.d(TAG, "tap-to-focus failed") } } cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)
CameraX:CameraProvider
使用 CameraProvider
时,需要进行一些设置才能使点触对焦正常工作。此示例假设您使用的是 PreviewView
。如果不是,您需要调整逻辑以应用于您的自定义 Surface
。
以下是使用 PreviewView
的步骤:
- 设置一个手势检测器来处理点击事件。
- 使用点击事件,使用
MeteringPointFactory.createPoint()
创建一个MeteringPoint
。 - 使用
MeteringPoint
,创建一个FocusMeteringAction
。 - 使用
Camera
上的CameraControl
对象(从bindToLifecycle()
返回),调用startFocusAndMetering()
,传入FocusMeteringAction
。 - (可选)响应
FocusMeteringResult
。 - 设置您的手势检测器以响应
PreviewView.setOnTouchListener()
中的触摸事件。
// CameraX: implement tap-to-focus with CameraProvider. // Define a gesture detector to respond to tap events and call // startFocusAndMetering on CameraControl. If you want to use a // coroutine with await() to check the result of focusing, see the // "Android development concepts" section above. val gestureDetector = GestureDetectorCompat(context, object : SimpleOnGestureListener() { override fun onSingleTapUp(e: MotionEvent): Boolean { val previewView = previewView ?: return val camera = camera ?: return val meteringPointFactory = previewView.meteringPointFactory val focusPoint = meteringPointFactory.createPoint(e.x, e.y) val meteringAction = FocusMeteringAction .Builder(meteringPoint).build() lifecycleScope.launch { val focusResult = camera.cameraControl .startFocusAndMetering(meteringAction).await() if (!result.isFocusSuccessful()) { Log.d(TAG, "tap-to-focus failed") } } } } ) ... // Set the gestureDetector in a touch listener on the PreviewView. previewView.setOnTouchListener { _, event -> // See pinch-to-zooom scenario for scaleGestureDetector definition. var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { didConsume = gestureDetector.onTouchEvent(event) } didConsume }
捏合缩放
放大和缩小预览是另一种常见的相机预览直接操作。随着设备上相机数量的增加,用户还期望在缩放时自动选择具有最佳焦距的镜头。
Camera1
使用 Camera1 缩放有两种方法。 Camera.startSmoothZoom()
方法从当前缩放级别动画到您传入的缩放级别。 Camera.Parameters.setZoom()
方法直接跳转到您传入的缩放级别。在使用任一方法之前,分别调用 isSmoothZoomSupported()
或 isZoomSupported()
,以确保您需要的相关缩放方法在您的相机上可用。
要实现捏合缩放,此示例使用 setZoom()
,因为预览表面上的触摸侦听器会在捏合手势发生时不断触发事件,因此它会每次立即更新缩放级别。 ZoomTouchListener
类定义如下,它应该设置为预览表面的触摸侦听器的回调。
// Camera1: implement pinch-to-zoom. // Define a scale gesture detector to respond to pinch events and call // setZoom on Camera.Parameters. val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.OnScaleGestureListener { override fun onScale(detector: ScaleGestureDetector): Boolean { val camera = camera ?: return false val parameters = try { camera.parameters } catch (e: RuntimeException) { return false } // In case there is any focus happening, stop it. camera.cancelAutoFocus() // Set the zoom level on the Camera.Parameters, and set // the Parameters back onto the Camera. val currentZoom = parameters.zoom parameters.setZoom(detector.scaleFactor * currentZoom) camera.setParameters(parameters) return true } } ) // Define a View.OnTouchListener to attach to your preview view. class ZoomTouchListener : View.OnTouchListener { override fun onTouch(v: View, event: MotionEvent): Boolean = scaleGestureDetector.onTouchEvent(event) } // Set a ZoomTouchListener to handle touch events on your preview view // if zoom is supported by the current camera. if (camera.getParameters().isZoomSupported()) { view.setOnTouchListener(ZoomTouchListener()) }
CameraX:CameraController
与点触对焦类似,CameraController
侦听 PreviewView 的触摸事件以自动处理捏合缩放。您可以使用 setPinchToZoomEnabled()
启用和禁用捏合缩放,并使用相应的 getter isPinchToZoomEnabled()
检查值。
getZoomState()
方法返回一个 LiveData
对象,用于跟踪 CameraController
上 ZoomState
的变化。
// CameraX: track the state of pinch-to-zoom over the Lifecycle of // a PreviewView, logging the linear zoom ratio. val pinchToZoomStateObserver = Observer{ state -> val zoomRatio = state.getZoomRatio() Log.d(TAG, "ptz-zoom-ratio $zoomRatio") } cameraController.getZoomState().observe(this, pinchToZoomStateObserver)
CameraX:CameraProvider
要使捏合缩放与 CameraProvider
正常工作,需要进行一些设置。如果您未使用 PreviewView
,则需要调整逻辑以应用于您的自定义 Surface
。
以下是使用 PreviewView
的步骤:
- 设置一个缩放手势检测器来处理捏合事件。
- 从
Camera.CameraInfo
对象中获取ZoomState
,其中Camera
实例是在您调用bindToLifecycle()
时返回的。 - 如果
ZoomState
具有zoomRatio
值,则将其保存为当前缩放比率。如果ZoomState
上没有zoomRatio
,则使用相机的默认缩放比率 (1.0)。 - 将当前缩放比率与
scaleFactor
的乘积作为新的缩放比率,并将其传递给CameraControl.setZoomRatio()
。 - 设置您的手势检测器以响应
PreviewView.setOnTouchListener()
中的触摸事件。
// CameraX: implement pinch-to-zoom with CameraProvider. // Define a scale gesture detector to respond to pinch events and call // setZoomRatio on CameraControl. val scaleGestureDetector = ScaleGestureDetector(context, object : SimpleOnGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { val camera = camera ?: return val zoomState = camera.cameraInfo.zoomState val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f camera.cameraControl.setZoomRatio( detector.scaleFactor * currentZoomRatio ) } } ) ... // Set the scaleGestureDetector in a touch listener on the PreviewView. previewView.setOnTouchListener { _, event -> var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { // See pinch-to-zooom scenario for gestureDetector definition. didConsume = gestureDetector.onTouchEvent(event) } didConsume }
拍照
本节介绍如何触发拍照,无论您是在快门按钮按下、计时器到期还是您选择的任何其他事件后进行拍照。
Camera1
在 Camera1 中,您首先定义一个 Camera.PictureCallback
来在请求时管理图片数据。以下是一个处理 JPEG 图像数据的简单 PictureCallback
示例。
// Camera1: define a Camera.PictureCallback to handle JPEG data. private val picture = Camera.PictureCallback { data, _ -> val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run { Log.d(TAG, "error creating media file, check storage permissions") return@PictureCallback } try { val fos = FileOutputStream(pictureFile) fos.write(data) fos.close() } catch (e: FileNotFoundException) { Log.d(TAG, "file not found", e) } catch (e: IOException) { Log.d(TAG, "error accessing file", e) } }
然后,无论何时您想拍照,您都会在您的 Camera
实例上调用 takePicture()
方法。此 takePicture()
方法有三个不同的参数用于不同的数据类型。第一个参数用于 ShutterCallback
(在此示例中未定义)。第二个参数用于 PictureCallback
处理原始(未压缩)相机数据。第三个参数是此示例使用的参数,因为它是一个用于处理 JPEG 图像数据的 PictureCallback
。
// Camera1: call takePicture on Camera instance, passing our PictureCallback. camera?.takePicture(null, null, picture)
CameraX:CameraController
CameraX 的 CameraController
通过实现自己的 takePicture()
方法保持 Camera1 的简单性。在这里,定义一个函数来配置 MediaStore
条目并拍摄一张照片以保存到该条目。
// CameraX: define a function that uses CameraController to take a photo. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private fun takePhoto() { // Create time stamped name and MediaStore entry. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .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") } } // Create output options object which contains file + metadata. val outputOptions = ImageCapture.OutputFileOptions .Builder(context.getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) .build() // Set up image capture listener, which is triggered after photo has // been taken. cameraController.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onError(e: ImageCaptureException) { Log.e(TAG, "photo capture failed", e) } override fun onImageSaved( output: ImageCapture.OutputFileResults ) { val msg = "Photo capture succeeded: ${output.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } } ) }
CameraX:CameraProvider
使用 CameraProvider
拍照的工作方式与使用 CameraController
几乎完全相同,但您首先需要创建和绑定一个 ImageCapture
UseCase
,以获得一个可以在其上调用 takePicture()
的对象。
// CameraX: create and bind an ImageCapture UseCase. // Make a reference to the ImageCapture UseCase at a scope that can be accessed // throughout the camera logic in your app. private var imageCapture: ImageCapture? = null ... // Create an ImageCapture instance (can be added with other // UseCase definitions). imageCapture = ImageCapture.Builder().build() ... // Bind UseCases to camera (adding imageCapture along with preview here, but // preview is not required to use imageCapture). This function returns a camera // object which can be used to perform operations like zoom, flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture)
然后,无论何时您想拍摄照片,您都可以调用 ImageCapture.takePicture()
。有关 takePhoto()
函数的完整示例,请参阅本节中的 CameraController
代码。
// CameraX: define a function that uses CameraController to take a photo. private fun takePhoto() { // Get a stable reference of the modifiable ImageCapture UseCase. val imageCapture = imageCapture ?: return ... // Call takePicture on imageCapture instance. imageCapture.takePicture( ... ) }
录制视频
录制视频比到目前为止所看到的场景要复杂得多。必须正确设置过程的每个部分,通常需要按特定顺序进行。此外,您可能需要验证视频和音频是否同步,或者处理其他设备不一致性。
如您所见,CameraX 再次为您处理了许多复杂性。
Camera1
使用 Camera1 录制视频需要仔细管理 Camera
和 MediaRecorder
,并且必须按特定顺序调用方法。您 **必须** 遵循此顺序才能使您的应用程序正常工作。
- 打开相机。
- 准备并启动预览(如果您的应用程序显示正在录制的视频,这通常是这种情况)。
- 通过调用
Camera.unlock()
解锁MediaRecorder
使用的相机。 - 通过在
MediaRecorder
上调用以下方法来配置录制- 使用
setCamera(camera)
连接您的Camera
实例。 - 调用
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
。 - 调用
setVideoSource(MediaRecorder.VideoSource.CAMERA)
。 - 调用
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))
设置质量。有关所有质量选项,请参阅CamcorderProfile
。 - 调用
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
。 - 如果您的应用有视频预览,请调用
setPreviewDisplay(preview?.holder?.surface)
。 - 调用
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
。 - 调用
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
。 - 调用
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
。 - 调用
prepare()
来完成MediaRecorder
的配置。
- 使用
- 要开始录制,请调用
MediaRecorder.start()
。 - 要停止录制,请调用以下方法。同样,请严格按照以下顺序进行操作。
- 调用
MediaRecorder.stop()
。 - 可选:通过调用
MediaRecorder.reset()
删除当前的MediaRecorder
配置。 - 调用
MediaRecorder.release()
。 - 锁定相机,以便将来的
MediaRecorder
会话可以使用它,方法是调用Camera.lock()
。
- 调用
- 要停止预览,请调用
Camera.stopPreview()
。 - 最后,要释放
Camera
以便其他进程可以使用它,请调用Camera.release()
。
以下是所有步骤的组合
// Camera1: set up a MediaRecorder and a function to start and stop video // recording. // Make a reference to the MediaRecorder at a scope that can be accessed // throughout the camera logic in your app. private var mediaRecorder: MediaRecorder? = null private var isRecording = false ... private fun prepareMediaRecorder(): Boolean { mediaRecorder = MediaRecorder() // Unlock and set camera to MediaRecorder. camera?.unlock() mediaRecorder?.run { setCamera(camera) // Set the audio and video sources. setAudioSource(MediaRecorder.AudioSource.CAMCORDER) setVideoSource(MediaRecorder.VideoSource.CAMERA) // Set a CamcorderProfile (requires API Level 8 or higher). setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)) // Set the output file. setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()) // Set the preview output. setPreviewDisplay(preview?.holder?.surface) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) // Prepare configured MediaRecorder. return try { prepare() true } catch (e: IllegalStateException) { Log.d(TAG, "preparing MediaRecorder failed", e) releaseMediaRecorder() false } catch (e: IOException) { Log.d(TAG, "setting MediaRecorder file failed", e) releaseMediaRecorder() false } } return false } private fun releaseMediaRecorder() { mediaRecorder?.reset() mediaRecorder?.release() mediaRecorder = null camera?.lock() } private fun startStopVideo() { if (isRecording) { // Stop recording and release camera. mediaRecorder?.stop() releaseMediaRecorder() camera?.lock() isRecording = false // This is a good place to inform user that video recording has stopped. } else { // Initialize video camera. if (prepareVideoRecorder()) { // Camera is available and unlocked, MediaRecorder is prepared, now // you can start recording. mediaRecorder?.start() isRecording = true // This is a good place to inform the user that recording has // started. } else { // Prepare didn't work, release the camera. releaseMediaRecorder() // Inform user here. } } }
CameraX:CameraController
使用 CameraX 的 CameraController
,您可以独立切换 ImageCapture
、VideoCapture
和 ImageAnalysis
UseCase
,只要 UseCase 列表可以同时使用。默认情况下,ImageCapture
和 ImageAnalysis
UseCase
已启用,这就是您不需要为拍照调用 setEnabledUseCases()
的原因。
要使用 CameraController
进行视频录制,您首先需要使用 setEnabledUseCases()
来允许 VideoCapture
UseCase
。
// CameraX: Enable VideoCapture UseCase on CameraController. cameraController.setEnabledUseCases(VIDEO_CAPTURE);
当您想要开始录制视频时,您可以调用 CameraController.startRecording()
函数。此函数可以将录制的视频保存到 File
中,如以下示例所示。此外,您需要传递一个 Executor
和一个实现 OnVideoSavedCallback
的类来处理成功和错误回调。当需要结束录制时,请调用 CameraController.stopRecording()
。
注意: 如果您使用的是 CameraX 1.3.0-alpha02 或更高版本,则还有一个额外的 AudioConfig
参数,允许您在视频中启用或禁用音频录制。要启用音频录制,您需要确保已获得麦克风权限。此外,在 1.3.0-alpha02 中,stopRecording()
方法已删除,startRecording()
返回一个 Recording
对象,可用于暂停、恢复和停止视频录制。
// CameraX: implement video capture with CameraController. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" // Define a VideoSaveCallback class for handling success and error states. class VideoSaveCallback : OnVideoSavedCallback { override fun onVideoSaved(outputFileResults: OutputFileResults) { val msg = "Video capture succeeded: ${outputFileResults.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) { Log.d(TAG, "error saving video: $message", cause) } } private fun startStopVideo() { if (cameraController.isRecording()) { // Stop the current recording session. cameraController.stopRecording() return } // Define the File options for saving the video. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val outputFileOptions = OutputFileOptions .Builder(File(this.filesDir, name)) .build() // Call startRecording on the CameraController. cameraController.startRecording( outputFileOptions, ContextCompat.getMainExecutor(this), VideoSaveCallback() ) }
CameraX:CameraProvider
如果您使用的是 CameraProvider
,则需要创建一个 VideoCapture
UseCase
并传入一个 Recorder
对象。在 Recorder.Builder
上,您可以设置视频质量,并且可以选择一个 FallbackStrategy
,该策略处理设备无法满足您所需质量规格的情况。然后将 VideoCapture
实例与其他 UseCase
绑定到 CameraProvider
。
// CameraX: create and bind a VideoCapture UseCase with CameraProvider. // Make a reference to the VideoCapture UseCase and Recording at a // scope that can be accessed throughout the camera logic in your app. private lateinit var videoCapture: VideoCaptureprivate var recording: Recording? = null ... // Create a Recorder instance to set on a VideoCapture instance (can be // added with other UseCase definitions). val recorder = Recorder.Builder() .setQualitySelector(QualitySelector.from(Quality.FHD)) .build() videoCapture = VideoCapture.withOutput(recorder) ... // Bind UseCases to camera (adding videoCapture along with preview here, but // preview is not required to use videoCapture). This function returns a camera // object which can be used to perform operations like zoom, flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, videoCapture)
此时,可以在 videoCapture.output
属性上访问 Recorder
。Recorder
可以启动保存到 File
、ParcelFileDescriptor
或 MediaStore
的视频录制。此示例使用 MediaStore
。
在 Recorder
上,有几种方法可以调用来准备它。调用 prepareRecording()
来设置 MediaStore
输出选项。如果您的应用有权使用设备的麦克风,请也调用 withAudioEnabled()
。然后,调用 start()
开始录制,传入上下文和 Consumer<VideoRecordEvent>
事件监听器来处理视频录制事件。如果成功,返回的 Recording
可用于暂停、恢复或停止录制。
// CameraX: implement video capture with CameraProvider. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private fun startStopVideo() { val videoCapture = this.videoCapture ?: return if (recording != null) { // Stop the current recording session. recording.stop() recording = null return } // Create and start a new recording session. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4") if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video") } } val mediaStoreOutputOptions = MediaStoreOutputOptions .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) .setContentValues(contentValues) .build() recording = videoCapture.output .prepareRecording(this, mediaStoreOutputOptions) .withAudioEnabled() .start(ContextCompat.getMainExecutor(this)) { recordEvent -> when(recordEvent) { is VideoRecordEvent.Start -> { viewBinding.videoCaptureButton.apply { text = getString(R.string.stop_capture) isEnabled = true } } is VideoRecordEvent.Finalize -> { if (!recordEvent.hasError()) { val msg = "Video capture succeeded: " + "${recordEvent.outputResults.outputUri}" Toast.makeText( baseContext, msg, Toast.LENGTH_SHORT ).show() Log.d(TAG, msg) } else { recording?.close() recording = null Log.e(TAG, "video capture ends with error", recordEvent.error) } viewBinding.videoCaptureButton.apply { text = getString(R.string.start_capture) isEnabled = true } } } } }
其他资源
我们在 Camera Samples GitHub 仓库 中提供了一些完整的 CameraX 应用。这些示例向您展示了本指南中的场景如何在完整的 Android 应用中发挥作用。
如果您在迁移到 CameraX 时需要更多支持,或者对 Android 相机 API 套件有任何疑问,请在 CameraX 讨论组 中与我们联系。