Android CameraX
developer android 用户指南 developer android 用户文档
Jetpack 是一个由多个库组成的套件,可帮助开发者遵循最佳实践、减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码,让开发者可将精力集中于真正重要的编码工作。
CameraX 是一个 Jetpack 库,旨在帮助您更轻松地开发相机应用。如果您要开发新应用,我们建议您从 CameraX 开始。它提供了一个一致且易于使用的 API,该 API 适用于绝大多数 Android 设备,并向后兼容 Android 5.0(API 级别 21)。
CameraX 结构
您可以使用 CameraX,借助名为“用例”的抽象概念与设备的相机进行交互。提供的用例如下:
- 预览:接受用于显示预览的 Surface,例如
PreviewView。 - 图片分析:为分析(例如机器学习)提供 CPU 可访问的缓冲区。
- 图片拍摄:拍摄并保存照片。
- 视频拍摄:通过
VideoCapture拍摄视频和音频
关键对象
提供了一致的 API 接口
CameraSelector(相机选择器)
用于选择要打开的相机设备。可以选择前置摄像头、后置摄像头或者其他可用的相机设备。
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build();Preview(预览)
用于在屏幕上显示相机预览。通过配置 Preview 对象,可以实现预览界面的定制和控制。
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(viewFinder.createSurfaceProvider());
ImageAnalysis(图像分析)
用于对相机捕获的图像进行分析。开发者可以通过 ImageAnalysis 处理图像数据,实现例如图像识别、文字识别等功能。
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setTargetResolution(new Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
imageAnalysis.setAnalyzer(executor, image -> {
// 处理图像数据的逻辑
// ...
});
Camera(相机)
将上述预览、图像分析和图像捕获等功能整合在一起,用于控制相机的整体行为。
Camera camera = Camera.open(cameraSelector);
camera.setPreview(preview);
camera.setImageAnalysis(imageAnalysis);
camera.setImageCapture(imageCapture);
CameraController (相机控制)
CameraController 在单个类中提供大多数 CameraX 核心功能。它只需少量设置代码,并且可自动处理相机初始化、用例管理、目标旋转、点按对焦、双指张合缩放等操作。扩展 CameraController 的具体类为 LifecycleCameraController。
val previewView: PreviewView = viewBinding.previewView
var cameraController = LifecycleCameraController(baseContext)
cameraController.bindToLifecycle(this)
cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
previewView.controller = cameraControllerCameraProvider
CameraProvider 仍然易于使用,但由于应用开发者会处理更多设置,因此有更多机会自定义配置,例如在 ImageAnalysis 中启用输出图片旋转或设置输出图像格式。您还可以使用自定义 Surface 进行相机预览以提高灵活性,而对于 CameraController,您需要使用 PreviewView。如果现有的 Surface 代码已是应用的其他部分的输入,则使用该代码会非常有用。
val preview = Preview.Builder().build()
val viewFinder: PreviewView = findViewById(R.id.previewView)
// 用它来绑定生命周期
// The use case is bound to an Android Lifecycle with the following code
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)
// PreviewView creates a surface provider and is the recommended provider
preview.setSurfaceProvider(viewFinder.getSurfaceProvider())安卓官方就相机 API 就改了三版, 离谱!
实例 (拍照)
https://developer.android.google.cn/media/camera/camerax/take-photo?hl=de
使用 GPS, 陀螺仪 + 磁力计 实时获取当前拍向的位置
依赖
在应用或模块的 build.gradle 文件中添加所需工件的依赖项:
dependencies {
// CameraX core library using the camera2 implementation
val camerax_version = "1.3.0-alpha04"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
// If you want to additionally use the CameraX Lifecycle library
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
// If you want to additionally use the CameraX VideoCapture library
implementation("androidx.camera:camera-video:${camerax_version}")
// If you want to additionally use the CameraX View class
implementation("androidx.camera:camera-view:${camerax_version}")
// If you want to additionally add CameraX ML Kit Vision Integration
implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
// If you want to additionally use the CameraX Extensions library
implementation("androidx.camera:camera-extensions:${camerax_version}")
}代码
package com.example.myapplication
import android.Manifest
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.ExecutionException
import java.util.logging.Logger
class CamActivity : AppCompatActivity(),LocationListener, SensorEventListener {
val log = Logger.getLogger("CamActivity");
//当前状态描述, 实时更新UI 及 定时器
private var realtimeLocation = "";
private var realtimeAngle = "";
private val mHandler: Handler = Handler(Looper.getMainLooper())
private val mRunnable = object : Runnable {
override fun run() {
val textView = findViewById<TextView>(R.id.textView)
textView.setText("经纬度: "+realtimeLocation+", 方向: "+realtimeAngle)
mHandler.postDelayed(this, 500)
}
}
//
private var previewView: PreviewView? = null
private var imageCapture: ImageCapture? = null
//
private var sensorManager: SensorManager? = null
//GPS
private var locationManager: LocationManager? = null
// 陀螺仪 & 磁力计
private var accelerometer: Sensor? = null
private var magnetometer: Sensor? = null
private val accelerometerReading = FloatArray(3)
private val magnetometerReading = FloatArray(3)
private val rotationMatrix = FloatArray(9)
private val orientationAngles = FloatArray(3)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(android.R.style.Theme_Light_NoTitleBar_Fullscreen)
setContentView(R.layout.activity_cam)
previewView = findViewById<PreviewView>(R.id.previewView)// PreviewView 是 CameraX 摄像头画面 预览组件
if (allPermissionsGranted()) {// 请求所有授权
initOnPermissionsGranted()
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
}
private fun allPermissionsGranted(): Boolean {
for (permission in REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
return true
}
private fun initOnPermissionsGranted(){
startCamera()
initializeSensors()
initializeLocation()
}
private fun startCamera() {
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({//初始化完成的回调
try {
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// set preview
val rotation = previewView?.display?.rotation
val preview = rotation?.let {
Preview
.Builder()
.setTargetRotation(it)// 设置为横屏
.build().also {
it.setSurfaceProvider(previewView?.surfaceProvider)
}
}
val cameraSelector = CameraSelector.Builder()// 选择摄像头, 现在手机 动不动N个摄像头
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
imageCapture = ImageCapture.Builder().build()
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
} catch (e: ExecutionException) {
e.printStackTrace()
} catch (e: InterruptedException) {
e.printStackTrace()
}
}, ContextCompat.getMainExecutor(this))
}
private fun initializeSensors() {
// 初始化传感器监听
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
if (sensorManager != null) {
accelerometer = sensorManager!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)//陀螺仪
magnetometer = sensorManager!!.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)//磁力计
log.info("initializeSensors")
}
}
private fun initializeLocation() {
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
if (locationManager != null) {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
log.info("initializeLocation")
// 请监听位置改变
locationManager!!.requestLocationUpdates(LocationManager.GPS_PROVIDER, 500L, 1.0f, this)//GPS
val lastKnownLocation = locationManager!!.getLastKnownLocation(LocationManager.GPS_PROVIDER)//最近一次的位置
lastKnownLocation?.let {
onLocationChanged(it)
}
}
}
override fun onLocationChanged(location: Location) {
locationUpdate(location.longitude, location.latitude)
}
private fun locationUpdate(lng : Double, lat: Double) {
log.warning("位置更新: $lng, $lat")
realtimeLocation = "$lng, $lat"
}
override fun onResume() {
super.onResume()
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
//注册 陀螺仪 传感器监听
sensorManager!!.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
//注册 磁力计 传感器监听
sensorManager!!.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_NORMAL)
mHandler.postDelayed(mRunnable, 500)//for UI
}
override fun onPause() {
super.onPause()
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
sensorManager?.unregisterListener(this)
mHandler.removeCallbacks(mRunnable)
}
override fun onSensorChanged(event: SensorEvent) {
// Update sensor readings based on sensor type
if (event.sensor === accelerometer) {
System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)
} else if (event.sensor === magnetometer) {
System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)
}
SensorManager.getRotationMatrix(
rotationMatrix,
null,
accelerometerReading,
magnetometerReading
)
SensorManager.getOrientation(rotationMatrix, orientationAngles)
SensorManager.getRotationMatrix(
rotationMatrix,
null,
accelerometerReading,
magnetometerReading
)
SensorManager.getOrientation(rotationMatrix, orientationAngles)
val azimuthInRadians = orientationAngles[0]
var azimuthInDegrees = Math.toDegrees(azimuthInRadians.toDouble()).toFloat()
azimuthInDegrees+=90//校准横屏, 正北是 0度
azimuthInDegrees = (azimuthInDegrees + 360) % 360
angleUpdate(azimuthInDegrees);
}
private fun angleUpdate(azimuthInDegrees :Float){
val direction = getDirectDesc(azimuthInDegrees)
// log.warning("角度更新: $azimuthInDegrees")
this.realtimeAngle = direction+"( "+azimuthInDegrees.toInt().toString()
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
initOnPermissionsGranted();
} else {
Toast.makeText(this, "未获得权限", Toast.LENGTH_SHORT).show()
finish();
}
}
}
private fun getDirectDesc(azimuthInDegrees: Float): String{
val direction: String = if (azimuthInDegrees >= 22.5 && azimuthInDegrees < 67.5) {
"东北"
} else if (azimuthInDegrees >= 67.5 && azimuthInDegrees < 112.5) {
"东"
} else if (azimuthInDegrees >= 112.5 && azimuthInDegrees < 157.5) {
"东南"
} else if (azimuthInDegrees >= 157.5 && azimuthInDegrees < 202.5) {
"南"
} else if (azimuthInDegrees >= 202.5 && azimuthInDegrees < 247.5) {
"西南"
} else if (azimuthInDegrees >= 247.5 && azimuthInDegrees < 292.5) {
"西"
} else if (azimuthInDegrees >= 292.5 && azimuthInDegrees < 337.5) {
"西北"
} else {
"北"
}
return direction
}
companion object {
private const val REQUEST_CODE_PERMISSIONS = 101
private val REQUIRED_PERMISSIONS =
arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION)
}
}
布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- camerax 预览组件-->
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/buttonsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<Button
android:id="@+id/captureButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Capture" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="TextView"
android:textColor="#FFFFFF" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>实例 (视频拍摄)
https://developer.android.google.cn/media/camera/camerax/preview?hl=de https://developer.android.google.cn/media/camera/camerax/video-capture?hl=de
视频捕获架构
https://developer.android.google.cn/media/camera/camerax/video-capture?hl=de
捕获系统通常会录制视频流和音频流,对其进行压缩,对这两个流进行多路复用,然后将生成的流写入磁盘
在 CameraX 中,用于视频捕获的解决方案是 VideoCapture 用例:
CameraX 视频捕获包括几个高级架构组件:
SurfaceProvider,表示视频来源。
AudioSource,表示音频来源。
用于对视频/音频进行编码和压缩的两个编码器。
用于对两个流进行多路复用的媒体复用器。
用于写出结果的文件保存器。
VideoCapture API 会对复杂的捕获引擎进行抽象化处理,为应用提供更加简单且直观的 API。
实时预览
https://developer.android.google.cn/media/camera/camerax/preview?hl=de
将 PreviewView 添加到布局
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/videoPreview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>使用 PreviewView 存在一些限制。使用 PreviewView 时,您无法执行以下任何操作:
- 创建
SurfaceTexture,以在TextureView和Preview.SurfaceProvider上进行设置。 - 从
TextureView检索SurfaceTexture,并在Preview.SurfaceProvider上对其进行设置。 - 从
SurfaceView获取Surface,并在Preview.SurfaceProvider上对其进行设置。
如果出现上述任何一种情况,Preview 就会停止将帧流式传输到 PreviewView。
获取 CameraProvider
import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.common.util.concurrent.ListenableFuture
private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
private lateinit var previewView : PreviewView;
class MainActivity : AppCompatActivity() {
private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
override fun onCreate(savedInstanceState: Bundle?) {
cameraProviderFuture = ProcessCameraProvider.getInstance(this)//cameraProviderFuture
previewView = findViewById<PreviewView>(R.id.videoPreview)//预览组件
}
}绑定预览, 检查 CameraProvider
请求 CameraProvider 后,请验证它能否在视图创建后成功初始化。以下代码展示了如何执行此操作:
// 请求 CameraProvider 来初始化 CameraX
cameraProviderFuture.addListener(Runnable {// 监听初始化完成
val cameraProvider = cameraProviderFuture.get()
bindPreview(cameraProvider)//绑定预览 生命周期
}, ContextCompat.getMainExecutor(this))fun bindPreview(cameraProvider : ProcessCameraProvider) {
var preview : Preview = Preview.Builder()
.build()
var cameraSelector : CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
// `SurfaceProvider`, 表示视频来源;
preview.setSurfaceProvider(previewView.getSurfaceProvider())
// 视频质量选择
val qualitySelector = QualitySelector.fromOrderedList(
listOf(Quality.FHD, Quality.HD, Quality.SD)
,FallbackStrategy.lowerQualityOrHigherThan(Quality.HD)
)
// Recorder 对象
val recorder = Recorder.Builder()
.setQualitySelector(qualitySelector).build()
// VideoCapture 对象
videoCapture = VideoCapture.withOutput(recorder)
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this as LifecycleOwner,
cameraSelector,
videoCapture,
preview
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}bindToLifecycle() 会返回一个 Camera 对象。如需详细了解如何控制相机输出(例如变焦和曝光),请参阅相机输出。
VideoCapture API
VideoCapture 既可以单独使用,也可以与其他用例搭配使用。受支持的具体组合取决于相机硬件功能,不过 Preview 和 VideoCapture 这一用例组合适用于所有设备。
VideoCapture API 包含可与应用通信的以下对象:
VideoCapture是顶级类()。VideoCapture通过CameraSelector和其他 CameraX 用例绑定到LifecycleOwner。如需详细了解这些概念和用法,请参阅 CameraX 架构。Recorder是与VideoCapture紧密耦合的 VideoOutput 实现。Recorder用于执行视频和音频捕获操作。应用通过Recorder创建录制对象。in short 可以理解为创建记录的对象PendingRecording会配置录制对象,同时提供启用音频和设置事件监听器等选项。您必须使用Recorder来创建PendingRecording。PendingRecording不会录制任何内容。Recording会执行实际录制操作。您必须使用PendingRecording来创建Recording。 in short 可以理解为每一次记录, 一个对象

