使用intent拍照

使用intent拍照:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。

学习目标

  • 能沿“使用intent拍照”的用户事件解释Android组件、状态所有者、线程与销毁边界。
  • 能围绕“学会通过隐式intent调用系统相机拍照、用FileProvider共享文件、处理拍照结果、以及应对Android 10+分区存储的挑战。”改出一个可运行结果,并用前后状态而非组件数量验收。
  • 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
  • 能用Intent合同、解析目标、权限、返回结果和重复副作用独立重放结论,并标明第四版机制与现代targetSdk政策的边界。

为什么需要隐式intent拍照

很多 App 需要"拍一张照片"——用户头像、商品图、打卡记录。自己实现一个相机?你要处理对焦、曝光、闪光灯、前后摄像头切换、预览帧渲染……工程量巨大,而且每个厂商的相机行为还不一样。

Android 的标准做法是:发出"我要拍照"的请求,让系统相机 App 帮你拍,拍完后把照片还给你。就像相机摄影里的"借设备"——你不需要自己造一台相机,只需告诉专业摄影师"帮我拍一张",他拍完把底片交给你。

这个看似简单的流程涉及三个关键技术:隐式 intent 调起相机、 安全共享文件路径、以及 Activity Result API 接收拍照结果。

使用intent拍照是怎么工作的

调用系统相机:ACTION_IMAGE_CAPTURE

是最经典的隐式 intent 之一。你发出"我要拍照"的请求,系统相机 App 响应并完成拍摄。

基本流程分四步:

你的App                    系统相机App
  │                            │
  │── ① 创建临时文件 ──────────│
  │── ② FileProvider 生成 URI ─│
  │── ③ 发出 ACTION_IMAGE_CAPTURE ──→ 打开相机、拍照
  │                            │
  │←── ④ 照片写入你指定的 URI ──│
  │── 显示照片                  │
Intent 拍照全链路:我的 App 与相机 App 经 FileProvider 安全交换文件紫 = 我的 App|绿 = 相机 App;文件只在私有目录,靠 content:// URI + 临时权限借出我的 App建空文件 + 取 content:// URIFile.createTempFile(…, cacheDir)FileProvider.getUriForFile(ctx, authority, file)我的 App发 ACTION_IMAGE_CAPTURE IntentputExtra(MediaStore.EXTRA_OUTPUT, uri)FLAG_GRANT_WRITE_URI_PERMISSION 授临时写权限相机 App拍照并写入该 content URI用户按下快门,相机 App 拍摄全分辨率 JPEG 直接写进你指定的文件我的 AppResult API 回到我的 AppregisterForActivityResult(TakePicture())回调 success == true 表示拍照完成我的 App读取文件、显示照片setImageURI(uri) 或 BitmapFactory.decodeFiledata 里没有大图,靠之前存的 uri 读为何必须用 FileProvider?file:// URI 在 Android 7.0(N, API 24)+跨 App 传递会抛 FileUriExposedException——它会暴露你的私有目录绝对路径。必须改用 content:// URI:经 FileProvider暴露文件并只授临时读写权限,用完即失效。
拍照五步:我的 App 先建空文件并用 FileProvider.getUriForFile() 取得 content:// URI,发 ACTION_IMAGE_CAPTURE Intent (带 EXTRA_OUTPUT + 临时写权限)→ 相机 App 把全分辨率图写进该 URI → Result API 回调 → 读文件显示。用 file:// URI 跨 App 传在 Android 7.0+ 会抛 FileUriExposedException,必须经 FileProvider 暴露成 content://

FileProvider:为什么不能直接用 file:// URI

Android 7.0(API 24)起,禁止用 file:// URI 在 App 之间传文件。如果你把 Uri.fromFile(photoFile) 传给相机 App,运行时会抛出 FileUriExposedException

原因很直观:别的 App 拿到你的文件系统绝对路径(如 /data/data/com.myapp/cache/IMG_001.jpg),就能直接读写你的私有目录——这是严重的安全漏洞。

FileProvider 把文件路径包装成 content:// URI,并附带临时读写权限。相机 App 只能访问你授权的那一个文件,拍完后权限自动失效。

Manifest 声明:

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

res/xml/file_paths.xml(定义可共享的目录范围):

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <cache-path name="cache_photos" path="." />
    <external-files-path name="external_photos" path="Pictures/" />
</paths>

