CameraX的使用
这是我参与「第四届青训营 」笔记创作活动的第3天
CameraX 是一个 Jetpack 支持库,目的是简化Camera的开发工作,它是基于Camera2 API的基础,向后兼容至 Android 5.0(API 级别 21)。 它有以下几个特性:
易用性
CameraX 引入了多个用例,使您可以专注于需要完成的任务,而无需花时间处理不同设备之间的细微差别;如:预览用例,图片分析用例,图片拍摄用例等。
新的相机体验
CameraX 有一个名为 Extensions 的可选插件,开发者可以通过扩展的形式使用和原生摄像头应用同样的功能(如:人像、夜间模式、HDR、滤镜、美颜)
生命周期管理
CameraX 和 Lifecycle 结合在一起,方便开发者管理生命周期。且相比较 camera2 减少了大量样板代码的使用
确保各设备间的一致性
Google 自己还打造了 CameraX 自动化测试实验室,对摄像头功能进行深度测试,确保能覆盖到更加广泛的设备。相当于 Google 帮我们把设备兼容测试工作给做了。
导入依赖
首先我们来导入依赖:最新版本请看CameraX | Android 开发者 | Android Developers (google.cn)
//核心库必须依赖
implementation("androidx.camera:camera-core:1.2.0-alpha04")
implementation("androidx.camera:camera-camera2:1.2.0-alpha04")
//如果想另外使用 CameraX Lifecycle 库
implementation("androidx.camera:camera-lifecycle:1.2.0-alpha04")
//如果使用cameraView,previewView
implementation("androidx.camera:camera-view:1.2.0-alpha04")
implementation("androidx.camera:camera-extensions:1.2.0-alpha04")
//如果想拍摄video
implementation "androidx.camera:camera-video:1.2.0-alpha04"
添加权限
记得要在AndroidManifest.xml文件中添加对应的权限
<uses-feature android:name="android.hardware.camera.any" /><!-- 使用uses-feature指定需要相机资源 -->
<uses-feature android:name="android.hardware.camera.autofocus" /> <!-- 需要自动聚焦 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><!-- 存储图片或者视频 -->
//如果安卓的版本大于11,访问本地文件需要添加这行代码在application中
android:requestLegacyExternalStorage="true"
动态授权
在Activity中添加
companion object {
const val REQUEST_CODE_PERMISSIONS = 10
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private val REQUIRED_PERMISSIONS = mutableListOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
).toTypedArray()
}
private lateinit var cameraSelector : CameraSelector
private lateinit var preview: Preview//预览对象
private lateinit var cameraProvider: ProcessCameraProvider//相机信息
private lateinit var camera: Camera//相机对象
private var imageCapture: ImageCapture? = null
private lateinit var cameraExecutor: ExecutorService
private fun initPermission(){
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,"授权失败!",Toast.LENGTH_SHORT).show()
finish()
}
}
}
编写相机界面
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
//相机预览界面
<androidx.camera.view.PreviewView
android:id="@+id/camera_preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.8" />
//相机拍照按钮
<ImageView
android:id="@+id/camera_click"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_camera_click"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="20dp"/>
//前后置相机切换
<ImageView
android:id="@+id/camera_switch"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginTop="30dp"
android:src="@drawable/ic_camera_switch"
app:layout_constraintEnd_toStartOf="@+id/guideline1"
app:layout_constraintTop_toTopOf="parent"
android:elevation="1dp"/>
//闪光灯模式切换
<ImageView
android:id="@+id/camera_flash"
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/ic_camera_flash_close"
app:layout_constraintEnd_toStartOf="@+id/guideline1"
app:layout_constraintTop_toBottomOf="@+id/camera_switch"
android:layout_marginTop="20dp"
android:elevation="1dp"/>
//返回键
<ImageView
android:id="@+id/camera_close"
android:layout_width="34dp"
android:layout_height="34dp"
android:src="@drawable/ic_camera_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/camera_switch"
android:layout_marginStart="20dp"
android:elevation="1dp"/>
//相册以及拍照完成后预览图
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/camera_album"
android:layout_width="50dp"
android:layout_height="50dp"
app:layout_constraintBottom_toBottomOf="@+id/camera_click"
app:layout_constraintEnd_toStartOf="@+id/guideline1"
app:layout_constraintStart_toEndOf="@+id/camera_click"
app:layout_constraintTop_toTopOf="@+id/camera_click"
app:civ_border_width="1dp"
android:src="@color/black"
app:civ_border_color="@color/white"/>
</androidx.constraintlayout.widget.ConstraintLayout>
启动相机
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// 将相机的生命周期绑定到应用进程
cameraProvider = cameraProviderFuture.get()
//预览配置
preview = Preview.Builder().build()
.also {
//提供previewView预览控件
it.setSurfaceProvider(cameraPreview.surfaceProvider)
}
imageCapture = ImageCapture.Builder().build()
imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF//设置默认闪光灯模式
cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK) //设置默认后置摄像头
.build()
try {
bindPreview()
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun bindPreview(){
cameraProvider.unbindAll()//防止之前绑定过,先解绑所有用例
camera = cameraProvider.bindToLifecycle(
this,
cameraSelector,
imageCapture,
preview)
//绑定用例
}
这样子就能够从预览界面中看到相机拍摄的画面了
拍照
private fun takePhoto() {
val imageCapture = imageCapture ?: return
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
.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")
}
}
// 创建包含文件 + 数据的输出对象,拍照完成后会保存在相册中
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
// 设置图片捕获监听器,拍照后触发
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){
Glide.with(cameraAlbum).load(output.savedUri).into(cameraAlbum)//拍照完后显示在图片预览中
}
}
)
}
切换前后置摄像头
cameraSelector =
if (cameraSelector.lensFacing == CameraSelector.LENS_FACING_BACK){
CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build()
}else{
CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
}
//改变配置时需要重新绑定
bindPreview()
切换闪光灯模式
when(imageCapture?.flashMode){
ImageCapture.FLASH_MODE_AUTO ->{
imageCapture!!.flashMode = ImageCapture.FLASH_MODE_ON
cameraFlash.setImageResource(R.drawable.ic_camera_flash_on)
}
ImageCapture.FLASH_MODE_OFF ->{
imageCapture!!.flashMode = ImageCapture.FLASH_MODE_AUTO
cameraFlash.setImageResource(R.drawable.ic_camera_flash_auto)
}
ImageCapture.FLASH_MODE_ON ->{
imageCapture!!.flashMode = ImageCapture.FLASH_MODE_OFF
cameraFlash.setImageResource(R.drawable.ic_camera_flash_close)
}
else->{}
}
实现聚焦显示
//设置触碰监听器
cameraPreview.setOnTouchListener { _, motionEvent ->
if (motionEvent.action == MotionEvent.ACTION_UP){
if (isOneClick){
val point = cameraPreview.meteringPointFactory
.createPoint(motionEvent.x, motionEvent.y)
val action = FocusMeteringAction.Builder(point,FLAG_AF)
.setAutoCancelDuration(3,TimeUnit.SECONDS)
.build()
showTapView(motionEvent.rawX.toInt(), motionEvent.rawY.toInt())
camera.cameraControl.startFocusAndMetering(action)
}
isOneClick = false
isZoomOver = false
}
if (motionEvent.pointerCount == 1){
if(!isZoomOver) isOneClick = true
}else{
//实现双指放大缩小
}
true
}
//显示聚焦图标
private fun showTapView(x: Int, y: Int) {
val size = resources.getDimension(R.dimen.DP70).toInt()
val popupWindow = PopupWindow(size, size)
val imageView = ImageView(this)
imageView.setImageResource(R.drawable.ic_focus)
popupWindow.animationStyle = R.style.camera_focus_anim_style//自己实现的聚焦动画
popupWindow.contentView = imageView
popupWindow.showAsDropDown(cameraPreview, x-size/2, y)
cameraPreview.postDelayed({ popupWindow.dismiss() }, 1000)
cameraPreview.playSoundEffect(SoundEffectConstants.CLICK)
}
实现双指放大缩小
在上面的聚焦显示的代码里添加
isOneClick = false
when (motionEvent.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> oldDist = getFingerSpacing(motionEvent)
MotionEvent.ACTION_MOVE -> {
val newDist: Float = getFingerSpacing(motionEvent)
if (newDist > oldDist) handleZoom(true)
else if (newDist < oldDist) handleZoom(false)
oldDist = newDist
isZoomOver = true
}
}
//计算手指的间距
private fun getFingerSpacing(event: MotionEvent): Float {
val x = event.getX(0) - event.getX(1)
val y = event.getY(0) - event.getY(1)
return sqrt((x * x + y * y).toDouble()).toFloat()
}
//根据双指移动时进行缩放
private fun handleZoom(isZoomIn : Boolean) {
if (camera.cameraInfo.zoomState.value != null){
val currentZoom = camera.cameraInfo.zoomState.value!!.zoomRatio
val maxZoom = camera.cameraInfo.zoomState.value!!.maxZoomRatio
if (isZoomIn && currentZoom < maxZoom) camera.cameraControl.setZoomRatio(currentZoom + 0.08F)//双指移动时缩放的速度
else if (currentZoom > 0) camera.cameraControl.setZoomRatio(currentZoom - 0.08F)
}
}