1. Android相机与相册开发基础
在移动应用开发中,相机和相册功能是最常用的基础能力之一。无论是社交应用的头像上传,还是电商平台的商品评价,都离不开图片的拍摄和选择。作为Android开发者,掌握这两个功能的实现原理和技巧至关重要。
记得我第一次实现相机功能时,遇到了一个典型问题:拍完照片后无法在相册中显示。后来发现是因为没有发送系统广播通知媒体库更新。这种看似简单的功能点,往往藏着不少技术细节。
1.1 核心组件与权限
实现相机和相册功能主要涉及以下几个关键组件:
- Camera API:Android系统提供的相机服务接口
- MediaStore:管理系统媒体文件的ContentProvider
- FileProvider:安全共享应用私有文件的特殊ContentProvider
- Intent:用于启动系统相机和相册界面
在AndroidManifest.xml中需要声明以下权限:
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>从Android 6.0开始,这些危险权限需要在运行时动态申请。我建议使用Google提供的ActivityResult API来处理权限请求,它比传统的onRequestPermissionsResult更简洁:
// 相机权限请求 ActivityResultLauncher<String> cameraPermission = registerForActivityResult( new ActivityResultContracts.RequestPermission(), granted -> { if (granted) { startCamera(); } else { showPermissionDeniedToast(); } });2. 相机拍照功能实现
2.1 启动系统相机
调用系统相机拍照的核心代码如下:
private Uri imageUri; // 用于保存照片URI private void startCamera() { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 创建临时文件 File photoFile = createImageFile(); if (photoFile != null) { imageUri = FileProvider.getUriForFile( this, "com.example.myapp.fileprovider", photoFile); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); } }这里有几个关键点需要注意:
- 从Android 7.0开始,直接使用file:// URI会抛出FileUriExposedException,必须使用FileProvider
- 照片保存路径应该使用getExternalFilesDir(),这样卸载应用时会自动清理
- 不同厂商手机可能有不同的相机实现,需要处理各种兼容性问题
2.2 处理拍照结果
在onActivityResult中处理返回的照片:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) { try { // 压缩图片避免OOM Bitmap bitmap = decodeSampledBitmapFromUri(imageUri, 1000, 1000); imageView.setImageBitmap(bitmap); // 通知系统相册更新 notifyMediaStore(imageUri); } catch (Exception e) { e.printStackTrace(); } } }图片压缩是个很重要的优化点,特别是处理高分辨率相机拍摄的照片:
private Bitmap decodeSampledBitmapFromUri(Uri uri, int reqWidth, int reqHeight) { // 先获取图片尺寸 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, options); // 计算采样率 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 解码图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, options); }3. 相册图片选择实现
3.1 启动系统相册
从相册选择图片的代码相对简单:
private void openAlbum() { Intent intent = new Intent(Intent.ACTION_PICK); intent.setType("image/*"); startActivityForResult(intent, REQUEST_IMAGE_PICK); }但在实际项目中,我们可能需要更精细的控制:
- 只显示图片不显示视频
- 限制选择的图片数量
- 支持多选
Android 13引入了新的照片选择器API,提供了更好的用户体验:
// 单选模式 ActivityResultLauncher<PickVisualMediaRequest> pickMedia = registerForActivityResult(new PickVisualMedia(), uri -> { if (uri != null) { handleSelectedImage(uri); } }); pickMedia.launch(new PickVisualMediaRequest.Builder() .setMediaType(PickVisualMedia.ImageOnly.INSTANCE) .build());3.2 处理不同Android版本的差异
处理相册选择结果时,需要特别注意不同Android版本的差异:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_IMAGE_PICK && resultCode == RESULT_OK) { Uri uri = data.getData(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { handleImageOnKitKat(uri); } else { handleImageBeforeKitKat(uri); } } } @TargetApi(19) private void handleImageOnKitKat(Uri uri) { String imagePath = null; if (DocumentsContract.isDocumentUri(this, uri)) { String docId = DocumentsContract.getDocumentId(uri); if ("com.android.providers.media.documents".equals(uri.getAuthority())) { String id = docId.split(":")[1]; String selection = MediaStore.Images.Media._ID + "=" + id; imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection); } } else if ("content".equalsIgnoreCase(uri.getScheme())) { imagePath = getImagePath(uri, null); } displayImage(imagePath); }4. 图片保存与优化
4.1 图片保存到本地
将图片保存到本地的标准做法:
public void saveImageToGallery(Bitmap bitmap) { // 创建保存目录 File dir = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES), "MyApp"); if (!dir.exists()) { dir.mkdirs(); } // 创建文件 String fileName = System.currentTimeMillis() + ".jpg"; File file = new File(dir, fileName); // 保存图片 try (FileOutputStream fos = new FileOutputStream(file)) { bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos); fos.flush(); // 通知系统相册更新 MediaStore.Images.Media.insertImage( getContentResolver(), file.getAbsolutePath(), fileName, null); // 发送广播刷新相册 sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); } catch (Exception e) { e.printStackTrace(); } }4.2 性能优化建议
在实际项目中,图片处理还需要考虑以下优化点:
- 内存优化:使用inSampleSize减少内存占用
- 异步加载:使用线程池或协程处理耗时操作
- 缓存策略:实现内存和磁盘二级缓存
- 图片压缩:根据显示尺寸压缩图片
- 生命周期管理:避免内存泄漏
一个简单的图片加载器实现:
public class ImageLoader { private ExecutorService executor = Executors.newFixedThreadPool(4); private LruCache<String, Bitmap> memoryCache; public ImageLoader() { // 初始化内存缓存 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; memoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount() / 1024; } }; } public void loadImage(String path, ImageView imageView) { // 先从内存缓存读取 Bitmap bitmap = memoryCache.get(path); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } // 异步加载 executor.execute(() -> { Bitmap loadedBitmap = decodeSampledBitmapFromFile(path, imageView.getWidth(), imageView.getHeight()); memoryCache.put(path, loadedBitmap); // 更新UI imageView.post(() -> { imageView.setImageBitmap(loadedBitmap); }); }); } }5. 常见问题与解决方案
5.1 权限相关问题
问题:在Android 10及以上版本,即使申请了存储权限,仍然无法访问某些文件。
解决方案:
- 使用MediaStore API替代直接文件访问
- 对于应用专属文件,使用getExternalFilesDir()
- 对于共享文件,使用存储访问框架(SAF)
5.2 相机方向问题
问题:某些手机拍摄的照片在ImageView中显示方向不正确。
解决方案:
public static Bitmap rotateImageIfRequired(Bitmap bitmap, Uri uri) throws IOException { ExifInterface exif = new ExifInterface(getContentResolver().openInputStream(uri)); int orientation = exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: return rotateBitmap(bitmap, 90); case ExifInterface.ORIENTATION_ROTATE_180: return rotateBitmap(bitmap, 180); case ExifInterface.ORIENTATION_ROTATE_270: return rotateBitmap(bitmap, 270); default: return bitmap; } }5.3 大图加载OOM问题
问题:加载高分辨率图片时容易导致内存溢出。
解决方案:
- 使用inSampleSize进行下采样
- 使用BitmapRegionDecoder加载图片区域
- 考虑使用第三方库如Glide或Picasso
6. 现代Android开发的最佳实践
随着Android开发的演进,现在推荐使用以下现代技术实现相机和相册功能:
- CameraX:Google推荐的相机开发库,简化了相机实现
- PhotoPicker:Android 13引入的新API,提供统一的图片选择界面
- Coil:基于Kotlin协程的图片加载库
- Activity Result API:简化权限请求和Activity结果处理
CameraX的基本使用示例:
// 创建CameraProviderFuture ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture.addListener(() -> { try { // 绑定生命周期 ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); Preview preview = new Preview.Builder().build(); ImageCapture imageCapture = new ImageCapture.Builder().build(); CameraSelector cameraSelector = new CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK) .build(); cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture); // 设置预览Surface preview.setSurfaceProvider( previewView.getSurfaceProvider()); } catch (Exception e) { e.printStackTrace(); } }, ContextCompat.getMainExecutor(this));在项目开发中,我发现合理组织代码结构非常重要。建议将相机和相册功能封装成独立的模块,通过接口暴露必要功能,这样既便于测试,也方便后续维护和功能扩展。