59、相机拍照与相册选择:Camera Intent、MediaStore、FileProvider、图片裁剪

说实话,Android 的相机和相册功能,看起来简单,做起来坑不少。

我最早做这个功能时,以为就是调个 Intent 完事。结果呢?

不同机型、不同 Android 版本,各种崩溃和黑屏。嗯,今天我把这些坑都给你填上。

核心流程:一张照片的旅程

我们先理清整体逻辑。一张照片从拍摄到显示,经历了什么?

核心链路:

  1. App 发起 Intent → 系统相机/相册
  2. 系统返回照片 Uri → App 接收
  3. App 读取 Uri → 裁剪/压缩 → 显示或上传

这里面最关键的,就是 Uri 的传递。Android 7.0 之后,FileProvider 成了必修课。你想想看,直接传 file:// Uri 会怎样?

会直接 Crash。我当年就被这个坑过,线上反馈一堆「拍照闪退」,查了半天才发现是 Uri 适配问题。

相机拍照与相册选择核心流程 App 发起请求 相机拍照 (Intent) 相册选择 (Intent) FileProvider 生成 Uri MediaStore 查询 Uri 裁剪/压缩/显示

拍照:Camera Intent + FileProvider

拍照的流程,说白了就是三步:

  1. 创建临时文件,获取 Uri
  2. 构建 Intent,传入 Uri
  3. 在 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