如果您的应用使用的是原始的 Camera
类(“Camera1”),自 Android 5.0(API 级别 21) 起已弃用,我们强烈建议您更新到现代的 Android 相机 API。Android 提供 CameraX(一个标准化、强大的 Jetpack 相机 API)和 Camera2(一个低级别框架 API)。对于绝大多数情况,我们建议您将您的应用迁移到 CameraX。原因如下:
- 易于使用:CameraX 处理底层细节,因此您可以减少从头构建相机体验的工作量,而更多地关注应用的差异化。
- CameraX 为您处理碎片化:CameraX 降低了长期维护成本和设备特定代码,为用户带来更高质量的体验。更多信息,请查看我们的 使用 CameraX 提升设备兼容性 博客文章。
- 高级功能:CameraX 经过精心设计,可轻松将高级功能集成到您的应用中。例如,您可以使用 CameraX Extensions轻松地将散景、面部润饰、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 并将其设置在 Camera 上 | 设置 PictureCallback 并调用 Camera 上的 takePicture() | 以特定顺序管理 Camera 和 MediaRecorder 配置 | 构建在预览 Surface 之上的自定义分析代码 |
↑ | ↑ | ↑ | ↑ |
设备特定代码 | |||
↑ | |||
设备旋转和缩放管理 | |||
↑ | |||
相机会话管理(相机选择、生命周期管理) | |||
Camera1 |
CameraX 中的兼容性和性能
CameraX 支持运行 Android 5.0(API 级别 21) 和更高版本的设备。这代表了超过 98% 的现有 Android 设备。CameraX 旨在自动处理设备之间的差异,从而减少应用中对设备特定代码的需求。此外,我们在我们的 CameraX 测试实验室 中对自 5.0 以来的所有 Android 版本的 150 多台物理设备进行了测试。您可以查看 目前测试实验室中的设备完整列表。
CameraX 使用 Executor
来驱动相机堆栈。如果您的应用有特定的线程要求,您可以 在 CameraX 上设置您自己的执行器。如果未设置,CameraX 将创建并使用优化的默认内部 Executor
。CameraX 所基于的许多平台 API 需要与硬件进行阻塞式进程间通信 (IPC),有时响应时间可能需要数百毫秒。因此,CameraX 只从后台线程调用这些 API,这确保主线程不会被阻塞,并且 UI 保持流畅。阅读有关线程的更多信息。
如果您的应用目标市场包括低端设备,CameraX 提供了一种使用 相机限制器 来减少设置时间的方法。由于连接到硬件组件的过程可能需要相当长的时间,尤其是在低端设备上,您可以指定应用所需的相机集。CameraX 只在设置期间连接到这些相机。例如,如果应用程序只使用后置摄像头,它可以使用 DEFAULT_BACK_CAMERA
设置此配置,然后 CameraX 将避免初始化前置摄像头以减少延迟。
Android 开发概念
本指南假设您对 Android 开发有一定的了解。除了基础知识之外,以下是一些在开始编写以下代码之前需要了解的概念:
- 视图绑定 为您的 XML 布局文件生成绑定类,允许您轻松地 在 Activity 中引用您的视图,如下面的几个代码片段中所示。与
findViewById()
(以前引用视图的方法)相比,有一些 视图绑定和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
,在CameraX中也可以做到这一点,方法是调用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
要显示预览,您需要使用android.view.SurfaceHolder.Callback
接口的实现编写您自己的Preview
类,该接口用于将图像数据从相机硬件传递到应用程序。然后,在您可以启动实时图像预览之前,必须将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之间的值,用于优先考虑焦点Areas
(如果设置了多个)。此示例仅使用一个Area
,因此权重值无关紧要。矩形的坐标范围为-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()
启用和禁用轻触对焦,并使用相应的getterisTapToFocusEnabled()
检查该值。
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()
以确保您需要的相关缩放方法在您的Camera上可用。
为了实现捏合缩放,此示例使用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()
启用和禁用捏合缩放,并使用相应的getterisPinchToZoomEnabled()
检查该值。
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()
。请参阅本节中的 CameraController
代码,了解 takePhoto()
函数的完整示例。
// 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
实例与MediaRecorder
。 - 调用
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
,它处理设备无法满足您所需质量规格的情况。然后使用其他 UseCase
将 VideoCapture
实例绑定到 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 讨论组 与我们联系。