Compose案例 — Android 调用系统相机拍照

0. 概览

在Android中,要想实现调用系统相机完成拍照显示功能,可以分为以下几个步骤:

  1. 申请对应的权限(相机权限与读写文件权限)
  2. 添加 fileProvider, 来读取相机生成的图片文件。
  3. 程序启动拍照时,动态检查与申请权限。
  4. 通过 startActivity 启动相机,进行拍照
  5. 拍照完成后,通过图片Uri获取图片。

1. 申请权限

  • 调用系统相机需要申请相机权限 android.permission.CAMERA
  • 这里我们使用程序缓存目录,不需要额外申请读写文件权限。

AndroidManifest.xml

xml 复制代码
<!--  声明使用相机功能,该项配置非必填项,展示为了向应用商店展示应用的功能与相机相关  -->
<uses-feature
    android:name="android.hardware.camera"
    android:required="true" />
<!--  相机权限:必填项,调用系统相机必须具备相机权限  -->
<uses-permission android:name="android.permission.CAMERA" />

2. 添加 fileProvider

在Android7.0之前,可以通过文件绝对路径将某个文件暴露给其他程序,例如: file:///data/local/com.android.camera2/1234567890.jpg, 但是这种方式不安全,在Android7.0之后,需要使用 fileProvider 来暴露文件。文件访问限制通过 fileProvider 严格限制。路径也修改为了下面这种方式: content://cn.mengfly.whereareyou.fileprovider/cameraCache/IMG_20260114_155934_3303541153976440558.jpg

fileProvider 允许开发者通过 file_paths.xml 定义那些目录下的文件是允许访问的, 避免授权范围过大。

AndroidManifest.xml

xml 复制代码
<application>
    <!-- ... -->
    <provider
        android:authorities="${applicationId}.fileprovider"
        android:name="androidx.core.content.FileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <!-- 在 res/xml/ 下创建 file_paths.xml 文件,声明那些文件可见  -->
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            
            android:resource="@xml/file_paths" />
    </provider>

</application>

res/xml/file_paths.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!-- 这里声明了应用的缓存目录,应用读取自己的缓存目录不需要权限,因此不申请文件读取权限也可以读取  -->
    <cache-path
        name="cameraCache"
        path="camera" />
</paths>

3. 动态申请权限

Compose中,申请权限使用 rememberLauncherForActivityResult + RequestMultiplePermissions 进行权限申请,考虑到权限申请有以下特征:

  1. 权限校验可能会在多处使用
  2. 用户肯定是在使用某项功能的时候, 该功能涉及敏感权限,需要在执行的时候判断权限,也即权限申请一定是 权限 + 执行功能
  3. 权限申请是异步的,需要在 onActivityResult 中处理权限申请结果。
  4. 权限申请需要考虑用户拒绝权限的情况。

因此,我简单地封装了一下,将权限验证逻辑封装到一个函数中,方便在多处使用。

封装如下:

ui/components/Permissions.kt

kotlin 复制代码
/**  
 * 权限申请失败后显示的弹窗  
 * @param deniedMap 被拒绝的权限列表, key: 权限名, value: 权限描述  
 * @param onDismiss 弹窗关闭后的回调  
 */  
@Composable  
fun PermissionDeniedDialog(context: Context,  
                           deniedMap: Map<String, String>,  
                           onDismiss: () -> Unit  
) {  
    AlertDialog(  
        properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true),
        onDismissRequest = { onDismiss() },  
        confirmButton = {  
            TextButton(onClick = {  
                context.startActivity(  
                    Intent(  
                        android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,  
                        Uri.fromParts("package", context.packageName, null)  
                    )  
                )  
                onDismiss()  
            }) {  
                Text("确定")  
            }  
        },  
        dismissButton = {  
            TextButton(onClick = {  
                onDismiss()  
            }) {  
                Text("取消")  
            }  
        },  
        title = {  
            Text("权限申请失败")  
        },  
        text = {  
            var text = "无法申请以下权限,您可在【权限管理】中手动开启\n"  
  
            text += deniedMap.keys  
                .mapIndexed { index, permission -> "${index + 1}. $permission : ${deniedMap[permission]}" }  
                .fastJoinToString("\n")  
  
            Text(text = text)  
        }  
    )  
  
}  
  
