Android 拍照和图库功能(适配Android 6.0和7.0系统和华为机型问题)
众所周知,调用相机拍照和图库中获取图片的功能,基本上是每个程序App必备的。
实现适配Android每个版本,国内手机,要处理的问题却也不少。例如:Android6.0权限问题,Android7.0 FileProvider问题,华为手机图库获取不到图片的问题。
本篇内容概述:
调用系统相机拍照
图库选取图片
处理华为图库获取不到图片问题
处理部分手机拍照后,图片旋转角度问题
RxJava加载图片,向上取整计算合适比例。
EasyPermission库处理去读写权限( 适配Android6.0系统及其以上)
FileProvider访问文件(适配Android7.0系统及其以上)
跳转其他程序,Activity被系统因内存不足回收,处理数据保存问题。
项目前期配置:
依赖库添加:
dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:26.+' compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' //谷歌官方权限库 compile 'pub.devrel:easypermissions:1.0.1' //异步消息通知库 compile 'io.reactivex:rxjava:1.3.3' compile 'io.reactivex:rxandroid:1.2.1'}
编码方式:Java+retrolambda库实现Java8特性
Android拍照功能
1. 赋予读写权限:
从Android6.0开始,需要动态赋予权限,而不是安装时候赋予权限。拍照功能需要用到写入磁盘的权限。
在AndroidManifest.xml中注册读写权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE">
第一步:检查权限和申请读写权限。 这里,使用EasyPermission库处理权限问题。
public class MainActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); checkWritePermission(); } /** * 检查读写权限权限 */ private void checkWritePermission() { boolean result = PermissionManager.checkPermission(this, Constance.PERMS_WRITE); if (!result) { PermissionManager.requestPermission(this, Constance.WRITE_PERMISSION_TIP , Constance.WRITE_PERMISSION_CODE, Constance.PERMS_WRITE); } } /** * 重写onRequestPermissionsResult,用于接受请求结果 * * @param requestCode * @param permissions * @param grantResults */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); //将请求结果传递EasyPermission库处理 EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this); } /** * 请求权限成功 * * @param requestCode * @param perms */ @Override public void onPermissionsGranted(int requestCode, List perms) { ToastUtils.showToast(getApplicationContext(), "用户授权成功"); } /** * 请求权限失败 * * @param requestCode * @param perms */ @Override public void onPermissionsDenied(int requestCode, List perms) { ToastUtils.showToast(getApplicationContext(), "用户授权失败"); /** * 若是在权限弹窗中,用户勾选了'NEVER ASK AGAIN.'或者'不在提示',且拒绝权限。 * 这时候,需要跳转到设置界面去,让用户手动开启。 */ if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) { new AppSettingsDialog.Builder(this).build().show(); } }}
权限管理类PermissionManager,代码如下:
public class PermissionManager { /** * @param context * return true:已经获取权限 * return false: 未获取权限,主动请求权限 */ // @AfterPermissionGranted(Constance.WRITE_PERMISSION_CODE) 是可选的 public static boolean checkPermission(Activity context, String[] perms) { return EasyPermissions.hasPermissions(context, perms); } /** * 请求权限 * @param context */ public static void requestPermission(Activity context,String tip,int requestCode,String[] perms) { EasyPermissions.requestPermissions(context, tip,requestCode,perms); }}
更多详情,请阅读Android EasyPermissions官方库,高效处理权限。
2. Intent调用相机进行拍照:
开启相机拍照是通过Intent来实现,在Intent中指定输出图片路径,相机拍照成功后,系统会将图片数据自动输出到指定路径,生成对应的图片。关闭相机后,会在对应的Activity中的onActivityResult()中返回结果,是否拍照成功的标示。
private String picturePath;/** *Activity中通过Intent调用相机,指定输出图片路径。 */@Overridepublic void camera() { this.picturePath = FileUtils.getBitmapDiskFile(this.getApplicationContext()); CameraUtils.openCamera(this, Constance.PICTURE_CODE, this.picturePath);}public class CameraUtils { /** * 打开相机 * @param context * @param requestCode * @return */ public static void openCamera(Activity context, int requestCode, String picturePath){ Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (intent.resolveActivity(context.getPackageManager()) != null) { /** * 指定拍照存储路径 * 7.0 及其以上使用FileProvider替换'file://'访问 */ if (Build.VERSION.SDK_INT>=24){ //这里的BuildConfig,需要是程序包下BuildConfig。 intent.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(context.getApplicationContext(), BuildConfig.APPLICATION_ID+".provider",new File(picturePath))); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); }else{ intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(picturePath))); } context.startActivityForResult(intent, requestCode); } }}
这里你会发觉多了,一个匹配android7.0的FileProvider,用于处理file://
访问的问题。接下来,会讲解到它。
工具类FileUtils生成图片的路径,代码如下:
public class FileUtils { /** * 获得存储bitmap的文件 * getExternalFilesDir()提供的是私有的目录,在app卸载后会被删除 * * @param context * @param * @return */ public static String getBitmapDiskFile(Context context) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalFilesDir(DIRECTORY_PICTURES).getAbsolutePath(); } else { cachePath =context.getFilesDir().getAbsolutePath(); } return new File(cachePath +File.separator+ getBitmapFileName()).getAbsolutePath(); } public static final String bitmapFormat = ".png"; /** * 生成bitmap的文件名:日期,md5加密 * * @return */ public static String getBitmapFileName() { StringBuilder stringBuilder = new StringBuilder(); try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); String currentDate = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); mDigest.update(currentDate.getBytes("utf-8")); byte[] b = mDigest.digest(); for (int i = 0; i < b.length; ++i) { String hex = Integer.toHexString(0xFF & b[i]); if (hex.length() == 1) { stringBuilder.append('0'); } stringBuilder.append(hex); } } catch (Exception e) { e.printStackTrace(); } String fileName = stringBuilder.toString() + bitmapFormat; return fileName; }}
3. 处理anroid7.0中禁止file
的Uri问题:
anroid7.0 行为变更:
android 7.0发生了一些行为变化,禁止应用程序向外部公开file://的URI。
尝试传递file://URI会触发FileUriExposedException。
应用程序之间共享数据,应该发送content://的URI,且授予URI临时访问权限。推举使用FileProvider。更多详情,阅读android 7.0行为变更。
配置FileProvider:
在src\main\res路径下创建xml文件夹,然后在创建一个provider_paths.xml文件,编写以下代码
<?xml version="1.0" encoding="utf-8"?><paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="Pictures" path="/">files-path> <external-path path="Android/data/${applicationId}/" name="files_root" /> <root-path name="root" path="/" />paths>
接下来,在AndroidManifest.xml中注册FileProvider:为FileProvidre配置,指定authorities,name ,不许对外共享,临时授权,访问目录配置
<provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> provider>
配置完成后,便可以直接使用定义authorities所对应的FileProvider。
更多详情,请阅读Android 7.0 报android.os.FileUriExposedException异常。
若是配置过程中遇到问题,请阅读Android FileProvider配置报错android.content.pm.ProviderInfo.loadXmlMetaData问题。
4. 处理系统内存不足时候,导致界面回收,数据丢失的问题:
当跳转到其它运行程序时候,系统可能因内存不足,回收了当前的Activity。而Activity当前数据没有保存,即使系统重新创建该Activity后,也会出现空白页面。
当系统因内存不足,回收activity前,会执行onSaveInstanceState(Bundle outState)
,因此,将拍照后的图片路径存储起来。
private String picturePath; /** * 防止系统内存不足销毁Activity * ,这里保存数据,便于恢复。 * @param outState */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(TAG, picturePath); }
当系统重新创建该Activity后,从onCreate()中参数中获取,图片路径:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recoverState(savedInstanceState); } /** * 恢复被系统销毁的数据 * @param savedInstanceState */ private void recoverState(Bundle savedInstanceState) { if (savedInstanceState != null) { this.picturePath = savedInstanceState.getString(TAG); } }
这里,举一个例子:
一个界面需要拍照很多张图片,然后显示出。因需要多次打开相机程序,再返回来加载生成的图片。这种需要,铁定容易碰到以上问题。
Activity被系统回收,具备偶然性,但存在问题,终究还是要处理。
这里,延伸一点:
android保存数据,要么放在内存中,要么放在磁盘中。磁盘读写是IO操作,又得筛选数据,面对这种需求,不推举使用。
5. RxJava异步加载拍照图片,向上取整加载:
当拍照完成或者取消,都会在Activity的onActivityResult()中返回结果,是否拍照成功的标示。
在磁盘中生成的图片是一个文件,加载文件是IO操作,耗时,考虑RxJava异步加载。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { //拍照返回 case Constance.PICTURE_CODE: if (resultCode == Activity.RESULT_OK) { loadPictureBitmap(); } break; default: break; } } private void loadPictureBitmap() { Observable bitmapObservable= ObservableUtils.loadPictureBitmap(getApplicationContext(), picturePath, show_iv); executeObservableTask(bitmapObservable); } private void executeObservableTask(Observable observable) { Subscription subscription = observable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(bitmap -> show_iv.setImageBitmap(bitmap) , error -> ToastUtils.showToast(getApplicationContext(), "加载图片出错") ); this.compositeSubscription.add(subscription); }
一个工具类ObservableUtils,构建Observable对象:
public class ObservableUtils { /** * 加载拍照的相片 * * @param context * @param picturePath * @param imageView * @return */ public static Observable loadPictureBitmap(Context context, String picturePath, ImageView imageView) { return Observable.create(subscriber -> { Bitmap bitmap = BitmapUtils.decodeFileBitmap(context, picturePath , imageView.getWidth(), imageView.getHeight()); subscriber.onNext(bitmap); }); }}
在Activity中显示的ImageView是具备大小的,按尺寸加载对应比率的Bitamp,可以节省内存。这里采用向上取整方式,计算合适的比率。
public class BitmapUtils { /** * @param context * @param path * @param targetWith * @param targetHeight * @return */ public synchronized static Bitmap decodeFileBitmap(Context context, String path, int targetWith, int targetHeight) { try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; decodeStreamToBitmap(context, path, options); options.inSampleSize = calculateScaleSize(options, targetWith, targetHeight); options.inJustDecodeBounds = false; Bitmap bitmap = decodeStreamToBitmap(context, path, options); return getNormalBitmap(bitmap, path); } catch (Exception e) { e.printStackTrace(); } return null; } private static Bitmap decodeStreamToBitmap(Context context, String path, BitmapFactory.Options options) { Bitmap bitmap = null; ContentResolver contentResolver = context.getContentResolver(); try { //MIME type需要添加前缀 InputStream inputStream = contentResolver.openInputStream( Uri.parse(path.contains("file:") ? path : "file://" + path)); bitmap = BitmapFactory.decodeStream(inputStream, null, options); inputStream.close(); } catch (Exception e) { e.printStackTrace(); } return bitmap; } /** * 采用向上取整的方式,计算压缩尺寸 * * @param options * @param targetWith * @param targetHeight * @return */ private static int calculateScaleSize(BitmapFactory.Options options, int targetWith, int targetHeight) { int simpleSize; if (targetWith > 0 && targetHeight > 0) { int scaleWith = (int) Math.ceil((options.outWidth * 1.0f) / targetWith); int scaleHeight = (int) Math.ceil((options.outHeight * 1.0f) / targetHeight); simpleSize = Math.max(scaleWith, scaleHeight); } else { simpleSize = 1; } if (simpleSize == 0) { simpleSize = 1; } return simpleSize; }}
细心的人会发觉getNormalBitmap(bitmap, path)
,这个用于处理图片旋转的问题。
6. 处理部分手机拍照后,图片旋转角度问题:
当图片角度旋转后,若是直接加载出来,对用户体验是非常差劲的。可通过ExifInterface对象,进行角度判断,加以处理。
/** * 根据存储的bitmap中旋转角度,来创建正常的bitmap * * @param bitmap * @param path * @return */ private static Bitmap getNormalBitmap(Bitmap bitmap, String path) { int rotate = getBitmapRotate(path); Bitmap normalBitmap; switch (rotate) { case 90: case 180: case 270: try { Matrix matrix = new Matrix(); matrix.postRotate(rotate); normalBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); } } catch (Exception e) { e.printStackTrace(); normalBitmap = bitmap; } break; default: normalBitmap = bitmap; break; } return normalBitmap; } /** * ExifInterface :这个类为jpeg文件记录一些image 的标记 * 这里,获取图片的旋转角度 * * @param path * @return */ private static int getBitmapRotate(String path) { int degree = 0; try { ExifInterface exifInterface = new ExifInterface(path); int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: degree = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: degree = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: degree = 270; break; default: break; } } catch (Exception e) { e.printStackTrace(); } return degree; }
实现一个完美的拍照功能,填了6个坑,真心不容易,相信不少的开发者都遇到过这些问题。接下来,检验成果的时候到了。
运行效果:
Android图库功能
实现图库选择相片的代码很简单,通过Intent开启图库,然后选择需要的图片,会在activity中onActivityResult()中返回Uri。接下来,根据Uri查询到对应的图片路径,最后根据路径加载Bitmap,显示到UI上。
1. 读取权限处理:
图库也是需要读取权限的,但上面的拍照功能具备了写入权限,写入权限包含读取权限,因此,这里不需要再做处理。更多详情,可以阅读 Android 6.0 访问图库时,报错 requires android.permission.READ_EXTERNAL_STORAGE异常.
2. 通过Intent开启相册:
/** * 打开图库 * @param context * @param requestCode */ public static void openGallery(Activity context, int requestCode) { Intent intent = new Intent(Intent.ACTION_PICK, null); intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,"image/*"); context.startActivityForResult(intent, requestCode); }
3. 处理图库程序返回的Uri:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { //图库返回 case Constance.GALLERY_CODE: if (resultCode == Activity.RESULT_OK) { Uri uri = data.getData(); loadGalleryBitmap(uri); } break; default: break; } }
很多小伙伴们都发觉,在华为某些型号的手机上,通过图库返回的Uri,查询不出来对应的图片路径。这就相当悲催了的事情。
4. 处理华为手机图库查询不到图片路径:
除开权限问题外,还有处理Uri的authority问题。
采用RxJava执行异步操作,处理Uri查询图片路径,根据路径加载合适的bitmap。
private void loadPictureBitmap() { Observable bitmapObservable= ObservableUtils.loadPictureBitmap( getApplicationContext() , picturePath, show_iv); executeObservableTask(bitmapObservable); } private void executeObservableTask(Observable observable) { Subscription subscription = observable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(bitmap -> show_iv.setImageBitmap(bitmap) , error -> ToastUtils.showToast(getApplicationContext(), "加载图片出错") ); this.compositeSubscription.add(subscription); }
查询到图片路径后,直接生成对应的bitmap:
public class ObservableUtils { /** * 加载拍照的相片 * * @param context * @param picturePath * @param imageView * @return */ public static Observable loadPictureBitmap(Context context, String picturePath, ImageView imageView) { return Observable.create(subscriber -> { Bitmap bitmap = BitmapUtils.decodeFileBitmap(context, picturePath , imageView.getWidth(), imageView.getHeight()); subscriber.onNext(bitmap); }); } /** * 加载图库中选取的相片 * @param context * @param uri * @param imageView * @return */ public static Observable loadGalleryBitmap(Context context, Uri uri, ImageView imageView) { return Observable.create(subscriber -> { String picturePath = CameraUtils.uriConvertPath(context, uri); subscriber.onNext(picturePath); }).flatMap(path -> loadPictureBitmap(context, (String) path, imageView)); }}
解决方法来源于网络:
public class CameraUtils { /** * 从相册中返回的Uri查询到对应图片的Path * @param context * @param uri * @return */ public static String uriConvertPath(Context context,Uri uri){ String path = null; String scheme = uri.getScheme(); if (scheme.equals("content")) { path =getPath(context, uri); } else { path = uri.getEncodedPath(); } return path; } /** *
功能简述:4.4及以上获取图片的方法 *
功能详细描述: *
注意: * @param context * @param uri * @return */ @TargetApi(Build.VERSION_CODES.KITKAT) private static String getPath(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } } // DownloadsProvider else if (isDownloadsDocument(uri)) { final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); } // MediaProvider else if (isMediaDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?"; final String[] selectionArgs = new String[] { split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else if ("content".equalsIgnoreCase(uri.getScheme())) { if (isGooglePhotosUri(uri)){ return uri.getLastPathSegment();} return getDataColumn(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } return null; } private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int index = cursor.getColumnIndexOrThrow(column); return cursor.getString(index); } } finally { if (cursor != null){ cursor.close();} } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ private static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ private static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ private static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is Google Photos. */ private static boolean isGooglePhotosUri(Uri uri) { return "com.google.android.apps.photos.content".equals(uri.getAuthority()); }}
踩完坑,直接看效果如何。
5. 效果如下:
Android的拍照和图库选择图片功能介绍完了,期间遇到的坑,心里都有数。本项目的代码也会分享出来,下面有连接。
项目案例:https://github.com/13767004362/EasyPermissionDemo
相关资源:
- Android 6.0 访问图库时,报错 requires android.permission.READ_EXTERNAL_STORAGE异常
- Android EasyPermissions官方库,高效处理权限
- Android 7.0 报android.os.FileUriExposedException异常
- Android FileProvider配置报错android.content.pm.ProviderInfo.loadXmlMetaData问题
- Android 7.0处理系统裁剪功能异常(适配版)
更多相关文章
- 最全的android图片加密
- Android中Gif图片的显示
- 让Android程序获得系统的权限,实现关机重启,静默安装等功能
- Android 蓝牙搜索不到设备(android M权限问题)
- Android平台上如何让应用程序获得系统权限以及如何使用platform
- android 手机安装应用程序(APK)权限详细对照表
- Android加载drawable中图片后自动缩放的原理
- Android所设计的权限
- Android仿人人客户端(v5.7.1)——对从服务器端(网络)获取的图片进行