Android拍照与相册选图功能实现与优化
1. Android系统拍照与选图功能实现解析在移动应用开发中调用系统相机拍照和相册选图是最基础但最容易出问题的功能之一。我经历过十几个需要头像上传功能的项目发现90%的兼容性问题都集中在Android 7.0的文件权限管理和不同厂商的系统定制上。本文将分享一套经过生产环境验证的完整解决方案。关键点从Android 7.0API 24开始直接使用file:// URI会触发FileUriExposedException必须改用FileProvider方案。这是很多新手开发者容易踩的坑。2. 核心功能实现步骤2.1 拍照功能实现首先在AndroidManifest.xml中添加必要权限uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE /拍照功能的核心代码实现private var currentPhotoUri: Uri? null fun dispatchTakePictureIntent() { val takePictureIntent Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { // 确保有相机应用能处理该intent resolveActivity(packageManager)?.also { // 创建临时文件 val photoFile createImageFile().apply { currentPhotoUri getUriForFile(thisMainActivity) } // 继续处理拍照结果 putExtra(MediaStore.EXTRA_OUTPUT, currentPhotoUri) } } startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE) } private fun createImageFile(): File { val timeStamp SimpleDateFormat(yyyyMMdd_HHmmss).format(Date()) val storageDir getExternalFilesDir(Environment.DIRECTORY_PICTURES) return File.createTempFile( JPEG_${timeStamp}_, .jpg, storageDir ).apply { // 保存文件路径供后续使用 currentPhotoPath absolutePath } }2.2 FileProvider配置详解在res/xml下创建file_paths.xml?xml version1.0 encodingutf-8? paths external-files-path namemy_images pathPictures / cache-path namemy_cache path/ / /pathsAndroidManifest中的Provider配置provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider经验之谈authorities建议使用applicationId作为前缀避免不同应用间的冲突。实测发现部分国产ROM会严格检查这个值。2.3 相册选图功能相册选择的完整实现需要考虑不同Android版本的处理差异fun openGallery() { val intent Intent().apply { type image/* action Intent.ACTION_GET_CONTENT addCategory(Intent.CATEGORY_OPENABLE) } // 使用Intent.createChooser避免直接调用系统相册 startActivityForResult( Intent.createChooser(intent, 选择图片), REQUEST_PICK_IMAGE ) }处理返回结果的版本适配代码override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { REQUEST_PICK_IMAGE - { if (resultCode RESULT_OK) { data?.data?.let { uri - // 统一处理URI handleSelectedImage(uri) } } } } } private fun handleSelectedImage(uri: Uri) { val inputStream contentResolver.openInputStream(uri) // 处理图片数据... }3. 图片裁剪功能实现3.1 系统裁剪方案fun startCropImage(uri: Uri) { val cropIntent Intent(com.android.camera.action.CROP).apply { setDataAndType(uri, image/*) putExtra(crop, true) putExtra(aspectX, 1) putExtra(aspectY, 1) putExtra(outputX, 300) putExtra(outputY, 300) putExtra(return-data, false) putExtra(MediaStore.EXTRA_OUTPUT, getTempUri()) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } // 检查是否有可用应用 if (cropIntent.resolveActivity(packageManager) ! null) { startActivityForResult(cropIntent, REQUEST_CROP_IMAGE) } else { Toast.makeText(this, 未找到可用的图片编辑应用, Toast.LENGTH_SHORT).show() } }3.2 第三方裁剪库方案由于系统裁剪在不同厂商设备上表现不一致推荐使用第三方库如ucrop添加依赖implementation com.github.yalantis:ucrop:2.2.6使用示例fun startUCrop(sourceUri: Uri) { val destinationUri Uri.fromFile(File(cacheDir, cropped.jpg)) UCrop.of(sourceUri, destinationUri) .withAspectRatio(1f, 1f) .withMaxResultSize(500, 500) .start(this) }4. 常见问题与解决方案4.1 权限问题处理private fun checkPermissions(): Boolean { return ContextCompat.checkSelfPermission( this, Manifest.permission.CAMERA ) PackageManager.PERMISSION_GRANTED ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE ) PackageManager.PERMISSION_GRANTED } private fun requestPermissions() { ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE ), PERMISSION_REQUEST_CODE ) }4.2 厂商兼容性问题小米设备需要在Intent中添加额外参数intent.putExtra(android.intent.extras.CAMERA_FACING, 1)华为设备部分机型需要单独处理Uri权限if (Build.MANUFACTURER.contains(HUAWEI)) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }4.3 大图处理技巧当处理高分辨率图片时建议先压缩fun decodeSampledBitmapFromUri(uri: Uri, reqWidth: Int, reqHeight: Int): Bitmap { return contentResolver.openInputStream(uri)?.use { input - val options BitmapFactory.Options().apply { inJustDecodeBounds true } BitmapFactory.decodeStream(input, null, options) // 计算采样率 options.inSampleSize calculateInSampleSize(options, reqWidth, reqHeight) // 重新读取 input.reset() options.inJustDecodeBounds false BitmapFactory.decodeStream(input, null, options) } ?: throw IOException(无法读取图片) } private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { val (height, width) options.run { outHeight to outWidth } 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 } } return inSampleSize }5. 性能优化建议使用缓存目录存储临时文件val cacheFile File(externalCacheDir, temp_${System.currentTimeMillis()}.jpg)及时清理不再使用的图片资源fun cleanTempFiles() { externalCacheDir?.listFiles()?.forEach { file - if (file.name.startsWith(temp_) file.lastModified() System.currentTimeMillis() - 24 * 60 * 60 * 1000) { file.delete() } } }使用WorkManager处理后台图片处理任务val compressionWork OneTimeWorkRequestBuilderImageCompressionWorker() .setInputData(workDataOf( image_uri to uri.toString() )) .build() WorkManager.getInstance(context).enqueue(compressionWork)在实际项目中我发现很多开发者会忽略对图片EXIF信息的处理这可能导致图片方向错误。建议添加以下处理fun correctImageOrientation(file: File): Bitmap { val exif ExifInterface(file.absolutePath) val orientation exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL ) val bitmap BitmapFactory.decodeFile(file.absolutePath) val matrix Matrix() when (orientation) { ExifInterface.ORIENTATION_ROTATE_90 - matrix.postRotate(90f) ExifInterface.ORIENTATION_ROTATE_180 - matrix.postRotate(180f) ExifInterface.ORIENTATION_ROTATE_270 - matrix.postRotate(270f) } return Bitmap.createBitmap( bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true ) }对于需要频繁调用相机功能的应用建议封装一个CameraHelper类来管理所有相机相关操作包括生命周期管理、异常处理和性能监控。这样可以避免内存泄漏和相机资源未释放的问题。