/**  
 * 创建权限申请对象  
 * @param context 上下文  
 * @param permissionMap 权限列表, key: 权限名, value: 权限描述  
 * @param onGrant 权限申请成功后的回调  
 * @return 权限申请对象, 通过调用 execute 方法,会自动判断权限,并申请权限, 如果权限被拒绝,则会显示权限申请失败的弹窗  
 * 如果权限申请通过,则会调用 onGrant 方法,执行业务逻辑  
 */  
@Composable  
fun rememberExecuteOnPermissions(context: Context,  
                                 permissionMap: Map<String, String>,  
                                 onGrant: () -> Unit  
): ExecuteOnPermissions {  
    val curGrant by rememberUpdatedState(newValue = onGrant)  
  
    var deniedMap: Map<String, String> by remember { mutableStateOf(emptyMap()) }  
  
    // 申请权限  
    val requestPermissionLauncher = rememberLauncherForActivityResult(  
        contract = ActivityResultContracts.RequestMultiplePermissions()  
    ) { permissions ->  
  
        // 判断权限是否被拒绝  
        deniedMap = permissions.filter { !it.value }.map {  
            it.key to permissionMap[it.key]!!  
        }.toMap()  
  
        if (deniedMap.isEmpty()) {  
            curGrant()  
        }  
    }  
  
    // 如果存在被拒绝的权限, 显示权限申请失败的弹窗  
    if (deniedMap.isNotEmpty()) {  
        PermissionDeniedDialog(context = context, deniedMap = deniedMap) {  
            deniedMap = emptyMap()  
        }  
    }  
  
    // 创建权限申请对象,通过调用 execute 方法,会自动判断权限,并申请权限  
    val executeOnPermissions =  
        ExecuteOnPermissions(permissionMap.keys.toList(), requestPermissionLauncher, curGrant)  
  
    return executeOnPermissions  
}  
  
  
data class ExecuteOnPermissions(  
    val permissions: List<String>,  
    val requestPermissionLauncher: ManagedActivityResultLauncher<Array<String>, Map<String, @JvmSuppressWildcards Boolean>>,  
    val onGrant: () -> Unit  
) {  
  
    /**  
     * 执行权限申请  
     * @param context 上下文  
     */  
    fun execute(context: Context) {  
        var isGrant = true  
        for (permission in permissions) {  
            if (isPermissionGrant(context = context, permission = permission)) {  
                continue  
            }  
            isGrant = false  
            break        }  
        if (!isGrant) {  
            // 申请权限  
            requestPermissionLauncher.launch(permissions.toTypedArray())  
        } else {  
            onGrant()  
        }  
    }  
}  
  
/**  
 * 判断权限是否被授予  
 */  
fun isPermissionGrant(context: Context, permission: String) =  
    ContextCompat.checkSelfPermission(  
        context,  
        permission  
    ) == PackageManager.PERMISSION_GRANTED

封装完成后,执行申请权限+拍照逻辑就会比较简单了,如下:

kotlin 复制代码
// 创建需要申请权限的执行器
val executeLauncher = rememberExecuteOnPermissions(  
    context = context,  
    permissionMap = mapOf(
    	Manifest.permission.CAMERA to "此权限用于拍照,无此权限无法进行拍照"
	) 
) {  
   // 权限校验已完成,执行调用相机逻辑
   doTakePhoto()
}

想要调用拍照功能的时候,只需要在按钮点击的时候调用 executeLauncher.execute(context) 即可调用功能

kotlin 复制代码
val context = LocalContext.current
FloatingActionButton(  
	// 调用
    onClick = { executeLauncher.execute(context) },  
    shape = FloatingActionButtonDefaults.largeShape  
) {  
    Icon(  
        painter = painterResource(R.drawable.camera_fill),  
        contentDescription = null,  
        modifier = Modifier.size(FloatingActionButtonDefaults.LargeIconSize)  
    )  
}
  1. 点击拍照按钮后自动申请权限
  2. 授权通过执行拍照逻辑
  3. 用户拒绝后,弹窗提示

4. 调用相机进行拍照

完成权限处理后就可以通过调用相机来进行拍照了,传统的 view 中,通过 startActivityForResult 调用相机,通过 onActivityResult 回调函数获取相机结果,在 [[Compose]] 中都是页面,没有这个回调函数。

