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


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

    首先,新建一个项目,选择Empty Activity。
    在这里插入图片描述

    一、增加依赖使用CameraX

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

    	//CameraX
        val camerax_version = "1.3.0-alpha04"
        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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    二、申请权限

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

    
    <uses-feature android:name="android.hardware.camera"   
                  android:required="false" />     
    <uses-permission   
                  android:name="android.permission.CAMERA" />
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.申请使用拍照的权限

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

    @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{
           //未授权,显示未授权的界面    
           } 
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    三、定义未授权的界面

    @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("请求授权")
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    通过定义按钮的点击动作,请求调用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。
    图片来源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,该对象没有专用的绘图表面。因此,视频要通过混合渲染,才能显示。在这个额外的步骤中,应用可以执行额外的处理工作,例如不受限制地缩放和旋转视频。

    @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()
                    }
                )
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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

    @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)
           } 
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    测试一下上述的代码,模拟器运行效果如下图所示:
    在这里插入图片描述
    但是,从这个运行效果可以发现,它仅仅是显示相机预览的画面,功能有限。因此,需要增加其他的功能。

    四、拍照的处理

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

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

    • takePicture(OutputFileOptions, Executor,OnImageSavedCallback):此方法将拍摄的图片保存到提供的文件位置。 运行
      ImageCapture可自定义执行程序有两种类型:回调执行程序和 IO 执行程序。

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

    @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()
                    }
                )
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    运行效果在模拟器中如下图所示:
    在这里插入图片描述
    可以注意到,上述代码虽然可以将拍下的图片生成一个Bitmap对象,但是并没有将这个图片保存到手机中。因此可以考虑通过调用 takePicture(OutputFileOptions, Executor,OnImageSavedCallback)函数来实现拍照并保存图片的功能。
    为此,做如下的处理:

    (1)自定义一个函数capturePicture:执行拍照功能,并将拍下的图片保存到文件中。
    (2)修改CameraContent组合函数,调用capturePicture函数实现拍照并保存照片的功能。

    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","保存照片失败")
            }
    
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    @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()
                    }
                )
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    运行界面如上图所示。执行多次拍照后,启动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

  • 相关阅读:
    论文阅读:H-ViT,一种用于医学图像配准的层级化ViT
    记录下双系统
    leetcode做题笔记189. 轮转数组
    【学习记录】tensorflow图像数据增强
    Apache Druid 数据摄取---本地数据和kafka流式数据
    GO泛型相关
    web前端期末大作业 HTML+CSS+JavaScript仿京东
    前端JavaScript中的 == 和 ===区别,以及他们的应用场景,快来看看吧,积累一点知识。
    Android:RecyclerView的ChildHelper
    OTN网络技术—(OTUk、ODUk、OPUk)OTN速率解析
  • 原文地址:https://blog.csdn.net/userhu2012/article/details/134075222