[Android]拍照及相册选取图片功能
之前项目中经常会遇到相册选取图片的功能,都是从网上找到现成的模板进行应用,实际应用时会发现有很多问题,趁着这段时间比较充裕,总结一下Android的拍照及相册选取图片;
需要适配6.0之前的版本、7.0版本和8.0版本;估计9.0和8.0差不多,目前视为同样兼容,等我回来更用新机进行测试后再做确认;
针对6.0 以下版本
--拍照部分--
(1)需要配置权限:
<uses-feature android:name="android.hardware.camera" />
<!--相机权限-->
<uses-permission android:name="android.permission.CAMERA" />
<!--写入SD卡的权限:如果你希望保存相机拍照后的照片-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--读取SD卡的权限:打开相册选取图片所必须的权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
(2)拍照功能代码实现:
/**
* 打开系统相机
*/
private void openSysCamera() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
new File(Environment.getExternalStorageDirectory(), imgName)));
startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
}
(3) 处理拍照后的图片<含剪裁图片>:
case CAMERA_RESULT_CODE:
tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
cropPic(Uri.fromFile(tempFile));
break;
(4)裁剪代码实现:
// 裁剪属性 cropIntent.putExtra("return-data", false); 时,使用自定义接收图片的Uri
private static final String IMAGE_FILE_LOCATION = "file:///" + Environment.getExternalStorageDirectory().getPath() + "/temp.jpg";
private Uri imageUri = Uri.parse(IMAGE_FILE_LOCATION);
/**
* 裁剪图片
*
* @param data
*/
private void cropPic(Uri data) {
if (data == null) {
return;
}
Intent cropIntent = new Intent("com.android.camera.action.CROP");
cropIntent.setDataAndType(data, "image/*");
// 开启裁剪:打开的Intent所显示的View可裁剪
cropIntent.putExtra("crop", "true");
// 裁剪宽高比
cropIntent.putExtra("aspectX", 1);
cropIntent.putExtra("aspectY", 1);
// 裁剪输出大小
cropIntent.putExtra("outputX", 320);
cropIntent.putExtra("outputY", 320);
cropIntent.putExtra("scale", true);
/**
* return-data
* 这个属性决定我们在 onActivityResult 中接收到的是什么数据,
* 如果设置为true 那么data将会返回一个bitmap
* 如果设置为false,则会将图片保存到本地并将对应的uri返回,当然这个uri得有我们自己设定。
* 系统裁剪完成后将会将裁剪完成的图片保存在我们所这设定这个uri地址上。我们只需要在裁剪完成后直接调用该uri来设置图片,就可以了。
*/
cropIntent.putExtra("return-data", true);
// 当 return-data 为 false 的时候需要设置这句
//cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// 图片输出格式
//cropIntent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
// 头像识别 会启动系统的拍照时人脸识别
//cropIntent.putExtra("noFaceDetection", true);
startActivityForResult(cropIntent, CROP_RESULT_CODE);
}
--相册部分--
/**
* 打开系统相册
*/
private void openSysAlbum() {
Intent albumIntent = new Intent(Intent.ACTION_PICK);
albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(albumIntent, ALBUM_RESULT_CODE);
}
/**
* 回调系统相册
*/
case ALBUM_RESULT_CODE:
// 相册
cropPic(data.getData());
break;
针对6.0加入了动态申请权限适配
6.0后需要进行动态权限申请,具体逻辑如下:
·如果用户点击了拒绝,但没有点击“不再询问”,这个时候再次进入 · 界面继续弹框;
·如果用户点击了拒绝,且选择了“不再询问”,那么再次进入此界面将会弹框提示打开 APP 的详情界面,手动开启对应权限。
(1)权限申请代码:
这里我们先放一个规范化的使用方法,后面我会把我常用的方法展示;
/**
* 初始化相机相关权限
* 适配6.0+手机的运行时权限
*/
private void initPermission() {
String[] permissions = new String[]{Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE};
//检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 之前拒绝了权限,但没有点击 不再询问 这个时候让它继续请求权限
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
Toast.makeText(this, "用户曾拒绝打开相机权限", Toast.LENGTH_SHORT).show();
ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
} else {
//注册相机权限
ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
}
}
}
(2)权限申请回调:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSIONS:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//成功
Toast.makeText(this, "用户授权相机权限", Toast.LENGTH_SHORT).show();
} else {
// 勾选了不再询问
Toast.makeText(this, "用户拒绝相机权限", Toast.LENGTH_SHORT).show();
/**
* 跳转到 APP 详情的权限设置页
*
* 可根据自己的需求定制对话框,点击某个按钮在执行下面的代码
*/
Intent intent = Util.getAppDetailSettingIntent(PhotoFromSysActivity.this);
startActivity(intent);
}
break;
}
}
/**
* 获取 APP 详情页面intent
*
* @return
*/
public static Intent getAppDetailSettingIntent(Context context) {
Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 9) {
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
} else if (Build.VERSION.SDK_INT <= 8) {
localIntent.setAction(Intent.ACTION_VIEW);
localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
}
return localIntent;
}
(3)下面就是我经常使用的第三方权限判断:
·使用前需要引用外部包:
implementation 'com.github.hotchemi:permissionsdispatcher:2.1.3'
·后面即可直接调用方法,把需要的权限以数组的形式放入PERMISSION_STARTSPOT中。
private static final String[] PERMISSION_STARTSPOT = new String[]{"android.permission.CAMERA"};
if (PermissionUtils.hasSelfPermissions(this, PERMISSION_STARTSPOT)) {
Navigator.navigateToWebActivity(this, Const.toBX, "填写报事报修单");
} else {
new AlertDialog.Builder(this)
.setTitle("申请拍照权限")
.setMessage("相机权限: 扫一扫二维码(要求)")
.setPositiveButton("确定", (dialog, which) -> ActivityCompat.requestPermissions(this, PERMISSION_STARTSPOT, REQUEST_STARTSPOT))
.setNegativeButton("取消", (dialog, which) -> dialog.dismiss())
.show();
}
针对Android7.0及以上适配
由于在Android7.0上,google使用了新的权限机制,所以导致在调用相机的时候,如果传递的URI为”file://”类型,系统会抛出FileUriExposedException这个错误.具体堆栈信息如下:
异常截图
Android 7.0 就是 File 路径的变更,需要使用 FileProvider 来做,下面看拍照的代码。
(1)拍照代码修改实现:
/**
* 打开系统相机
*/
private void openSysCamera() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
// cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
// new File(Environment.getExternalStorageDirectory(), imgName)));
// File file = new File(Environment.getExternalStorageDirectory(), imgName);
try {
file = createOriImageFile();
} catch (IOException e) {
e.printStackTrace();
}
if (file != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
imgUriOri = Uri.fromFile(file);
} else {
imgUriOri = FileProvider.getUriForFile(this, getPackageName() + ".provider", file);
}
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imgUriOri);
startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
}
}
(2)File 对象的创建和 拍照图片的 Uri 对象创建方式更改。创建原图像保存的代码如下:
/**
* 创建原图像保存的文件
*
* @return
* @throws IOException
*/
private File createOriImageFile() throws IOException {
String imgNameOri = "HomePic_" + new SimpleDateFormat(
"yyyyMMdd_HHmmss").format(new Date());
File pictureDirOri = new File(getExternalFilesDir(
Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/OriPicture");
if (!pictureDirOri.exists()) {
pictureDirOri.mkdirs();
}
File image = File.createTempFile(
imgNameOri, /* prefix */
".jpg", /* suffix */
pictureDirOri /* directory */
);
imgPathOri = image.getAbsolutePath();
return image;
}
(3)拍照回调代码修改:
case CAMERA_RESULT_CODE:
// tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
// cropPic(Uri.fromFile(tempFile));
// 适配 Android7.0+
cropPic(getImageContentUri(file));
break;
/**
* 7.0以上获取裁剪 Uri
*
* @param imageFile
* @return
*/
private Uri getImageContentUri(File imageFile) {
String filePath = imageFile.getAbsolutePath();
Cursor cursor = getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{MediaStore.Images.Media._ID},
MediaStore.Images.Media.DATA + "=? ",
new String[]{filePath}, null);
if (cursor != null && cursor.moveToFirst()) {
int id = cursor.getInt(cursor
.getColumnIndex(MediaStore.MediaColumns._ID));
Uri baseUri = Uri.parse("content://media/external/images/media");
return Uri.withAppendedPath(baseUri, "" + id);
} else {
if (imageFile.exists()) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DATA, filePath);
return getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} else {
return null;
}
}
}
上面只是拍照代码的修改,下面还有一个 FileProvider 问题需要做如下配置。
(1)在 res 目录下创建一个名为 xml 的文件夹,并在其下创建一个名为 file_paths.xml 文件,其内容如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path
name="images"
path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
<external-path
name="images"
path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
<external-files-path
name="images"
path="files/Pictures/OriPicture" />
<root-path
name="images"
path="" />
<root-path
name="images"
path="" />
</paths>
(2) 在 AndroidMainfest.xml 中的 application 节点下做如下配置:
<!--FileProvider共享文件、缓存-->
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.cxs.yukumenu.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
注意:我发现了authorities所填写的包名,需要有debug和release的区分,应用时需要注意。
(1)我当前项目是这样应用的,还是有待优化和封装的,如下:
private Intent createCameraIntent() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File externalDataDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DCIM);
File cameraDataDir = new File(externalDataDir.getAbsolutePath() +
File.separator + "browser-photos");
cameraDataDir.mkdirs();
mCameraFilePath = cameraDataDir.getAbsolutePath() + File.separator +
System.currentTimeMillis() + ".jpg";
// 中间的参数 authority 可以随意设置.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
uri = Uri.fromFile(new File(mCameraFilePath));
} else {
uri = FileProvider.getUriForFile(getActivity(), "cn.ebatech.propertyandroid.fileprovider", new File(mCameraFilePath));
}
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
return cameraIntent;
}
(2)回调结果,加入了Broadcast,实现如下:
if (requestCode == TAKE_PHOTO) {
File cameraFile = new File(mCameraFilePath);
Uri result = resultCode == RESULT_OK && cameraFile.exists() ? Uri.fromFile(cameraFile) : null;
if (result != null) {
// Broadcast to the media scanner that we have a new photo
// so it will be added into the gallery for the user.
BaseApplication.getInstance().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result));
}
if (uploadMessageAboveL != null) {
uploadMessageAboveL.onReceiveValue(result == null ? null : new Uri[]{result});
uploadMessageAboveL = null;
} else if (uploadMessage != null) {
uploadMessage.onReceiveValue(result);
uploadMessage = null;
}
}
延伸问题:(虽然我开发中未曾遇到,但是查找资料时有这方面的提示,于是乎总结进来,待用)
<IllegalArgumentException: Failed to find configured root that contains>
java.lang.RuntimeException: Unable to start activity ComponentInfo{.../....EditInfoActivity}: java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/Android/data/.../files/Cache/30001748.jpg
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2680)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2741)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1492)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:160)
at android.app.ActivityThread.main(ActivityThread.java:6139)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:874)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:764)
...
(1)问题解决:
明明这个路径对应的文件是存在的,为什么会找不到呢,感觉到肯定是路径配置那里出问题,经过查找资料,才发现file_paths.xml中配置的external-path是如下作用:
该方式提供在外部存储区域根目录下的文件。
它对应Environment.getExternalStorageDirectory返回的路径:eg:”/storage/emulated/0”;
而external-files-path的作用是我需要的这种路径即:
该方式提供在应用的外部存储区根目录的下的文件。
它对应Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)返回的路径。
eg:”/storage/emulated/0/Android/data/com.jph.simple/files”。