使用intent拍照
使用intent拍照:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。
学习目标
- 能沿“使用intent拍照”的用户事件解释Android组件、状态所有者、线程与销毁边界。
- 能围绕“学会通过隐式intent调用系统相机拍照、用FileProvider共享文件、处理拍照结果、以及应对Android 10+分区存储的挑战。”改出一个可运行结果,并用前后状态而非组件数量验收。
- 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
- 能用Intent合同、解析目标、权限、返回结果和重复副作用独立重放结论,并标明第四版机制与现代targetSdk政策的边界。
为什么需要隐式intent拍照
很多 App 需要"拍一张照片"——用户头像、商品图、打卡记录。自己实现一个相机?你要处理对焦、曝光、闪光灯、前后摄像头切换、预览帧渲染……工程量巨大,而且每个厂商的相机行为还不一样。
Android 的标准做法是:发出"我要拍照"的请求,让系统相机 App 帮你拍,拍完后把照片还给你。就像相机摄影里的"借设备"——你不需要自己造一台相机,只需告诉专业摄影师"帮我拍一张",他拍完把底片交给你。
这个看似简单的流程涉及三个关键技术:隐式 intent 调起相机、↡ContentProvider 的特殊子类,用于在 App 之间安全共享文件。生成受权限保护的 content:// URI 替代 file:// URI,Android 7.0+ 跨 App 传文件的标准方式。 安全共享文件路径、以及 Activity Result API 接收拍照结果。
使用intent拍照是怎么工作的
调用系统相机:ACTION_IMAGE_CAPTURE
↡Android 系统定义的标准拍照动作常量,值为 `android.media.action.IMAGE_CAPTURE`。发出带此 action 的隐式 intent,系统会调起已安装的相机 App 完成拍照。是最经典的隐式 intent 之一。你发出"我要拍照"的请求,系统相机 App 响应并完成拍摄。
基本流程分四步:
你的App 系统相机App
│ │
│── ① 创建临时文件 ──────────│
│── ② FileProvider 生成 URI ─│
│── ③ 发出 ACTION_IMAGE_CAPTURE ──→ 打开相机、拍照
│ │
│←── ④ 照片写入你指定的 URI ──│
│── 显示照片 │
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.cacheDir,external-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)
}
private Uri photoUri;
private final ActivityResultLauncher<Uri> takePictureLauncher =
registerForActivityResult(new ActivityResultContracts.TakePicture(), success -> {
if (success) {
imageView.setImageURI(photoUri);
}
});
public void takePhoto() {
File photoFile = File.createTempFile("IMG_", ".jpg", getCacheDir());
photoUri = FileProvider.getUriForFile(
this, getPackageName() + ".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)
}
}
// 方式 B:全分辨率(推荐)
private final ActivityResultLauncher<Uri> takePictureLauncher =
registerForActivityResult(new ActivityResultContracts.TakePicture(), success -> {
if (success) {
Bitmap bitmap = BitmapFactory.decodeFile(photoFile.getAbsolutePath());
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)
↡Android 10(API 29)引入的存储隔离机制。App 默认只能访问自己的私有目录和公共媒体集合(通过 MediaStore),不能随意读写外部存储的任意路径。进一步收紧了存储访问:App 不能随意读写 /sdcard/ 下的任意路径。
对拍照的影响:
- App 私有目录(
cacheDir、getExternalFilesDir)不受 Scoped Storage 限制——FileProvider 共享这些目录里的文件完全合规 - 如果想把照片存到公共相册(让用户在系统图库里也能看到),需要用
MediaStore.Images.MediaAPI 写入,而不是直接写文件路径 WRITE_EXTERNAL_STORAGE权限在 Android 10+ 对 App 私有目录已不需要;Android 13+ 改用细粒度媒体权限READ_MEDIA_IMAGES
CameraX:现代替代方案
如果需要自定义相机界面(实时滤镜、扫码框、连续拍摄),隐式 intent 就不够用了。Google 推荐的现代方案是 CameraX 库——基于 Camera2 API 封装,自动处理设备兼容性、生命周期绑定、权限管理。但学习曲线比隐式 intent 陡得多,适合有定制需求的场景。
动手:完成一个"拍照并显示"功能
猜一猜:如果忘了配置 FileProvider,在 Android 7.0 以上的设备上点"拍照"会发生什么?Logcat 里会出现什么异常类名?
① 配置 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)
}
}
public class PhotoActivity extends AppCompatActivity {
private ActivityPhotoBinding binding;
private File photoFile;
private Uri photoUri;
private final ActivityResultLauncher<Uri> takePictureLauncher =
registerForActivityResult(new ActivityResultContracts.TakePicture(), success -> {
if (success) {
binding.imageView.setImageURI(photoUri);
} else {
Toast.makeText(this, "拍照取消", Toast.LENGTH_SHORT).show();
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityPhotoBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.btnTakePhoto.setOnClickListener(v -> takePhoto());
}
private void takePhoto() {
try {
photoFile = File.createTempFile("IMG_", ".jpg", getCacheDir());
photoUri = FileProvider.getUriForFile(
this, getPackageName() + ".fileprovider", photoFile
);
takePictureLauncher.launch(photoUri);
} catch (IOException e) {
Toast.makeText(this, "无法创建临时文件", Toast.LENGTH_SHORT).show();
}
}
}关键点: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,已废弃Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
captureIntent.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名称掩盖旧行为。
章专属可重放状态实验
先预测“拍照、取消、旋转、返回及照片加载”发生后,CrimeRepository、FileProvider、相机应用与 ImageView应怎样改变文件 URI、临时权限、照片字节、方向和解码尺寸;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。
实验一:所有者—状态—结果合同
选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。
Owner · state · observable result
使用intent拍照:状态合同
用 FileProvider URI 委托相机写入照片并按目标尺寸解码显示
验证场景
第四版正式目录节点
taking-pictures · 正常任务
16. Taking Pictures with Intents:固定 SDK、设备配置和初始状态,触发“拍照、取消、旋转、返回及照片加载”
冻结入口: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拍照:事件轨迹
不变量:只接受非空可解码文件,授权在任务结束后不继续扩大
交付证据:URI grant、结果码、文件大小、EXIF、采样率和内存轨迹
实验三:章专属反例与同输入恢复
注入“把 file URI 暴露给外部相机或全尺寸解码导致权限异常与 OOM”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有URI grant、结果码、文件大小、EXIF、采样率和内存轨迹一起恢复才算修复。
Fault · cancel · restore
使用intent拍照:反例与恢复
故障:把 file URI 暴露给外部相机或全尺寸解码导致权限异常与 OOM
第 1 次使用相同 SDK、设备配置、初始状态与用户事件
保持正常输入不变,仅注入“把 file URI 暴露给外部相机或全尺寸解码导致权限异常与 OOM”
只接受非空可解码文件,授权在任务结束后不继续扩大
URI grant、结果码、文件大小、EXIF、采样率和内存轨迹