Android拍照选图功能实现与FileProvider详解
1. Android系统拍照与选图功能实现解析
在移动应用开发中,调用系统相机拍照和相册选图是最基础但最容易踩坑的功能之一。最近在实现用户头像上传功能时,我重新梳理了这套流程,发现从Android 7.0开始引入的FileProvider机制让不少开发者头疼。本文将详细讲解如何正确处理不同Android版本下的拍照、选图和裁剪功能,特别针对FileProvider的配置和URI处理给出完整解决方案。
关键提示:从Android 7.0(API 24)开始,直接使用file:// URI会触发FileUriExposedException,必须改用FileProvider提供的content:// URI。
2. 核心功能实现步骤
2.1 拍照功能实现
首先在AndroidManifest.xml中添加必要的权限声明:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />拍照功能的核心代码实现:
private fun openCamera() { // 创建临时文件存储拍照结果 val outputImage = File(externalCacheDir, "output_${System.currentTimeMillis()}.jpg") try { if (outputImage.exists()) outputImage.delete() outputImage.createNewFile() } catch (e: IOException) { e.printStackTrace() return } // 根据版本选择URI生成方式 imageUri = if (Build.VERSION.SDK_INT >= 24) { FileProvider.getUriForFile( this, "${packageName}.fileprovider", outputImage ) } else { Uri.fromFile(outputImage) } // 启动相机Intent val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, imageUri) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } startActivityForResult(intent, REQUEST_CAMERA) }2.2 FileProvider配置详解
在AndroidManifest.xml中配置FileProvider:
<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> <external-cache-path name="camera_cache" path="." /> <external-files-path name="external_files" path="." /> <cache-path name="internal_cache" path="." /> </paths>经验之谈:建议配置多个路径类型,适配不同存储需求。name属性会成为URI路径的一部分,path="."表示允许访问该目录下的所有文件。
3. 相册选图功能实现
3.1 基础选图实现
private fun pickFromGallery() { val intent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "image/*" addCategory(Intent.CATEGORY_OPENABLE) } startActivityForResult( Intent.createChooser(intent, "选择图片"), REQUEST_GALLERY ) }3.2 兼容不同Android版本的URI处理
在onActivityResult中处理返回结果:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { REQUEST_GALLERY -> { if (resultCode == RESULT_OK) { data?.data?.let { uri -> val imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { handleImageOnKitKat(uri) } else { handleImageBeforeKitKat(uri) } imageUri?.let { startCrop(it) } } } } // 其他case处理... } } @TargetApi(Build.VERSION_CODES.KITKAT) private fun handleImageOnKitKat(uri: Uri): Uri? { var path: String? = null if (DocumentsContract.isDocumentUri(this, uri)) { val docId = DocumentsContract.getDocumentId(uri) when (uri.authority) { "com.android.providers.media.documents" -> { val id = docId.split(":")[1] val selection = "${MediaStore.Images.Media._ID}=?" path = getImagePath( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, arrayOf(id) ) } "com.android.providers.downloads.documents" -> { val contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), docId.toLong() ) path = getImagePath(contentUri, null, null) } } } else if ("content".equals(uri.scheme, true)) { path = getImagePath(uri, null, null) } else if ("file".equals(uri.scheme, true)) { path = uri.path } return path?.let { FileProvider.getUriForFile( this, "$packageName.fileprovider", File(it) ) } } private fun handleImageBeforeKitKat(uri: Uri): Uri { val path = getImagePath(uri, null, null) return Uri.fromFile(File(path)) } private fun getImagePath( uri: Uri, selection: String?, selectionArgs: Array<String>? ): String? { var path: String? = null contentResolver.query(uri, null, selection, selectionArgs, null)?.use { cursor -> if (cursor.moveToFirst()) { path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)) } } return path }4. 图片裁剪功能实现
4.1 系统裁剪功能调用
private fun startCrop(uri: Uri) { // 创建裁剪后图片的保存路径 val outputFile = File( getExternalFilesDir(Environment.DIRECTORY_PICTURES), "cropped_${System.currentTimeMillis()}.jpg" ) val cropIntent = Intent("com.android.camera.action.CROP").apply { setDataAndType(uri, "image/*") // 适配Android 7.0+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } // 裁剪参数配置 putExtra("crop", "true") putExtra("aspectX", 1) putExtra("aspectY", 1) putExtra("outputX", 300) putExtra("outputY", 300) putExtra("scale", true) putExtra("return-data", false) putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile)) putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) } // 检查是否有可处理的Activity if (cropIntent.resolveActivity(packageManager) != null) { startActivityForResult(cropIntent, REQUEST_CROP) } else { Toast.makeText(this, "未找到可用的图片裁剪应用", Toast.LENGTH_SHORT).show() } }4.2 裁剪结果处理
在onActivityResult中补充裁剪结果处理:
REQUEST_CROP -> { if (resultCode == RESULT_OK) { // 获取裁剪后的图片文件 val croppedFile = File( getExternalFilesDir(Environment.DIRECTORY_PICTURES), "cropped_${System.currentTimeMillis()}.jpg" ) if (croppedFile.exists()) { // 显示或上传裁剪后的图片 val bitmap = BitmapFactory.decodeFile(croppedFile.absolutePath) imageView.setImageBitmap(bitmap) } } }5. 常见问题与解决方案
5.1 权限问题处理
在Android 6.0+需要动态申请权限:
private fun checkPermissions(): Boolean { val permissions = mutableListOf<String>() if (ContextCompat.checkSelfPermission( this, Manifest.permission.CAMERA ) != PackageManager.PERMISSION_GRANTED ) { permissions.add(Manifest.permission.CAMERA) } if (ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE ) != PackageManager.PERMISSION_GRANTED ) { permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } if (permissions.isNotEmpty()) { ActivityCompat.requestPermissions( this, permissions.toTypedArray(), REQUEST_PERMISSIONS ) return false } return true } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_PERMISSIONS) { if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) { // 权限已授予 openCamera() } else { Toast.makeText(this, "需要相机和存储权限", Toast.LENGTH_SHORT).show() } } }5.2 其他常见问题
FileProvider路径配置错误
- 症状:出现FileProvider异常或找不到文件
- 解决方案:检查file_paths.xml中的路径配置是否与代码中使用的位置一致
裁剪后图片方向不正确
- 症状:裁剪后的图片出现旋转
- 解决方案:在显示图片前检查EXIF信息并纠正方向
部分机型无法返回裁剪结果
- 症状:某些国产ROM上裁剪后不返回结果
- 解决方案:使用第三方裁剪库如uCrop作为备选方案
大图片处理OOM
- 症状:加载大图时内存溢出
- 解决方案:使用BitmapFactory.Options进行采样压缩
6. 性能优化建议
图片压缩处理在显示或上传前对图片进行适当压缩:
private fun compressImage(file: File): File { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeFile(file.absolutePath, options) val reqWidth = 1024 val reqHeight = 1024 val (width, height) = options.run { outWidth to outHeight } var inSampleSize = 1 if (height > reqHeight || width > reqWidth) { val halfHeight = height / 2 val halfWidth = width / 2 while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { inSampleSize *= 2 } } options.inJustDecodeBounds = false options.inSampleSize = inSampleSize val bitmap = BitmapFactory.decodeFile(file.absolutePath, options) // 保存压缩后的图片 val outputFile = File.createTempFile("compressed_", ".jpg", cacheDir) FileOutputStream(outputFile).use { out -> bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out) } bitmap.recycle() return outputFile }缓存管理
- 及时清理不再需要的临时文件
- 使用LRU缓存管理频繁使用的图片
异步处理使用协程或RxJava将耗时操作移到后台线程:
private fun processImageAsync(file: File) { CoroutineScope(Dispatchers.IO).launch { val compressedFile = compressImage(file) withContext(Dispatchers.Main) { // 更新UI imageView.setImageBitmap(BitmapFactory.decodeFile(compressedFile.absolutePath)) } } }
在实际项目中,我发现很多开发者会忽略Android版本差异带来的兼容性问题,特别是从相册获取图片路径的处理。通过封装一个统一的图片选择器工具类,可以大大减少重复代码和潜在错误。另外,对于要求较高的图片裁剪需求,建议考虑使用专业的第三方库如uCrop,它们通常提供更稳定的裁剪体验和更丰富的功能选项。