Android笔记(八):基于CameraX库结合Compose和传统视图组件PreviewView实现照相机画面预览和照相功能

CameraX是JetPack库之一,通过CameraX可以向应用增加相机的功能。在下列内容中,将介绍一个结合CameraX实现一个简单的拍照应用。本应用必须采用Android SDK 34。并通过该简单示例,了解传统View层次组件的UI组件如何与Compose组件结合实现移动应用界面的定制。

首先,新建一个项目,选择Empty Activity。

一、增加依赖使用CameraX

在新建项目的模块build.gradle.kt中增加依赖如下所示:

c 复制代码
	//CameraX
    val camerax_version = "1.3.0-beta04"
    implementation("androidx.camera:camera-core:$camerax_version")
    implementation("androidx.camera:camera-camera2:${camerax_version}")
    implementation("androidx.camera:camera-lifecycle:${camerax_version}")
    implementation("androidx.camera:camera-video:${camerax_version}")

    implementation("androidx.camera:camera-view:${camerax_version}" )
    implementation("androidx.camera:camera-extensions:${camerax_version}")
    
    //accompanist处理权限依赖库
    val accompanist_version = "0.31.2-alpha"
    implementation("com.google.accompanist:accompanist-permissions:$accompanist_version")

二、申请权限

1.AndroidManifest.xml配置使用照相机特性和权限

c 复制代码
<uses-feature android:name="android.hardware.camera"   
              android:required="false" />     
<uses-permission   
              android:name="android.permission.CAMERA" />

2.申请使用拍照的权限

使用rememberPermissionState函数申请权限,该函数的参数为permission,表示需要申请的权限。rememberPermissionState函数的返回值为PermissionState类型,表示获取权限的状态。

c 复制代码
@OptIn(ExperimentalPermissionsApi::class) 
@Preview 
@Composable 
fun CameraScreen() {    
	val cameraPermissionState = rememberPermissionState(permission = android.Manifest.permission.CAMERA)    
	LaunchedEffect(key1 = Unit){
       if(!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale){
           cameraPermissionState.launchPermissionRequest()
       }    
	}

   if(cameraPermissionState.status.isGranted){
       //接受拍照的授权
       }
 
  else{
       //未授权,显示未授权的界面    
       } 
 }

在上述代码中,因为通过cameraPermissionState.launchPermissionRequest()请求申请照相机权限是一个异步过程,因此,将这个请求的处理放置在LaunchedEffect代码段内。

三、定义未授权的界面

c 复制代码
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoPermissionScreen(cameraPermissionState:  PermissionState) {
    Column(horizontalAlignment = Alignment.CenterHorizontally){
        val message = if(cameraPermissionState.status.shouldShowRationale){
            "未获取照相机权限导致无法使用照相机功能"
        }else{
            "请授权照相机的权限"
        }
        Text(message)
        Spacer(modifier = Modifier.height(8.dp))
        Button(onClick={
            cameraPermissionState.launchPermissionRequest()
        }){
            Text("请求授权")
        }
    }
}

通过定义按钮的点击动作,请求调用cameraPermissionState.launchPermissionRequest()再次申请权限。运行界面如下图所示:

四、结合PreviewView实现预览相机画面

PreviewView,这是一种可以剪裁、缩放和旋转以确保正确显示的 传统的视图组件。

可以结合Preview将处于活动状态的相机中的图片预览会流式传输到 PreviewView 中的 Surface。因为是传统的View系统的组件,并不是Compose组件,因此需要在Compose界面中利用AndroidView来使用View系统的组件。

PreviewView中有些属性需要注意:

(1)缩放类型

相机预览视频分辨率常常与PreviewView 的尺寸不同,需要通过设置缩放类型scaleType来剪裁或添加遮幅式黑边使得预览画面来适应视图(保持原始宽高比)。

FIT_CENTER、FIT_START 和 FIT_END,用于添加遮幅式黑边。整个视频内容会调整(放大或缩小)为可在目标 PreviewView 中显示的最大尺寸。预览的内容会被完整显示。但是可能出现空白部分,每一帧的画面会与FIT_CENTER目标视图的中心、FIT_START起始或FIT_END结束位置对齐。CameraX 使用的默认缩放类型是 FILL_CENTER。

三种设置的实现效果如下图所示,图片来源https://developer.android.google.cn/static/images/training/camera/camerax/camera-preview/camera_preview_view_scale_type_fit.png?hl=zh-cn。

