通过TakePicture契约启动相机并保存图片至图库的正确方式
我在Android应用中需要让用户用相机拍摄照片供应用使用,同时要确保照片保存到设备的共享图库中。我使用TakePicture活动结果契约,通过MediaStore API构建Uri传入,Activity中的代码如下:
private var takePictureUri: Uri? = null val takePictureLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { result: Boolean -> val uri = takePictureUri if(result && uri != null) { // Do something with the captured image } } fun takePicture() { val collectionUri = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI } val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US) .format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, timestamp) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") } takePictureUri = contentResolver.insert(collectionUri, contentValues) takePictureLauncher.launch(takePictureUri) }
目前代码运行正常,但我发现所有TakePicture活动的示例都用FileProvider API提供Uri而非MediaStore API,因此担心这种方式不正确或支持性不好。特别顾虑的是,这种方式要求启动TakePicture活动前指定MIME类型,如何确保TakePicture返回的是JPEG而非HEIF等其他格式?是否有更正确的实现方案?
关于MediaStore方式的合法性与支持性
你当前用MediaStore生成Uri传递给TakePicture契约的方式是完全合法且受官方支持的,并非错误实现。大部分示例采用FileProvider,只是因为FileProvider更适配App私有文件的场景——如果拍摄的照片不需要立刻进入系统图库,用FileProvider更直接;但你的需求是要照片进入共享图库,MediaStore反而更贴合,因为插入MediaStore的Uri本身就会触发系统图库的索引,不需要额外做刷新操作。
如何确保返回JPEG格式
你在ContentValues中指定MIME_TYPE为image/jpeg,绝大多数系统相机应用会遵循这个要求输出JPEG格式,但要注意:第三方相机应用可能存在不遵守约定的情况,毕竟TakePicture契约本身没有强制相机必须按指定格式输出。
如果要更稳妥地保证最终是JPEG,可以做两步处理:
- 拍摄完成后,读取Uri对应的文件,用
ContentResolver.getType(uri)获取真实MIME类型; - 如果不是JPEG,就对图片进行格式转换,用BitmapFactory读取后再以JPEG格式重新写入。
更完善的实现优化建议
你的代码已经能满足核心需求,这里补充几个实用优化点:
- 线程安全的时间格式化:
SimpleDateFormat不是线程安全的,建议API 26+改用DateTimeFormatter,低版本可以给SimpleDateFormat加同步锁; - 权限兼容处理:Android 10+插入MediaStore不需要
WRITE_EXTERNAL_STORAGE权限,但Android 9及以下必须申请该权限,否则插入会失败; - Uri生命周期管理:拍摄完成后及时置空
takePictureUri,避免内存泄漏; - 异常兜底:
contentResolver.insert()可能返回null(比如存储空间不足),要增加判空逻辑,避免启动相机时传入null导致崩溃。
优化后的代码示例:
private var takePictureUri: Uri? = null val takePictureLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { result: Boolean -> val uri = takePictureUri if (result && uri != null) { // 检查实际MIME类型 val actualMimeType = contentResolver.getType(uri) if ("image/jpeg" != actualMimeType) { // 这里可以加入格式转换逻辑 } // 处理图片 } // 置空Uri,避免内存泄漏 takePictureUri = null } fun takePicture() { // Android 9及以下检查WRITE_EXTERNAL_STORAGE权限 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1001) return } val collectionUri = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { MediaStore.Images.Media.EXTERNAL_CONTENT_URI } // 线程安全的时间戳生成 val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss", Locale.US) .format(LocalDateTime.now()) } else { synchronized(SimpleDateFormat::class.java) { SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(System.currentTimeMillis()) } } val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, timestamp) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") // 可选:指定DCIM目录,让照片存到更规范的系统图库路径 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM) } } takePictureUri = contentResolver.insert(collectionUri, contentValues) takePictureUri?.let { takePictureLauncher.launch(it) } ?: run { Toast.makeText(this, "无法创建照片文件,请检查存储空间", Toast.LENGTH_SHORT).show() } }
内容的提问来源于stack exchange,提问作者James