配置和创建录制对象
流程大概是
- 使用
QualitySelector创建Recorder。 - 使用其中一个
OutputOptions配置Recorder。 - 如果需要,使用
withAudioEnabled()启用音频。 - 使用
VideoRecordEvent监听器调用start()以开始录制。 - 针对
Recording使用pause()/resume()/stop()来控制录制操作。 - 在事件监听器内响应
VideoRecordEvents。
貌似 VideoCapture 参与的不多, 它仅作为一个顶层类
fun bindPreview(cameraProvider : ProcessCameraProvider) {
var preview : Preview = Preview.Builder()
.build()
var cameraSelector : CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
// `SurfaceProvider`, 表示视频来源;
preview.setSurfaceProvider(previewView.getSurfaceProvider())
// 优先画质选择
val qualitySelector = QualitySelector.fromOrderedList(
listOf(Quality.FHD, Quality.HD, Quality.SD)
,FallbackStrategy.lowerQualityOrHigherThan(Quality.HD)
)
// 1. 使用 QualitySelector 创建 Recorder。
val recorder = Recorder.Builder()
.setQualitySelector(qualitySelector).build()
// 创建 VideoCapture 对象
videoCapture = VideoCapture.withOutput(recorder)
try {
cameraProvider.unbindAll()
// 绑定生命周期
cameraProvider.bindToLifecycle(
this as LifecycleOwner,
cameraSelector,
videoCapture,
preview
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
private fun startRecording() {
//输出配置
val fileOpt = FileOutputOptions.Builder(createVideoFile()).build()
// 2.使用其中一个 OutputOptions 配置 Recorder。
var prepareRecording = videoCapture.output.prepareRecording(this, fileOpt)
// 3.如果需要,使用 withAudioEnabled() 启用音频。
// prepareRecording.withAudioEnabled()
// 4. 使用 VideoRecordEvent 监听器调用 start() 以开始录制。
val recording = prepareRecording.start(
ContextCompat.getMainExecutor(this),
{
//6. 在事件监听器内响应 VideoRecordEvents。
if(it is VideoRecordEvent.Finalize){
// 完成录制
}
Log.i(TAG, "录制事件, 当前状态: $it")
}
)
// 5. 针对 Recording 使用 pause()/resume()/stop() 来控制录制操作。
//recording?.pause()
//recording?.stop()
}
您可以使用以下方法暂停、恢复和停止正在进行的 Recording:
请注意,无论录制处于暂停状态还是活跃状态,您都可以调用 stop() 来终止 Recording。
如果您已使用 PendingRecording.start() 注册了 EventListener,Recording 会使用 VideoRecordEvent 进行通信。