(2)渲染画面的实现模式

PreviewView 的属性implementationMode用来设置画面渲染的实现模式,实现模式主要有两种:

PERFORMANCE 是默认模式。PreviewView 会使用 SurfaceView 显示视频串流,但在某些情况下会回退为使用 TextureView。SurfaceView 具有专用的绘图界面,该对象更有可能通过内部硬件合成器实现硬件叠加层,尤其是当预览视频上面没有其他界面元素(如按钮)时。通过使用硬件叠加层进行渲染,视频帧会避开 GPU 路径,从而能降低平台功耗并缩短延迟时间。

COMPATIBLE 模式。在此模式下,PreviewView 会使用 TextureView;不同于 SurfaceView,该对象没有专用的绘图表面。因此,视频要通过混合渲染,才能显示。在这个额外的步骤中,应用可以执行额外的处理工作,例如不受限制地缩放和旋转视频。

c 复制代码
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CameraContent() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember{LifecycleCameraController(context)}

    Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues: PaddingValues ->
        Box(modifier = Modifier.fillMaxSize()) {
            //在Compose中使用View系统中的PreviewView
            AndroidView(modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues),
                factory = { context ->
                    PreviewView(context).apply {
                        //设置布局宽度和高度占据全屏
                        layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                        //设置背景颜色
                        setBackgroundColor(Color.BLACK)
                        //设置渲染的实现模式
                        implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                        //设置缩放方式
                        scaleType = PreviewView.ScaleType.FILL_START
                    }.also{
                        it.controller = cameraController
                        cameraController.bindToLifecycle(lifecycleOwner)
                    }
                },
                onReset = {},
                onRelease = {
                    cameraController.unbind()
                }
            )
        }
    }
}

这时,重新修改CameraScreen,代码如下:

c 复制代码
@OptIn(ExperimentalPermissionsApi::class) 
@Preview 
@Composable 
fun CameraScreen() {    
	val cameraPermissionState = rememberPermissionState(permission = android.Manifest.permission.CAMERA)    
	LaunchedEffect(key1 = Unit){
       if(!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale){
           cameraPermissionState.launchPermissionRequest()
       }    
	}

   if(cameraPermissionState.status.isGranted){
       //接受拍照的授权
        CameraContent()    
       }
 
  else{
       //未授权,显示未授权的界面    
       NoPermissionScreen(cameraPermissionState)
       } 
 }

测试一下上述的代码,模拟器运行效果如下图所示:

但是,从这个运行效果可以发现,它仅仅是显示相机预览的画面,功能有限。因此,需要增加其他的功能。

四、拍照的处理

CameraX提供了拍照的功能,通过调用takePicture来实现拍照。takePicture函数有两种形式:

  • takePicture(Executor, OnImageCapturedCallback):此方法为拍摄的图片提供内存缓冲区。

  • takePicture(OutputFileOptions, Executor,OnImageSavedCallback):此方法将拍摄的图片保存到提供的文件位置。 运行

    ImageCapture可自定义执行程序有两种类型:回调执行程序和 IO 执行程序。

下列代码展示了一个简单的拍照功能,通过调用takePicture(Executor, OnImageCapturedCallback)函数来实现。但是在下列代码中,拍下的图片并没有保存,仅仅是生成了一个Bitmap对象

c 复制代码
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CameraContent() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember{LifecycleCameraController(context)}
    //请求关联context的主线程的Excutor
    val cameraExecutor = ContextCompat.getMainExecutor(context)

    Scaffold(modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
            FloatingActionButton(onClick = {
                cameraController.takePicture(cameraExecutor,object : ImageCapture.OnImageCapturedCallback(){
                    override fun onCaptureSuccess(image: ImageProxy) {
                        super.onCaptureSuccess(image)
                        //拍照得到图片
                        val cameraBitmap = image.toBitmap()
                    }
                    override fun onError(exception: ImageCaptureException) {
            			//拍照失败的处理
        			}
                })
            }) {
                Icon(modifier = Modifier.clip(CircleShape), 
                     painter = painterResource(id = R.mipmap.len),
                     contentDescription = "照相机")
            }
        },
        floatingActionButtonPosition = FabPosition.Center) { paddingValues: PaddingValues ->
        Box(modifier = Modifier.fillMaxSize()) {
            //在Compose中使用View系统中的PreviewView
            AndroidView(modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues),
                factory = { context ->
                    PreviewView(context).apply {
                        layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                        //设置背景颜色
                        setBackgroundColor(Color.BLACK)
                        //设置渲染模式
                        implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                        //设置缩放方式
                        scaleType = PreviewView.ScaleType.FILL_START
                    }.also{
                        it.controller = cameraController
                        cameraController.bindToLifecycle(lifecycleOwner)
                    }
                },
                onReset = {},
                onRelease = {
                    cameraController.unbind()
                }
            )
        }
    }
}

