Android 拍照及相册选取图片功能,已适配Android6.0、7.0、8.0
更换头像或者上传图片功能已基本是每个 APP 所具备的基础功能了,但这对于开发者来说是一个很麻烦的事情,除机型之外,适配版本就至少要考虑这几种情况(6.0以下版本、6.0的动态权限、7.0的FileProvider、8.0的特殊情况)。
今天来个总结,方便自己也方便同行。
功能说明
本文的示例以下图为准:
界面只有一个 ImageView,点击 ImageView 弹出 DialogFragment,分别是拍照和相册选择图片功能,其中都带有系统的裁剪功能,将裁剪后的图片显示在 ImageView 上。如果不需要裁剪功能,只需要将代码中的裁剪方法注释掉即可。
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);
}
其中 imgName 是拍照图片的名字,一般以时间戳再加上自定义字符串命名;CAMERA_RESULT_CODE 是自定义的一个常量,作为拍照的请求码。
onActivityResult 处理拍照后的图片
case CAMERA_RESULT_CODE:
tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
cropPic(Uri.fromFile(tempFile));
break;
将拍照后的图片创建成一个 File 对象,用来裁剪,裁剪的功能代码是和相册选取图片通用的。
裁剪代码
/**
* 裁剪图片
*
* @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);
}
其中的 CROP_RESULT_CODE 也是定义的静态常量,同拍照时定义的常量相同的作用。
如果裁剪时将 return-data 设置为 false,那么需要定义一个 Uri 来保存裁剪后的图片路径。
// 裁剪属性 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);
图片裁剪完成回调
case CROP_RESULT_CODE:
// 裁剪时,这样设置 cropIntent.putExtra("return-data", true); 处理方案如下
if (data != null) {
Bundle bundle = data.getExtras();
if (bundle != null) {
Bitmap bitmap = bundle.getParcelable("data");
imageView.setImageBitmap(bitmap);
// 把裁剪后的图片保存至本地 返回路径
String urlpath = FileUtilcll.saveFile(this, "crop.jpg", bitmap);
L.e("裁剪图片地址->" + urlpath);
}
}
// 裁剪时,这样设置 cropIntent.putExtra("return-data", false); 处理方案如下
// try {
// ivHead.setImageBitmap(BitmapFactory.decodeStream(
// getActivity().getContentResolver().openInputStream(imageUri)));
// } catch (FileNotFoundException e) {
// e.printStackTrace();
// }
break;
FileUtilcll 代码
/**
* 图片文件操作
*/
public class FileUtilcll {
/**
* 将Bitmap 图片保存到本地路径,并返回路径
*
* @param fileName 文件名称
* @param bitmap 图片
* @param资源类型,参照 MultimediaContentType 枚举,根据此类型,保存时可自动归类
*/
public static String saveFile(Context c, String fileName, Bitmap bitmap) {
return saveFile(c, "", fileName, bitmap);
}
public static String saveFile(Context c, String filePath, String fileName, Bitmap bitmap) {
byte[] bytes = bitmapToBytes(bitmap);
return saveFile(c, filePath, fileName, bytes);
}
/**
* Bitmap 转 字节数组
*
* @param bm
* @return
*/
public static byte[] bitmapToBytes(Bitmap bm) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(CompressFormat.JPEG, 100, baos);
return baos.toByteArray();
}
public static String saveFile(Context c, String filePath, String fileName, byte[] bytes) {
String fileFullName = "";
FileOutputStream fos = null;
String dateFolder = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA)
.format(new Date());
try {
String suffix = "";
if (filePath == null || filePath.trim().length() == 0) {
filePath = Environment.getExternalStorageDirectory() + "/cxs/" + dateFolder + "/";
}
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
File fullFile = new File(filePath, fileName + suffix);
fileFullName = fullFile.getPath();
fos = new FileOutputStream(new File(filePath, fileName + suffix));
fos.write(bytes);
} catch (Exception e) {
fileFullName = "";
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
fileFullName = "";
}
}
}
return fileFullName;
}
}
到此,拍照功能就完成了,附带了裁剪。剩下的就是相册选取照片,这个也不难,固定代码,其他功能和拍照时相同的。
/**
* 打开系统相册
*/
private void openSysAlbum() {
Intent albumIntent = new Intent(Intent.ACTION_PICK);
albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(albumIntent, ALBUM_RESULT_CODE);
}
其中的 ALBUM_RESULT_CODE 同样是定义的常量。用来在 onActivityResult 中处理返回结果。
case ALBUM_RESULT_CODE:
// 相册
cropPic(data.getData());
break;
和拍照公用裁剪代码,这里直接调用即可,其他逻辑不变。
6.0动态权限适配
除了上述功能保持不变之外,需要在代码中动态申请文件读写权限和相机权限。这里我将申请的时机写在了刚进入页面,而不是刚打开 APP,也不是点击按钮的时候。
逻辑
- 如果用户点击了拒绝,但没有点击“不再询问”,这个时候再次进入界面继续弹框;
- 如果用户点击了拒绝,且选择了“不再询问”,那么再次进入此界面将会弹框提示打开 APP 的详情界面,手动开启对应权限。
权限申请代码
/**
* 初始化相机相关权限
* 适配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);
}
}
}
这里我将需要的权限统一写在了一个数组里面。其中的 REQUEST_PERMISSIONS 和上面拍照定义的常量功能相同。
权限申请回调
@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;
}
}
其中的 getAppDetailSettingIntent()方法代码如下:
/**
* 获取 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;
}
到这,权限这个适配问题就解决了,接下来该是 Android 7.0 的适配了,这里我们只需要修改拍照的功能即可,相册是没有问题的。
Android7.0 适配
Android 7.0 就是 File 路径的变更,需要使用 FileProvider 来做,下面看拍照的代码。
拍照代码修改
/**
* 打开系统相机
*/
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);
}
}
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;
}
拍照回调代码修改
case CAMERA_RESULT_CODE:
// tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
// cropPic(Uri.fromFile(tempFile));
// 适配 Android7.0+
cropPic(getImageContentUri(file));
break;
getImageContentUri() 代码如下:
/**
* 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 问题需要做如下配置。
- 在 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>
其中这里的 android:authorities 属性值要和代码中 FileProvider.getUriForFile()的第二个参数值保持一致。
注意
- 代码已全部给出,粘贴即可使用
- 本次测试真机:华为5.1.1,一加8.0.0
总结
- 机型适配是个细活,但能锻炼排查问题和解决问题能力。
- 本文的代码有很多是可以抽取的,如果是在项目中使用,那么建议提取出特定功能的代码。
下一篇: 一份完整的拍照后加水印代码