众所周知,调用相机拍照和图库中获取图片的功能,基本上是每个程序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特性
从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官方库,高效处理权限。
开启相机拍照是通过Intent来实现,在Intent中指定输出图片路径,相机拍照成功后,系统会将图片数据自动输出到指定路径,生成对应的图片。关闭相机后,会在对应的Activity中的onActivityResult()中返回结果,是否拍照成功的标示。
private String picturePath;
/**
*Activity中通过Intent调用相机,指定输出图片路径。
*/
@Override
public 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;
}
}
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文件,编写以下代码
<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问题。
当跳转到其它运行程序时候,系统可能因内存不足,回收了当前的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操作,又得筛选数据,面对这种需求,不推举使用。
当拍照完成或者取消,都会在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)
,这个用于处理图片旋转的问题。
当图片角度旋转后,若是直接加载出来,对用户体验是非常差劲的。可通过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个坑,真心不容易,相信不少的开发者都遇到过这些问题。接下来,检验成果的时候到了。
实现图库选择相片的代码很简单,通过Intent开启图库,然后选择需要的图片,会在activity中onActivityResult()中返回Uri。接下来,根据Uri查询到对应的图片路径,最后根据路径加载Bitmap,显示到UI上。
图库也是需要读取权限的,但上面的拍照功能具备了写入权限,写入权限包含读取权限,因此,这里不需要再做处理。更多详情,可以阅读 Android 6.0 访问图库时,报错 requires android.permission.READ_EXTERNAL_STORAGE异常.
/**
* 打开图库
* @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);
}
@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,查询不出来对应的图片路径。这就相当悲催了的事情。
除开权限问题外,还有处理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());
}
}
踩完坑,直接看效果如何。
Android的拍照和图库选择图片功能介绍完了,期间遇到的坑,心里都有数。本项目的代码也会分享出来,下面有连接。
相关资源: