jetpack库之一的 CameraX。值得了解与学习
最初编写摄像头相关的应用时,使用的是 Camera.open() 接口。简单快捷。后台 Camera 接口标记为过时。而 Camera2 接口又相对复杂,于是很长一段时间,在编写 Demo 演示类 App 时,我依旧偏向使用
Camera.open()
接口。现在 google 有 Camerax 库,简化 Camera2 相机操作,那么自然要体验一下了
要求
- Android 最低 sdk版本为 21
- androidx 组件库最低 1.1.1
准备工作
创建一个空的工程,并导入依赖项
在项目 app 的 build.gradle
文件的 android{}
中,添加 java 1.8 要求
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
dependencies
中添加 Camerax 依赖库
implementation "androidx.camera:camera-core:1.0.0-beta10"
implementation "androidx.camera:camera-camera2:1.0.0-beta10"
// 上面两个是核心库,这个是用于 surface 预览需要的库
implementation "androidx.camera:camera-lifecycle:1.0.0-beta10"
implementation "androidx.camera:camera-view:1.0.0-alpha17"
添加权限配置
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
编写请求权限的代码
请求权限是使用 Camera 的必备操作
代码如下:
class MainActivity : AppCompatActivity() {
val CAMERA_PERMISSION = 1001
val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
reqCameraPermission();
} else {
openCameraPreview()
}
}
private fun reqCameraPermission() {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
CAMERA_PERMISSION
)
}
/**
* 打开相机预览
*/
private fun openCameraPreview() {
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == CAMERA_PERMISSION) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
reqCameraPermission()
} else {
openCameraPreview()
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
}
打开相机预览画面
相机预览不可避免的要用到 SurfaceView
,那么在 layout 中要定义布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/previewSurface"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
上面的 MainActivity
类,定义了一个 openCameraPreview()
方法,然后权限请求也都有了。现在只需要在这个方法中实现方法就好了。
这个方法的具体实现为下列这样:
/**
* 打开相机预览
*/
private fun openCameraPreview() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
val cameraProvider = cameraProviderFuture.get()
val surfaceView = findViewById<PreviewView>(R.id.previewSurface)
val preview = Preview.Builder()
.build().also {
it.setSurfaceProvider(surfaceView.surfaceProvider)
}
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
try {
cameraProvider.unbind()
cameraProvider.bindToLifecycle(this, cameraSelector, preview)
} catch (e: Exception) {
Log.w(TAG, e)
}
}, ContextCompat.getMainExecutor(this))
}
点击 AndroidStudio
的 run
按钮,稍等片刻,app 上便会看到相机的预览画面了
仅以预览页面来说,需要编写的代码量确实降低许多。而且可以感知生命周期,不需要在 onStart、onStop、onDestory 中做各种操作了
使用原来的 Camera api ,一旦生命周期管理不当,就有可能导致相机崩溃。而现在这些工作都交给库去处理
进阶使用
相机的使用一般是预览后拍摄图片。或者是实时的视频流获取,所以要尝试使用拍照和实时视屏流功能
拍照
在 xml 布局文件中增加一个按钮:
<Button
android:id="@+id/captureBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:text="拍照"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
然后在 MainActivity
中定义 ImageCapture
var imageCapture: ImageCapture? = null
private fun openCameraPreview() {
.........
// 修改 预览方法中 ,bindToLifecycle 的调用
imageCapture = ImageCapture.Builder()
.build()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
}
定义一个新的方法,并且为之前创建的 button 添加点击事件:
/**
* 截图图片
*/
private fun takePhoto() {
val imageCapture = imageCapture ?: return
// Create time-stamped output file to hold the image
val photoFile = File(
this.getExternalFilesDir("images"),
"test.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 ,然后 bindToLifecycle 方法中再增加绑定 ImageCapture ,录制视频也是同样的方式
需要使用的是 VideoCapture
videoCapture = VideoCapture.Builder()
.build()
try {
cameraProvider.unbind()
cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
videoCapture
)
} catch (e: Exception) {
Log.w(TAG, e)
}
注意:录制视频和拍照两者不能共存
所以在使用 VideoCapture
时,要将 ImageCapture
移除
否则会出现如下错误:
May be attempting to bind too many use cases. Existing surfaces: [] New configs:
然后定义两个按钮,一个开始,一个结束录制:
private fun recoverStart() {
val videoCapture = videoCapture ?: return
val mp4File = File(
this.getExternalFilesDir("video"),
"test.mp4"
)
val options = VideoCapture.OutputFileOptions.Builder(mp4File).build()
videoCapture.startRecording(options, Executors.newSingleThreadExecutor(),
object : VideoCapture.OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
Log.i(TAG, "onVideoSaved: saved")
}
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
Log.i(TAG, "onError: $videoCaptureError")
}
})
// 停止按钮调用
// videoCapture.stopRecording()
}
增加音频权限,并且代码中要动态申请权限
由于录制视频时,默认是有声音的,所以录制时必须要在清单文件中增加权限
<uses-permission android:name="android.permission.RECORD_AUDIO" />
如果没有音频权限在点击录制按钮时是会报错的!!!
成功录制视频:
目前录制视频功能还需要
@SuppressLint("RestrictedApi")
注释,否则会有一个红色警告,所以暂时还不推荐使用 CameraX 的库直接录制 mp4
获取实时视频流
拍照和录制是最基本的功能,而我需要获取实时的视频流,然后推送给某个程序进行处理。目前未找到 camerax 能够直接提供 Callback 的方法
也就是说目前 CameraX 暂时只能满足拍照、录制、预览等处理,对于实时视频流获取,依旧要使用 Camera2
后续
CameraX 出来也有一段时间了。目前很多网站上均有相关的教程和文章。
通过实际编写,发现部分接口与以前的教程相比,有略微的改动。
随着时间推移,可能本篇文章的部分方法和接口也会有所修改