运行效果在模拟器中如下图所示:

可以注意到,上述代码虽然可以将拍下的图片生成一个Bitmap对象,但是并没有将这个图片保存到手机中。因此可以考虑通过调用 takePicture(OutputFileOptions, Executor,OnImageSavedCallback)函数来实现拍照并保存图片的功能。

为此,做如下的处理:

(1)自定义一个函数capturePicture:执行拍照功能,并将拍下的图片保存到文件中。

(2)修改CameraContent组合函数,调用capturePicture函数实现拍照并保存照片的功能。

c 复制代码
fun capturePicture(cameraController:LifecycleCameraController,cameraExecutor:Executor){
	//创建文件名以img为开始,扩展名为jpg的临时文件
    val file = File.createTempFile("img",".jpg")
    //配置最新拍下照片文件的位置和元数据
    val outputFileOptions = ImageCapture.OutputFileOptions.Builder(file).build()
    //定义拍照,cameraExecutor处理拍照后的回调 object:ImageCapture.OnImageSavedCallback创建匿名对象对捕获图片保存
    cameraController.takePicture(outputFileOptions,cameraExecutor,object:ImageCapture.OnImageSavedCallback{
        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
            //拍照的图片成功保存的处理
            Log.d("CAMERA_APP","成功保存照片在:${outputFileResults.savedUri}")
        }
     
        override fun onError(exception: ImageCaptureException) {
            //拍照的图片保存失败的处理
            Log.d("CAMERA_APP","保存照片失败")
        }

    })
}

修改CameraContent函数,修改拍照的处理:

c 复制代码
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CameraContent() {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember{LifecycleCameraController(context)}
    //请求关联context的主线程的Excutor
    val cameraExecutor = ContextCompat.getMainExecutor(context)

    Scaffold(modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
            FloatingActionButton(onClick = {
                //处理拍照
                capturePicture(cameraController,cameraExecutor)
            }) {
                Icon(modifier = Modifier.clip(CircleShape).size(24.dp,24.dp),
                    painter = painterResource(id = R.mipmap.len),contentDescription = "照相机")
            }
        },
        floatingActionButtonPosition = FabPosition.Center) { paddingValues: PaddingValues ->
        Box(modifier = Modifier.fillMaxSize()) {
            //在Compose中使用View系统中的PreviewView
            AndroidView(modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues),
                factory = { context ->
                    PreviewView(context).apply {
                        layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                        //设置背景颜色
                        setBackgroundColor(Color.BLACK)
                        //设置渲染模式
                        implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                        //设置缩放方式
                        scaleType = PreviewView.ScaleType.FILL_START
                    }.also{
                        it.controller = cameraController
                        cameraController.bindToLifecycle(lifecycleOwner)
                    }
                },
                onReset = {},
                onRelease = {
                    cameraController.unbind()
                }
            )
        }
    }
}

运行界面如上图所示。执行多次拍照后,启动Android Studio的Device Explorer,在data/data目录中找到创建的项目模块包名:

在包名的下级目录cache中可以发现多次拍照的图片,也可以根据跟踪日志发现保存图片文件的路径,如下图所示。

参考文献

(1) ImageCapture.Builder https://developer.android.google.cn/reference/androidx/camera/core/ImageCapture.Builder#setIoExecutor(java.util.concurrent.Executor)

(2)CameraX概览

https://developer.android.google.cn/training/camerax?hl=zh-cn

相关推荐
一隅论数智3 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
深圳老胡3 天前
STM32F407 控制 L6470 步进电机驱动 —— 控制过程简介
笔记·stm32·单片机·嵌入式硬件·代码规范
Because_of_Her13 天前
并查集-听课笔记
笔记·算法·并查集
彧azz3 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android