700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Android 拍照和图库功能(适配Android 6.0和7.0系统和华为机型问题)

Android 拍照和图库功能(适配Android 6.0和7.0系统和华为机型问题)

时间:2021-05-04 11:19:35

相关推荐

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{@Overrideprotected 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*/@Overridepublic 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*/@Overridepublic void onPermissionsGranted(int requestCode, List<String> perms) {ToastUtils.showToast(getApplicationContext(), "用户授权成功");}/*** 请求权限失败** @param requestCode* @param perms*/@Overridepublic void onPermissionsDenied(int requestCode, List<String> 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="/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 ,不许对外共享,临时授权,访问目录配置

<!-- FileProvider配置访问路径,适配7.0及其以上 --><provider android:name="android.support.v4.content.FileProvider"android:authorities="${applicationId}.provider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid: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*/@Overrideprotected void onSaveInstanceState(Bundle outState) {super.onSaveInstanceState(outState);outState.putString(TAG, picturePath);}

当系统重新创建该Activity后,从onCreate()中参数中获取,图片路径:

@Overrideprotected 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异步加载。

@Overrideprotected 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<Bitmap> bitmapObservable= ObservableUtils.loadPictureBitmap(getApplicationContext(), picturePath, show_iv);executeObservableTask(bitmapObservable);}private void executeObservableTask(Observable<Bitmap> observable) {Subscription subscription = observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(bitmap ->show_iv.setImageBitmap(bitmap), error ->ToastUtils.showToast(getApplicationContext(), "加载图片出错"));positeSubscription.add(subscription);}

一个工具类ObservableUtils,构建Observable对象:

public class ObservableUtils {/*** 加载拍照的相片** @param context* @param picturePath* @param imageView* @return*/public static Observable<Bitmap> 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:

@Overrideprotected 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<Bitmap> bitmapObservable= ObservableUtils.loadPictureBitmap( getApplicationContext() , picturePath, show_iv);executeObservableTask(bitmapObservable);}private void executeObservableTask(Observable<Bitmap> observable) {Subscription subscription = observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(bitmap ->show_iv.setImageBitmap(bitmap), error ->ToastUtils.showToast(getApplicationContext(), "加载图片出错"));positeSubscription.add(subscription);}

查询到图片路径后,直接生成对应的bitmap:

public class ObservableUtils {/*** 加载拍照的相片** @param context* @param picturePath* @param imageView* @return*/public static Observable<Bitmap> 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<Bitmap> 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;}/*** <br>功能简述:4.4及以上获取图片的方法* <br>功能详细描述:* <br>注意:* @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;// DocumentProviderif (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {// ExternalStorageProviderif (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];}}// DownloadsProviderelse 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);}// MediaProviderelse 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);}// Fileelse 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的拍照和图库选择图片功能介绍完了,期间遇到的坑,心里都有数。本项目的代码也会分享出来,下面有连接。

项目案例:/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处理系统裁剪功能异常(适配版)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。