cache-path 对应 context.cacheDirexternal-files-path 对应 context.getExternalFilesDir(null)——这两个都是 App 私有目录,其他 App 无法直接访问,但通过 FileProvider 可以临时授权。

代码中获取 Content URI:

photoUri = FileProvider.getUriForFile( this, "${packageName}.fileprovider",
// 与 manifest authorities 一致 photoFile ) ```
</Tab>
<Tab label="Java">
```java File photoFile = File.createTempFile("IMG_", ".jpg", getCacheDir());
Uri photoUri = FileProvider.getUriForFile( this, getPackageName() +
".fileprovider", photoFile ); ```
</Tab>
</CodeTabs>
 
### 处理拍照结果:Activity Result API
 
旧写法 `onActivityResult` 已废弃。现代写法用 `registerForActivityResult`:
 
<CodeTabs>
<Tab label="Kotlin">
```kotlin
private lateinit var photoUri: Uri
 
private val takePictureLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
if (success) {
// 照片已写入 photoUri 指向的文件
binding.imageView.setImageURI(photoUri)
}
}
 
fun takePhoto() {
val photoFile = File.createTempFile("IMG\_", ".jpg", cacheDir)
photoUri = FileProvider.getUriForFile(
this, "${packageName}.fileprovider", photoFile
)
takePictureLauncher.launch(photoUri)
}
 

ActivityResultContracts.TakePicture() 是 AndroidX 提供的专用 Contract——它自动处理 ACTION_IMAGE_CAPTURE intent 的发出和结果解析,比手动写 intent 更简洁。

全分辨率 vs 缩略图

不传 MediaStore.EXTRA_OUTPUT 时,相机返回 intent data 里的是一个压缩过的缩略图 Bitmap(通常只有几百像素)。传了 EXTRA_OUTPUT 并指定 FileProvider URI,相机 App 会把原始全分辨率照片写入你指定的文件。

// 方式 A:缩略图(不推荐用于展示大图)
private val captureLauncher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    val thumbnail = result.data?.extras?.get("data") as? Bitmap
    binding.imageView.setImageBitmap(thumbnail)  // 模糊的小图
}
 
// 方式 B:全分辨率(推荐)
private val takePictureLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { success ->
if (success) {
// 用 BitmapFactory 加载并缩放显示
val bitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
binding.imageView.setImageBitmap(bitmap)
}
}
 

大图直接 decodeFile 可能 OOM。生产环境应加采样缩放:

fun decodeSampledBitmap(path: String, reqWidth: Int, reqHeight: Int): Bitmap {
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeFile(path, this)
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
        inJustDecodeBounds = false
        BitmapFactory.decodeFile(path, this)
    }!!
}

Android 10+ 分区存储(Scoped Storage)

进一步收紧了存储访问:App 不能随意读写 /sdcard/ 下的任意路径。

对拍照的影响:

  • App 私有目录cacheDirgetExternalFilesDir)不受 Scoped Storage 限制——FileProvider 共享这些目录里的文件完全合规
  • 如果想把照片存到公共相册(让用户在系统图库里也能看到),需要用 MediaStore.Images.Media API 写入,而不是直接写文件路径
  • WRITE_EXTERNAL_STORAGE 权限在 Android 10+ 对 App 私有目录已不需要;Android 13+ 改用细粒度媒体权限 READ_MEDIA_IMAGES

CameraX:现代替代方案

如果需要自定义相机界面(实时滤镜、扫码框、连续拍摄),隐式 intent 就不够用了。Google 推荐的现代方案是 CameraX 库——基于 Camera2 API 封装,自动处理设备兼容性、生命周期绑定、权限管理。但学习曲线比隐式 intent 陡得多,适合有定制需求的场景。

动手:完成一个"拍照并显示"功能

猜一猜:如果忘了配置 FileProvider,在 Android 7.0 以上的设备上点"拍照"会发生什么?Logcat 里会出现什么异常类名?

分步1 / 4

① 配置 FileProvider

AndroidManifest.xml<application> 内添加 <provider> 标签,authorities 设为 "${applicationId}.fileprovider"。 创建 res/xml/file_paths.xml,声明 <cache-path name="cache_photos" path="." />。 效果:Gradle Sync 后,你的 App 具备了安全共享 cache 目录文件的能力。

代码逐段拆解

完整拍照 Activity 示例

下面是一个可运行的最小实现,覆盖了 FileProvider 配置、权限声明、拍照启动和结果显示。

AndroidManifest.xml 权限声明:

<!-- 只有直接拨出电话才需要;隐式 intent 调相机通常不需要 CAMERA 权限 -->
<!-- Android 13+ 读取相册才需要 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

Activity 完整代码:

class PhotoActivity : AppCompatActivity() {
    private lateinit var binding: ActivityPhotoBinding
    private lateinit var photoFile: File
    private lateinit var photoUri: Uri
 
    private val takePictureLauncher = registerForActivityResult(
        ActivityResultContracts.TakePicture()
    ) { success ->
        if (success) {
            binding.imageView.setImageURI(photoUri)
        } else {
            Toast.makeText(this, "拍照取消", Toast.LENGTH_SHORT).show()
        }
    }
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityPhotoBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.btnTakePhoto.setOnClickListener { takePhoto() }
    }
 
    private fun takePhoto() {
        photoFile = File.createTempFile("IMG_", ".jpg", cacheDir)
        photoUri = FileProvider.getUriForFile(
            this, "${packageName}.fileprovider", photoFile
        )
        takePictureLauncher.launch(photoUri)
    }
 
}
 

关键点:photoUri 必须是成员变量——回调触发时 intent 的 data 里通常没有照片(用了 EXTRA_OUTPUT 后相机直接写文件),你必须靠之前保存的 URI 引用来读取照片。

手动 Intent 写法(理解原理用)

ActivityResultContracts.TakePicture() 底层其实就是下面这段代码:

val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
    putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
    addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
startActivityForResult(captureIntent, REQUEST_CODE)  // 旧 API,已废弃

FLAG_GRANT_WRITE_URI_PERMISSION 告诉系统:把对这个 URI 的写权限临时授予相机 App。没有这个 flag,相机 App 无法写入你指定的文件。

容易踩的坑

小结

  • 拍照三步:创建临时文件 → FileProvider 生成 content:// URI → 隐式 intent 调起相机
  • FileProvider 替代 file:// URI,Android 7.0+ 跨 App 传文件的强制要求
  • EXTRA_OUTPUT 拿全分辨率照片;不传只拿压缩缩略图
  • registerForActivityResult(TakePicture()) 替代废弃的 onActivityResult
  • Android 10+ Scoped Storage:App 私有目录不受限;存公共相册需 MediaStore API

练习

问题 1:“第16章 Taking Pictures with Intents”覆盖哪些正式节点和项目主线?

问题 2:怎样建立本页最小可执行实验?

问题 3:为什么只在正常点击路径运行不能证明完成?

问题 4:怎样设计能推翻当前实现的反例?

问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?

问题 6:本页达到独立交接标准需要什么?

名词解释

本章出现的专业名词,用大白话再讲一遍。

FileProvider

ContentProvider 的特殊子类,用于在 App 之间安全共享文件。生成受权限保护的 content:// URI 替代 file:// URI,可设置临时读写权限。Android 7.0+ 跨 App 传文件的标准方式。详见本章"FileProvider"一节。

MediaStore.ACTION_IMAGE_CAPTURE

Android 系统定义的标准拍照动作常量。发出带此 action 的隐式 intent,系统调起已安装的相机 App 完成拍照。配合 EXTRA_OUTPUT 可获取全分辨率照片。详见本章"调用系统相机"一节。

Scoped Storage

Android 10(API 29)引入的存储隔离机制。App 默认只能访问自己的私有目录和公共媒体集合(通过 MediaStore),不能随意读写外部存储的任意路径。详见本章"Android 10+ 分区存储"一节。

“使用intent拍照”不使用未获授权的纸书正文;InformIT出版信息授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。

为什么“使用intent拍照”必须回到可观察状态

“使用intent拍照”的学习结果不是记住类名,而是能预测“学会通过隐式intent调用系统相机拍照、用FileProvider共享文件、处理拍照结果、以及应对Android 10+分区存储的挑战。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。

第四版机制逐项深读

16. Taking Pictures with Intents

在“使用intent拍照”中,“16. Taking Pictures with Intents”通过FileProvider URI把写入能力临时交给相机应用,再按目标尺寸解码;验证要覆盖无相机、取消、零字节文件、旋转和内存上限。

A Place for Your Photo

在“使用intent拍照”中,验证“A Place for Your Photo”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。

File Storage

在“使用intent拍照”中,“File Storage”通过FileProvider URI把写入能力临时交给相机应用,再按目标尺寸解码;验证要覆盖无相机、取消、零字节文件、旋转和内存上限。

Using a Camera Intent

在“使用intent拍照”中,“Using a Camera Intent”通过FileProvider URI把写入能力临时交给相机应用,再按目标尺寸解码;验证要覆盖无相机、取消、零字节文件、旋转和内存上限。

Scaling and Displaying Bitmaps

在“使用intent拍照”中,“Scaling and Displaying Bitmaps”通过FileProvider URI把写入能力临时交给相机应用,再按目标尺寸解码;验证要覆盖无相机、取消、零字节文件、旋转和内存上限。

Declaring Features

在“使用intent拍照”中,“Declaring Features”服务于“学会通过隐式intent调用系统相机拍照、用FileProvider共享文件、处理拍照结果、以及应对Android 10+分区存储的挑战。”;解释要落到用户事件、Android所有者、状态变化、线程和可观察结果,并给出一个会推翻实现的反例。

Challenge: Detail Display

在“使用intent拍照”中,“Challenge: Detail Display”用于推翻“学会通过隐式intent调用系统相机拍照、用FileProvider共享文件、处理拍照结果、以及应对Android 10+分区存储的挑战。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。

Challenge: Efficient Thumbnail Load

在“使用intent拍照”中,“Challenge: Efficient Thumbnail Load”通过FileProvider URI把写入能力临时交给相机应用,再按目标尺寸解码;验证要覆盖无相机、取消、零字节文件、旋转和内存上限。

“使用intent拍照”验收回顾

“使用intent拍照”只有在Intent合同、解析目标、权限、返回结果和重复副作用能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。

← 上一页:隐式intent · 下一页:应用本地化 →

章专属可重放状态实验

先预测“拍照、取消、旋转、返回及照片加载”发生后,CrimeRepository、FileProvider、相机应用与 ImageView应怎样改变文件 URI、临时权限、照片字节、方向和解码尺寸;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。

实验一:所有者—状态—结果合同

选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。

Owner · state · observable result

使用intent拍照:状态合同

用 FileProvider URI 委托相机写入照片并按目标尺寸解码显示

验证场景

第四版正式目录节点

taking-pictures · 正常任务

16. Taking Pictures with Intents固定 SDK、设备配置和初始状态,触发“拍照、取消、旋转、返回及照片加载”

状态所有者CrimeRepository、FileProvider、相机应用与 ImageView
受控状态文件 URI、临时权限、照片字节、方向和解码尺寸
触发事件拍照、取消、旋转、返回及照片加载

冻结入口:16. Taking Pictures with Intents

记录CrimeRepository、FileProvider、相机应用与 ImageView的初始文件 URI、临时权限、照片字节、方向和解码尺寸

观察:URI grant、结果码、文件大小、EXIF、采样率和内存轨迹中的“16. Taking Pictures with Intents”轨迹

预期:由CrimeRepository、FileProvider、相机应用与 ImageView提交文件 URI、临时权限、照片字节、方向和解码尺寸,并持续满足“只接受非空可解码文件,授权在任务结束后不继续扩大”

实验二:事件与生命周期轨迹

沿五次转换逐步执行“拍照、取消、旋转、返回及照片加载”。每一步只允许CrimeRepository、FileProvider、相机应用与 ImageView按职责提交状态,并持续核对“只接受非空可解码文件,授权在任务结束后不继续扩大”。

Deterministic event replay

使用intent拍照:事件轨迹

选择一次状态转换1 / 5

不变量:只接受非空可解码文件,授权在任务结束后不继续扩大

交付证据:URI grant、结果码、文件大小、EXIF、采样率和内存轨迹

实验三:章专属反例与同输入恢复

注入“把 file URI 暴露给外部相机或全尺寸解码导致权限异常与 OOM”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有URI grant、结果码、文件大小、EXIF、采样率和内存轨迹一起恢复才算修复。

Fault · cancel · restore

使用intent拍照:反例与恢复

故障:把 file URI 暴露给外部相机或全尺寸解码导致权限异常与 OOM

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“把 file URI 暴露给外部相机或全尺寸解码导致权限异常与 OOM”

3. 检查所有者一致

只接受非空可解码文件,授权在任务结束后不继续扩大

4. 核对结果一致

URI grant、结果码、文件大小、EXIF、采样率和内存轨迹

讨论

评论区加载中…