1. CameraX架构
看官方文档 CameraX架构
有如下这一段话
使用CameraX,借助名为"用例"的抽象概念与设备的相机进行交互。
- 预览 : 接受用于显示预览的Surface,例如
PreviewView
- 图片分析 : 为分析 (例如机器学习) 提供CPU可访问的缓冲区
- 图片拍摄 : 拍摄并保存图片
- 视频拍摄 : 通过
VideoCapture
拍摄视频和音频
不同用例可以组合使用,也可以同时处于活跃状态。
例如,应用中可以加入预览用例,以便让用户查看进入相机视野的画面
加入图片分析用例,以确定照片里的人物是否在微笑
还可以加入图片拍摄用例,以便在人物微笑时拍摄照片
第一次看的时候,一脸懵逼,“用例”,是个什么鬼玩意。
后来,研究了一下,知道"用例"的英文原文叫做Use Case
,CameraX中的每一项操作,对应着一种UseCase
- 预览 :
Preview.java
- 图片分析 :
ImageAnalysis.java
- 图片拍摄 :
ImageCapture.java
- 视频拍摄 :
VideoCapture.java
可以看到,这几个类都是继承自UseCase.java
类的
public final class Preview extends UseCase {//...}public final class ImageAnalysis extends UseCase {//...}public final class ImageCapture extends UseCase {//...}public final class VideoCapture extends UseCase {//...}
接下来让我们来尝试使用一下。
2. 前置操作
首先,我们需要新建一个项目,然后引入依赖
// CameraX core library using the camera2 implementationdef camerax_version = "1.2.0-alpha02" //1.2.0-alpha02// The following line is optional, as the core library is included indirectly by camera-camera2implementation "androidx.camera:camera-core:${camerax_version}"implementation "androidx.camera:camera-camera2:${camerax_version}"// If you want to additionally use the CameraX Lifecycle libraryimplementation "androidx.camera:camera-lifecycle:${camerax_version}"// If you want to additionally use the CameraX VideoCapture libraryimplementation "androidx.camera:camera-video:${camerax_version}"// If you want to additionally use the CameraX View classimplementation "androidx.camera:camera-view:${camerax_version}"// If you want to additionally add CameraX ML Kit Vision Integrationimplementation "androidx.camera:camera-mlkit-vision:${camerax_version}"// If you want to additionally use the CameraX Extensions libraryimplementation "androidx.camera:camera-extensions:${camerax_version}"
在AndroidManifest.xml
里添加权限
<uses-permission android:name="android.permission.CAMERA" /><uses-feature android:name="android.hardware.camera.any" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.RECORD_AUDIO" />
别忘了申请权限
ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO ), 123 )
3. 预览 : Preview.java
首先修改activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/camera_container" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/black"> <androidx.camera.view.PreviewView android:id="@+id/previewView" 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.kt
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var cameraProvider: ProcessCameraProvider private var preview: Preview? = null private var camera: Camera? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) //TODO 省略了权限申请,具体看文章中 "前置操作" 部分 setUpCamera(binding.previewView) } private fun setUpCamera(previewView: PreviewView) { val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ try { cameraProvider = cameraProviderFuture.get() bindPreview(cameraProvider, previewView) } catch (e: Exception) { e.printStackTrace() } }, ContextCompat.getMainExecutor(this)) } private fun bindPreview( cameraProvider: ProcessCameraProvider, previewView: PreviewView ) { //解除所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常 cameraProvider.unbindAll() preview = Preview.Builder().build() camera = cameraProvider.bindToLifecycle( this, CameraSelector.DEFAULT_BACK_CAMERA, preview ) preview?.setSurfaceProvider(previewView.surfaceProvider) }}
看下效果
4. 图像分析 : ImageAnalysis.java
图像分析用例ImageAnalysis
为应用提供可实时分析的图像数据,我们可以对这些图像执行图像处理、计算机视觉或机器学习推断。
val imageAnalysis = ImageAnalysis.Builder() // enable the following line if RGBA output is needed. // .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888) .setTargetResolution(Size(1280, 720)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build()imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { imageProxy -> val rotationDegrees = imageProxy.imageInfo.rotationDegrees // insert your code here. // 在这里处理图片的解析,比如解析成二维码之类的 ... // after done, release the ImageProxy object imageProxy.close()})
在调用cameraProvider.bindToLifecycle()
时,进行传入
cameraProvider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis)
5. 拍照 : ImageCapture.java
5.1 仅拍照
这里,我们需要先创建一个imageCapture
imageCapture = ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)//.setTargetAspectRatio(screenAspectRatio)//.setTargetRotation(binding.previewView.display.rotation).build()
然后,在调用cameraProvider.bindToLifecycle()
时,进行传入
camera = cameraProvider.bindToLifecycle( this,CameraSelector.DEFAULT_BACK_CAMERA, preview, imageCapture)
增加takePicture()
方法进行拍照
//进行拍照private fun takePicture() { imageCapture?.let { imageCapture -> val mainExecutor = ContextCompat.getMainExecutor(this) imageCapture.takePicture(mainExecutor, object : ImageCapture.OnImageCapturedCallback() { override fun onCaptureSuccess(image: ImageProxy) { super.onCaptureSuccess(image) } override fun onError(exception: ImageCaptureException) { super.onError(exception) } }) // 让画面闪一下,营造拍照的感觉 // We can only change the foreground Drawable using API level 23+ API if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Display flash animation to indicate that photo was captured binding.root.postDelayed({ binding.root.foreground = ColorDrawable(Color.WHITE) binding.root.postDelayed( { binding.root.foreground = null }, 50L ) }, 100L) } }}
5.2 拍照并保存到本地存储
我们也可以拍照后,保存到本地存储中
private fun createFile(baseFolder: File, format: String, extension: String) = File( baseFolder, SimpleDateFormat(format, Locale.US) .format(System.currentTimeMillis()) + extension )fun getOutputDirectory(context: Context): File { val appContext = context.applicationContext val mediaDir = context.externalMediaDirs.firstOrNull()?.let { File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } } return if (mediaDir != null && mediaDir.exists()) mediaDir else appContext.filesDir}companion object { private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS" private const val PHOTO_EXTENSION = ".jpg"}//进行拍照并保存到本地private fun takePictureSaveToDisk() { imageCapture?.let { imageCapture -> // Create output file to hold the image val photoFile = createFile(getOutputDirectory(this), FILENAME, PHOTO_EXTENSION) Log.i(TAG, "photoFile:$photoFile") // Setup image capture metadata val metadata = ImageCapture.Metadata().apply { // Mirror image when using the front camera isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT } // Create output options object which contains file + metadata val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile) .setMetadata(metadata) .build() // Setup 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 = output.savedUri ?: Uri.fromFile(photoFile) Log.d(TAG, "Photo capture succeeded: $savedUri") // Implicit broadcasts will be ignored for devices running API level >= 24 // so if you only target API level 24+ you can remove this statement if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { application.sendBroadcast(Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri) ) } // If the folder selected is an external media directory, this is // unnecessary but otherwise other apps will not be able to access our // images unless we scan them using [MediaScannerConnection] val mimeType = MimeTypeMap.getSingleton() .getMimeTypeFromExtension(savedUri.toFile().extension) MediaScannerConnection.scanFile( application, arrayOf(savedUri.toFile().absolutePath), arrayOf(mimeType) ) { _, uri -> Log.d(TAG, "Image capture scanned into media store: $uri") } } }) // 让画面闪一下,营造拍照的感觉 // We can only change the foreground Drawable using API level 23+ API if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Display flash animation to indicate that photo was captured binding.root.postDelayed({ binding.root.foreground = ColorDrawable(Color.WHITE) binding.root.postDelayed( { binding.root.foreground = null }, 50L ) }, 100L) } }}
然后,我们可以在相册里找到这张图片了,图片的真实位置位于/storage/emulated/0/Android/media/你的包名/项目名/
中。
6. 视频录制 : VideoCapture.java
视频录制用的是VideoCapture
videoCapture = VideoCapture.Builder()//.setTargetRotation(previewView.getDisplay().getRotation()).setVideoFrameRate(25).setBitRate(3 * 1024 * 1024).build()
在调用cameraProvider.bindToLifecycle()
时,进行传入。
camera = cameraProvider.bindToLifecycle( this,CameraSelector.DEFAULT_BACK_CAMERA, preview, videoCapture )
需要注意的是,videoCapture
无法和imageAnalysis
、imageCapture
一起使用。
如果同一个页面中这几个功能融合在一起,则需要通过标志位来进行判断。
if (isVideo) { mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture);} else { mCamera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, imageAnalysis);}
开始录制
private val RECORDED_FILE_NAME = "recorded_video"private val RECORDED_FILE_NAME_END = "video/mp4"@SuppressLint("RestrictedApi")private fun startRecording() {//TODO 这里省略了RECORD_AUDIO、PERMISSION_GRANTED权限的判断 val contentValues = ContentValues() contentValues.put( MediaStore.MediaColumns.DISPLAY_NAME, RECORDED_FILE_NAME + "_" + System.currentTimeMillis() ) contentValues.put(MediaStore.MediaColumns.MIME_TYPE, RECORDED_FILE_NAME_END) val outputFileOptions = VideoCapture.OutputFileOptions.Builder( getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues ).build() videoCapture.startRecording( outputFileOptions, ContextCompat.getMainExecutor(this), object : VideoCapture.OnVideoSavedCallback { override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) { Log.i(TAG, "视频保存成功:${outputFileResults.savedUri}") } override fun onError( videoCaptureError: Int, message: String, cause: Throwable? ) { Log.i(TAG, "当出现异常 cause:$cause") } } )}
停止视频录制
videoCapture.stopRecording()
当我们执行停止视频录制
之后,就可以在相册里看到多了一个录制的视频了。
介绍了CameraX
里一些常用的UseCase
,我们接下来来看下CameraX
中的其他一些功能。
7. 切换前后摄像头
我们之前使用cameraProvider.bindToLifecycle()
的时候,有一个参数是CameraSelector
。
CameraX
默认给我们提供了前置摄像头和后置摄像头的CameraSelector
public final class CameraSelector { @NonNull public static final CameraSelector DEFAULT_FRONT_CAMERA = new CameraSelector.Builder().requireLensFacing(LENS_FACING_FRONT).build(); @NonNull public static final CameraSelector DEFAULT_BACK_CAMERA = new CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build();//...}
我们去切换摄像头的时候,就是重新调用一下bindPreview
方法,传入新的cameraSelector
值就好了
private fun bindPreview( cameraProvider: ProcessCameraProvider, previewView: PreviewView, cameraSelector : CameraSelector ) { // 解除所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常 cameraProvider.unbindAll() preview = Preview.Builder().build() camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview ) preview?.setSurfaceProvider(previewView.surfaceProvider) }
CameraX
还为我们提供了判断前置/后置摄像头
是否存在的方法
private fun hasBackCamera(): Boolean { return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false}private fun hasFrontCamera(): Boolean { return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false}
看下效果
8. 对焦
当点击androidx.camera.view.PreviewView
的时候,去调用CameraX
的对焦方法startFocusAndMetering
()就好了。
在onCreate()
中添加如下代码
binding.previewView.setOnTouchListener { view, event -> val action = FocusMeteringAction.Builder( binding.previewView.getMeteringPointFactory() .createPoint(event.getX(), event.getY()) ).build(); showTapView(event.x.toInt(), event.y.toInt()) camera?.getCameraControl()?.startFocusAndMetering(action) true}
增加showTapView()
private fun showTapView(x: Int, y: Int) { val popupWindow = PopupWindow( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ) val imageView = ImageView(this) imageView.setImageResource(R.drawable.ic_focus_view) popupWindow.contentView = imageView popupWindow.showAsDropDown(binding.previewView, x, y) binding.previewView.postDelayed({ popupWindow.dismiss() }, 600) binding.previewView.playSoundEffect(SoundEffectConstants.CLICK)}
看下效果
9. 缩放
通过GestureDetector
监听缩放事件,然后在回调的时候进行执行如下代码,就可以使用双指放大缩小图像
override fun zoom(delta: Float) {val zoomState = camera?.cameraInfo?.zoomStatezoomState?.value?.let {val currentZoomRatio = it.zoomRatiocamera?.cameraControl?.setZoomRatio(currentZoomRatio * delta)}}
缩放操作具体详见这篇文章 Android使用CameraX实现相机快速实现放大缩小
10. 本文Demo下载
本文的源码Demo详见 : Android CameraX Demo : 实现预览/拍照/录制视频/图片分析/对焦/切换摄像头等操作
推荐阅读 我的另一篇关于Camera的文章 : Android 从零开发一个简易的相机App
来源地址:https://blog.csdn.net/EthanCo/article/details/125603671