不过 [[Compose]] 为我们提供了 rememberLauncherForActivityResult,没错,权限验证调用的也是这个。

该函数接收 contract 和 onResult 两个参数:contract 表示执行的操作类型,onResult 是执行后的回调函数。

调用相机我们需要启动相机,以及获取相机的结果,如下:

kotlin 复制代码
// 相机要写入的Uri
var imageUri: Uri? by remember { mutableStateOf(null) }
// 解析后的图片  
var capturedImageBitmap by remember { mutableStateOf<Bitmap?>(null) }  

// 相机拍照Launcher
val cameraLauncher = rememberLauncherForActivityResult(  
    contract = ActivityResultContracts.StartActivityForResult()  
) {  
	// 处理相机的拍照结果
    if (it.resultCode == RESULT_OK) {  
        imageUri?.let { uri ->  
            val bitmap = ImageDecoder.createSource(  
                context.contentResolver,  
                uri  
            )  
            capturedImageBitmap = ImageDecoder.decodeBitmap(bitmap)  
        }  
    }  
}

接下来,通过该 Launcher 启动相机,结合上面提到的权限验证的部分。

kotlin 复制代码
// 启动相机拍照
fun doTakePhoto() {  
    takePhoto(context) {  
        imageUri = it
        // 构建拍照Intent  
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {  
            putExtra(MediaStore.EXTRA_OUTPUT, imageUri)  
        }  
        // 启动Intent
        cameraLauncher.launch(intent)  
    }  
}

/**  
 * 获取拍照要保存的文件Uri  
 */
private fun takePhoto(context: Context, onUriCreated: (Uri) -> Unit) {  
    val imageFile = createCacheImageFile(context = context)  
  	
  	// 将绝对路径地址通过 Provider 转换为资源地址
    val uri = FileProvider.getUriForFile(  
        context,  
        "${context.packageName}.fileprovider",  
        imageFile  
    )  
    onUriCreated(uri)  
}

/**  
 * 创建缓存图片文件  
 */
private fun createCacheImageFile(context: Context): File {  
    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())  
    val storeDir = File(context.cacheDir, "camera")  
  
    if (!storeDir.exists()) {  
        storeDir.mkdirs()  
    }  
    return File.createTempFile("IMG_${timeStamp}_", ".jpg", storeDir)  
}

5. 显示图片

在上面的图片拍摄完成后,系统会将图片解析为 bitmap,并存储在 capturedImageBitmap 变量中,通过 Image 即可显示该图片。

kotlin 复制代码
if (capturedImageBitmap != null) {  
    Image(  
        bitmap = capturedImageBitmap!!.asImageBitmap(),  
        modifier = Modifier.padding(innerPadding),  
        contentDescription = null  
    )  
} else {  
    Box(  
        Modifier  
            .padding(innerPadding)  
            .fillMaxSize()  
    ) {  
        Text(  
            "请点击拍照按钮",  
            modifier = Modifier  
                .fillMaxWidth()  
                .align(Alignment.Center),  
            textAlign = TextAlign.Center  
        )  
    }  
}
相关推荐
黄林晴4 小时前
告别手写延迟!Android Ink API 1.0 正式版重磅发布,4ms 极致体验触手可及
android·android jetpack
黄林晴1 天前
Compose Multiplatform 1.10.0 重磅发布!三大核心升级,跨平台开发效率再提升
android·android jetpack
Jony_2 天前
Android 设计架构演进历程
android·android jetpack
我命由我123452 天前
Android 项目路径包含非 ASCII 字符问题:Your project path contains non-ASCII characters
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
ljt27249606615 天前
Compose笔记(六十八)--MutableStateFlow
android·笔记·android jetpack
zFox6 天前
三、Kotlin协程+异步加载+Loading状态
kotlin·android jetpack·协程
我命由我123456 天前
Kotlin 面向对象 - 装箱与拆箱
android·java·开发语言·kotlin·android studio·android jetpack·android-studio
我命由我123456 天前
Android Jetpack Compose - Snackbar、Box
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
alexhilton7 天前
Jetpack Compose内部的不同节点类型
android·kotlin·android jetpack