59、相机拍照与相册选择:Camera Intent、MediaStore、FileProvider、图片裁剪
说实话,Android 的相机和相册功能,看起来简单,做起来坑不少。
我最早做这个功能时,以为就是调个 Intent 完事。结果呢?
不同机型、不同 Android 版本,各种崩溃和黑屏。嗯,今天我把这些坑都给你填上。
核心流程:一张照片的旅程
我们先理清整体逻辑。一张照片从拍摄到显示,经历了什么?
核心链路:
- App 发起 Intent → 系统相机/相册
- 系统返回照片 Uri → App 接收
- App 读取 Uri → 裁剪/压缩 → 显示或上传
这里面最关键的,就是 Uri 的传递。Android 7.0 之后,FileProvider 成了必修课。你想想看,直接传 file:// Uri 会怎样?
会直接 Crash。我当年就被这个坑过,线上反馈一堆「拍照闪退」,查了半天才发现是 Uri 适配问题。
拍照:Camera Intent + FileProvider
拍照的流程,说白了就是三步:
- 创建临时文件,获取 Uri
- 构建 Intent,传入 Uri
- 在 onActivityResult 中接收结果
这里有个关键点:临时文件必须用 FileProvider 暴露。
我的习惯:在 manifest 中注册 FileProvider,然后在 res/xml 下创建 file_paths.xml。这样所有文件路径都统一管理,不会乱。
先看 file_paths.xml 怎么写:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-cache-path
name="camera_photos"
path="." />
</paths>
然后在 AndroidManifest.xml 中注册:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
接下来是拍照的核心代码:
private fun takePhoto() {
val photoFile = File(
requireContext().externalCacheDir,
"photo_${System.currentTimeMillis()}.jpg"
)
photoUri = FileProvider.getUriForFile(
requireContext(),
"${requireContext().packageName}.fileprovider",
photoFile
)
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startActivityForResult(intent, REQUEST_CAMERA)
}
我曾经踩过的坑:忘记加 FLAG_GRANT_WRITE_URI_PERMISSION。结果在部分机型上,相机写入文件时直接报错。记住,这个 flag 必须加,否则系统相机没有权限写入你的临时文件。
相册选择:MediaStore 的正确姿势
相册选择相对简单,但也要注意版本适配。
private fun pickFromGallery() {
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
startActivityForResult(intent, REQUEST_GALLERY)
}
收到结果后,我们需要从 Uri 中读取图片。这里有个细节:不要直接用 Uri 的 path 字段。
为什么?因为 content:// Uri 的 path 是虚拟路径,不是真实文件路径。你需要通过 ContentResolver 来读取。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != Activity.RESULT_OK) return
when (requestCode) {
REQUEST_CAMERA -> {
// photoUri 就是之前传进去的 Uri
startCrop(photoUri)
}
REQUEST_GALLERY -> {
data?.data?.let { uri ->
startCrop(uri)
}
}
REQUEST_CROP -> {
// 裁剪完成,显示图片
data?.data?.let { uri ->
imageView.setImageURI(uri)
}
}
}
}
我建议:在 onActivityResult 中,不要做耗时操作。如果图片很大,可以考虑用协程或 AsyncTask 来异步处理。否则主线程卡顿,用户体验会很差。
图片裁剪:系统裁剪 vs 自定义裁剪
系统自带的裁剪功能,通过 com.android.camera.action.CROP 这个 Intent 就能调用。但说实话,这个 Intent 是非公开 API,不同厂商的实现差异很大。
我遇到过的情况:
- 小米手机:裁剪界面正常,但返回的 Uri 是空的
- 华为手机:直接不支持这个 Intent,会闪退
- 三星手机:裁剪比例不对,总是被拉伸
所以,我的建议是:如果项目对裁剪要求不高,用系统裁剪;如果要求高,直接用第三方库。
系统裁剪的代码示例:
private fun startCrop(sourceUri: Uri) {
val cropFile = File(
requireContext().externalCacheDir,
"crop_${System.currentTimeMillis()}.jpg"
)
val cropUri = FileProvider.getUriForFile(
requireContext(),
"${requireContext().packageName}.fileprovider",
cropFile
)
val intent = Intent("com.android.camera.action.CROP").apply {
setDataAndType(sourceUri, "image/*")
putExtra("crop", "true")
putExtra("aspectX", 1)
putExtra("aspectY", 1)
putExtra("outputX", 300)
putExtra("outputY", 300)
putExtra(MediaStore.EXTRA_OUTPUT, cropUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startActivityForResult(intent, REQUEST_CROP)
}
注意:系统裁剪 Intent 在 Android 10 以上可能无法使用。如果你要适配到 Android 10+,建议直接用第三方库,比如 uCrop 或 Android-Image-Cropper。我自己项目里用的就是 uCrop,稳定可靠。
版本适配总结
不同 Android 版本对相机和相册的权限要求不同,我整理了一个表格:
| Android 版本 | 拍照权限 | 相册权限 | Uri 方式 |
|---|---|---|---|
| Android 6.0 以下 | 无需动态申请 | 无需动态申请 | file:// Uri |
| Android 6.0 - 9.0 | 需动态申请 CAMERA | 需动态申请 READ_EXTERNAL_STORAGE | FileProvider |
| Android 10+ | 需动态申请 CAMERA | 无需存储权限(使用 MediaStore) | FileProvider / MediaStore |
| Android 11+ | 需动态申请 CAMERA | 需申请 READ_MEDIA_IMAGES | FileProvider / MediaStore |
你看,版本适配确实麻烦。但只要你按照上面的模板来写,基本不会出大问题。
避坑指南
最后,分享几个我亲身踩过的坑:
- 临时文件路径冲突:如果同时拍多张照片,文件名要加时间戳或 UUID,否则会覆盖。
- 图片太大导致 OOM:显示前一定要做压缩,用 BitmapFactory.Options 的 inSampleSize 来缩放。
- 裁剪后图片方向不对:有些手机拍出来的照片有 EXIF 方向信息,读取时要根据 orientation 做旋转。
- Android 11 的包可见性:如果你要检测是否有相机应用,需要添加
<queries>声明。
核心要点回顾:
- 拍照用 FileProvider 生成 Uri,别忘了加写权限
- 相册选择用 MediaStore,读取时用 ContentResolver
- 裁剪建议用第三方库,避免厂商兼容问题
- 版本适配要提前规划,尤其是 Android 10 和 11
好了,相机和相册的内容就到这里。代码模板可以直接复用,但记得根据你的项目需求做调整。
公众号:蓝海资料掘金营,微信deep3321