0. 概览
在Android中,要想实现调用系统相机完成拍照显示功能,可以分为以下几个步骤:
- 申请对应的权限(相机权限与读写文件权限)
- 添加 fileProvider, 来读取相机生成的图片文件。
- 程序启动拍照时,动态检查与申请权限。
- 通过 startActivity 启动相机,进行拍照
- 拍照完成后,通过图片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 进行权限申请,考虑到权限申请有以下特征:
- 权限校验可能会在多处使用
- 用户肯定是在使用某项功能的时候, 该功能涉及敏感权限,需要在执行的时候判断权限,也即权限申请一定是
权限+执行功能 - 权限申请是异步的,需要在
onActivityResult中处理权限申请结果。 - 权限申请需要考虑用户拒绝权限的情况。
因此,我简单地封装了一下,将权限验证逻辑封装到一个函数中,方便在多处使用。
封装如下:
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)
)
}
- 点击拍照按钮后自动申请权限
- 授权通过执行拍照逻辑
- 用户拒绝后,弹窗提示



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
)
}
}