前言

最近Android对于文件的许多方法进行了修改,网络上又没有对Android4到Android11关于系统相机、系统相册和系统裁剪的适配方案,我花了几天事件总结了一下,先上源码

DEMO源码

先对Android的文件系统进行一个初步的总结:

在AndroidQ(Android10)以前,Android的文件系统并不是特别的严格,各个app可以获取到各个位置的文件的路径,安全性非常差。

在AndroidQ以后,文件系统进行了改革,使用了分区储存模式(Scoped Storage),也叫沙盒模式,何谓沙盒?每个App在安装之后会在文件系统中创建一个名称为该App包名命名的文件夹,这个文件夹就叫做沙盒。该模式下,应用只能访问沙盒内部的文件和公共目录下的多媒体文件和下载文件。

拍照、选择系统相册、裁剪都需要用到Uri,Uri分为两种,一种是file类型的,一种是content类型的,file类型的uri可直接得到该uri的真实路径,content类型的uri是一个匿名uri,无法获取具体的文件路径。

AndroidQ以上统一使用公共目录进行拍照和裁剪图片的存储,而对于AndroidQ以下,还需进行AndroidN(Android7)的区分,在AndroidN到AndroidQ以下的拍照使用的uri变成了content,如果还是使用file类型的uri,则会报错,所以需要使用FileProvider进行一个转换,详情看以下的适配过程:

Android版本 拍照传入intent的uri类型 裁剪传入intent的uri类型
Android7以下(不包括Android7) file file
Android7到Android10以下(不包括Android10) content file

对于拍照和裁剪得到的图片,肯定也会收到影响,以下就进行适配的基本介绍。

适配介绍

在AndroidManifest.xml中添加以下配置:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.camerademo">
<!-- 相机权限和文件读写权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.camerademo.fileprovider2"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider><!-- app的fileProvider声明,Android7.0-Android10配置 -->
</application>
</manifest>

在项目的res文件夹中创建一个xml目录,并且在xml目录下创建一个file_paths.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<!--自定义fileProvider路径,Android7.0以上需配置-->
<paths>
<!--external-files-path代表的是context.getExternalFilesDir(null)路径-->
<external-files-path
name="images"
path="."/>
</paths>

在Activity中定义一个全局的Uri对图片进行接收,以便后续操作:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Uri uri;
......
}

1.拍照

检查权限:

if (CameraUtils.checkTakePhotoPermission(this)) {//检查权限
//有权限,打开相机
openCamera();
} else {
//无权限,申请
CameraUtils.requestTakePhotoPermissions(this);
}

打开相机,这里的uri就是拍照后的图片:

//打开相机
private void openCamera() {
uri = CameraUtils.openCamera(this, "test", "albumDir");
}

具体逻辑:

/**
* 打开相机
* AndroidQ以上:图片保存进公共目录内(公共目录/picture/子文件夹)
* AndroidQ以下:相片保存进沙盒目录内(沙盒目录/picture/子文件夹)
* @param activity activity
* @param name 相片名
* @param child 存放的子文件夹
* @return 成功即为uri,失败为null,等到相机拍照后,该uri即为照片
*/
public static Uri openCamera(Activity activity, String name, String child) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(activity.getPackageManager()) == null) {
//无相机
Log.e(TAG, "无相机");
return null;
}
if (name == null || name.equals("")) {
name = System.currentTimeMillis() + ".png";
} else {
name = name + ".png";
}
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Log.e(TAG, "不存在存储卡或没有读写权限");
return null;
}
Uri uri;
if (isAndroidQ) {
uri = createImageUriAboveAndroidQ(activity, name, child);
} else {
uri = createImageCameraUriBelowAndroidQ(activity, name, child);
}
if (uri == null) {
Log.e(TAG, "用于存放照片的uri创建失败");
return null;
}
Log.e(TAG, "cameraUri:" + uri);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
activity.startActivityForResult(intent, CAMERA_TAKE_PHOTO);
return uri;
} /**
* AndroidQ以上创建用于保存相片的uri,(公有目录/pictures/child)
* @param activity activity
* @param name 文件名
* @param child 子文件夹
* @return uri
*/
private static Uri createImageUriAboveAndroidQ(Activity activity, String name, String child) {
ContentValues contentValues = new ContentValues();//内容
ContentResolver resolver = activity.getContentResolver();//内容解析器
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, name);//文件名
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/*");//文件类型
if (child != null && !child.equals("")) {
//存放子文件夹
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/" + child);
} else {
//存放picture目录
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
}
return resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
} /**
* AndroidQ以下创建用于保存拍照的照片的uri,(沙盒目录/pictures/child)
* 拍照传入的intent中
* Android7以下:file类型的uri
* Android7以上:content类型的uri
* @param activity activity
* @param name 文件名
* @param child 子文件夹
* @return content uri
*/
private static Uri createImageCameraUriBelowAndroidQ(Activity activity, String name, String child) {
File pictureDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);//标准图片目录
assert pictureDir != null;//获取沙盒内标准目录是不会为null的
if (getDir(pictureDir)) {
if (child != null && !child.equals("")) {//存放子文件夹
File childDir = new File(pictureDir + "/" + child);
if (getDir(childDir)) {
File picture = new File(childDir, name);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//适配Android7以上的path转uri
return FileProvider.getUriForFile(activity, AUTHORITY, picture);
} else {
//Android7以下
return Uri.fromFile(picture);
}
} else {
return null;
}
} else {//存放当前目录
File picture = new File(pictureDir, name);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//适配Android7以上的path转uri,该方法得到的uri为content类型的
return FileProvider.getUriForFile(activity, AUTHORITY, picture);
} else {
//Android7以下,该方法得到的uri为file类型的
return Uri.fromFile(picture);
}
}
} else {
return null;
}
}

在onActivityResult中使用imageView的setImageURI()方法即可打开该图片,并且告知图库图片更新:

if (requestCode == CameraUtils.CAMERA_TAKE_PHOTO) {
//相机跳转回调
ivPicture.setImageURI(uri);//展示图片
//通知系统相册更新信息
CameraUtils.updateSystem(this, uri);
}

由于广播更新的方法已经弃用:

context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));

使用以下方法更新图库:

/**
* 更新系统相册
* @param uri uri
*/
public static void updateSystem(Context context, Uri uri) {
if (uri == null) {
Log.e(TAG, "uri为空");
return;
}
MediaScannerConnection.scanFile(context, new String[]{uri.getPath()}, null, null);
}

2.相册

检查权限,打开相册:

if (CameraUtils.checkSelectPhotoPermission(this)) {//检查权限
//有权限,打开相册
openAlbum();
} else {
//无权限,申请
CameraUtils.requestSelectPhotoPermissions(this);
} //打开相册
private void openAlbum() {
uri = null;
CameraUtils.openAlbum(this);
} //打开相册
public static void openAlbum(Activity activity) {
Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
activity.startActivityForResult(intent, CAMERA_SELECT_PHOTO);
}

相册回调:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//activity跳转回调
...
} else if (requestCode == CameraUtils.CAMERA_SELECT_PHOTO) {
//相册跳转回调
if (data != null){
ivPicture.setImageURI(data.getData());
uri = data.getData();
}
}
}

3.裁剪

检查权限,打开裁剪:

//裁剪
if (CameraUtils.checkCropPermission(this)) {//检查权限
//有权限,打开裁剪
openCrop();
} else {
//无权限,申请
CameraUtils.requestCropPermissions(this);
} private void openCrop() {
uri = CameraUtils.openCrop(this, uri, "testCrop", "cropDir");
}

具体逻辑:

/**
* 图片裁剪,裁剪后存放在沙盒目录下(沙盒目录/picture/子文件夹)
* @param activity activity
* @param uri 图片uri
* @param name 裁剪后的图片名
* @param child 子文件夹
* @return 裁剪后的图片uri
*/
public static Uri openCrop(Activity activity, Uri uri, String name, String child) {
if (uri == null) {
Log.e(TAG, "uri为空");
return null;
}
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//未挂在存储设备或者没有读写权限
return null;
}
if (name != null && !name.equals("")) {
name = name + ".png";
} else {
name = System.currentTimeMillis() + ".png";
} Uri resultUri;
if (isAndroidQ) {
resultUri = createImageUriAboveAndroidQ(activity, name, child);
} else {
resultUri = createImageCropUriBelowAndroidQ(activity, name, child);
}
if (resultUri == null) {
Log.e(TAG, "用于存放照片的uri创建失败");
return null;
}
Log.e(TAG, "cropUri:" + resultUri);
Intent intent = new Intent("com.android.camera.action.CROP");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.setDataAndType(uri, "image/*");
// 设置裁剪
intent.putExtra("crop", "true");
// aspectX aspectY 是宽高的比例
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1); intent.putExtra(MediaStore.EXTRA_OUTPUT, resultUri);
// 图片格式
intent.putExtra("outputFormat", "png");
intent.putExtra("noFaceDetection", true);// 取消人脸识别
intent.putExtra("return-data", true);// true:不返回uri,false:返回uri
activity.startActivityForResult(intent, CAMERA_CROP);
return resultUri;
} /**
* AndroidQ以下创建用于保存裁剪的uri,(沙盒目录/pictures/child)
* 裁剪传入intent的uri跟拍照不同
* 在AndroidQ以下统一使用file类型的uri,所以统一用Uri.fromFile()方法返回
* @param activity activity
* @param name 文件名
* @param child 子文件夹
* @return file uri
*/
private static Uri createImageCropUriBelowAndroidQ(Activity activity, String name, String child) {
File pictureDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);//标准图片目录
assert pictureDir != null;//获取沙盒内标准目录是不会为null的
if (getDir(pictureDir)) {
if (child != null && !child.equals("")) {//存放子文件夹
File childDir = new File(pictureDir + "/" + child);
if (getDir(childDir)) {
File picture = new File(childDir, name);
return Uri.fromFile(picture);
} else {
return null;
}
} else {//存放当前目录
File picture = new File(pictureDir, name);
return Uri.fromFile(picture);
}
} else {
return null;
}
}

裁剪回调:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//activity跳转回调
...
} else if (requestCode == CameraUtils.CAMERA_CROP) {
//裁剪跳转回调
if (uri == null) {
return;
}
ivPicture.setImageURI(uri);
//通知系统相册更新信息
CameraUtils.updateSystem(this, uri);
}
}

4.转换File

相册默认将图片复制到沙盒内进行操作,拍照和裁剪在AndroidQ以下会直接拿到源文件,AndroidQ以上默认复制到沙盒内操作

if (uri != null) {
File file = CameraUtils.uriToFile(this, uri);
if (file != null) {
tvFilePath.setText("路径:" + file.getPath());
} else {
tvFilePath.setText("file:null");
}
} else {
tvFilePath.setText("null");
} /**
* 将uri转换为file
* uri类型为file的直接转换出路径
* uri类型为content的将对应的文件复制到沙盒内的cache目录下进行操作
* @param context 上下文
* @param uri uri
* @return file
*/
public static File uriToFile(Context context, Uri uri) {
if (uri == null) {
Log.e(TAG, "uri为空");
return null;
}
File file = null;
if (uri.getScheme() != null) {
Log.e(TAG, "uri.getScheme():" + uri.getScheme());
if (uri.getScheme().equals(ContentResolver.SCHEME_FILE) && uri.getPath() != null) {
//此uri为文件,并且path不为空(保存在沙盒内的文件可以随意访问,外部文件path则为空)
file = new File(uri.getPath());
} else if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
//此uri为content类型,将该文件复制到沙盒内
ContentResolver resolver = context.getContentResolver();
@SuppressLint("Recycle")
Cursor cursor = resolver.query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
try {
InputStream inputStream = resolver.openInputStream(uri);
if (context.getExternalCacheDir() != null) {
//该文件放入cache缓存文件夹中
File cache = new File(context.getExternalCacheDir(), fileName);
FileOutputStream fileOutputStream = new FileOutputStream(cache);
if (inputStream != null) {
// FileUtils.copy(inputStream, fileOutputStream);
//上面的copy方法在低版本的手机中会报java.lang.NoSuchMethodError错误,使用原始的读写流操作进行复制
byte[] len = new byte[Math.min(inputStream.available(), 1024 * 1024)];
int read;
while ((read = inputStream.read(len)) != -1) {
fileOutputStream.write(len, 0, read);
}
file = cache;
fileOutputStream.close();
inputStream.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return file;
}

至此,适配已经完成,以下是测试结果:

机型 Android版本 拍照 图库 裁剪 获取file
红米k30s至尊纪念版-Redmi K30S Uitra(真机) Android11 成功 成功 成功 拍照、图库、裁剪均可
华为Mate10-HUAWEI ALP-AL00(mumu模拟器) Android6.0.1 成功 成功 成功 拍照、图库、裁剪均可
小米9-MI 9(夜神模拟器) Android7.1.2 成功 成功 成功 拍照、图库、裁剪均可
三星Note10-SM N976N(夜神模拟器) Android5.1.1 成功 成功 成功 拍照、图库、裁剪均可
荣耀9-LLD-AL00(真机) Android9.1.0 成功 成功 成功 拍照、图库、裁剪均可

在测试的最后发现一个问题,部分机型在拍照和裁剪之后,无法更新进系统相册,有知道原因的请告知,谢谢!

如果文章内容有错误的,敬请批评指正!

欢迎添加本人QQ骚扰:1336140321

适配Android4.4~Android11,调用系统相机,系统相册,系统图片裁剪,转换文件(对图片进行上传等操作)的更多相关文章

  1. iOS开发 调用系统相机和相册

    调用系统相机和相册 (iPad,iPhone)打开相机:(iPad,iPhone)//先设定sourceType为相机,然后判断相机是否可用(ipod)没相机,不可用将sourceType设定为相片库 ...

  2. iOS开发 调用系统相机和相册 分类: ios技术 2015-03-30 15:52 65人阅读 评论(0) 收藏

     调用系统相机和相册 (iPad,iPhone) 打开相机:(iPad,iPhone) //先设定sourceType为相机,然后判断相机是否可用(ipod)没相机,不可用将sourceType设定为 ...

  3. Android相机、相册获取图片显示并保存到SD卡

    Android相机.相册获取图片显示并保存到SD卡 [复制链接]   电梯直达 楼主    发表于 2013-3-13 19:51:43 | 只看该作者 |只看大图  本帖最后由 happy小妖同学 ...

  4. iOS 从相机或相册获取图片并裁剪

    今天遇到一个用户头像上传的问题,需要从相册或者相机中读取图片.代码很简单,抽取关键部分,如下: //load user image - (void)UesrImageClicked { UIActio ...

  5. vue实现PC端调用摄像头拍照人脸录入、移动端调用手机前置摄像头人脸录入、及图片旋转矫正、压缩上传base64格式/文件格式

    进入正题 1. PC端调用摄像头拍照上传base64格式到后台,这个没什么花里胡哨的骚操作,直接看代码 (canvas + video) <template> <div> &l ...

  6. Android调用系统相机和相册并解决data为空,OOM,图片角度不对的问题

    最近公司项目用到手机拍照的问题,好不容易在网上copy了一些代码,但是运行起来一大堆bug,先是三星手机上运行程序直接崩掉,debug了一下原来是onActivityResult中data返回为空,找 ...

  7. C#/.net 通过js调用系统相机进行拍照,图片无损压缩后进行二维码识别

    这两天撸了一个需求,通过 JS  调用手机后置相机,进行拍照扫码.前台实现调用手机相机,然后截取图片并上传到后台的功能.后台接收传过来的图片后,通过调用开源二维码识别库 ZXing 进行二维码数据解析 ...

  8. MUI 单个图片上传预览(拍照+系统相册):先选择->预览->上传提交

    1 html部分 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> < ...

  9. Anndroid 使用相机或相册打开图片

    安卓操作相机or相册 笔者做这方面测试的时候,没遇到什么大坑基本上,需要注意的有两点 1.   使用相册打开读取图片需要使用运行时权限,而且还是要在AndroidManifest.xml中进行权限声明 ...

随机推荐

  1. webrtc之TURE、STUN、摄像头打开实战

    前言: 大家周末好,今天给 webrtc之TURE.STUN.摄像头打开实战 大家分享的是webrtc第一篇文章,在之前的音视频文章里面没有分享过关于webrtc的内容:在上个周末分享了一篇关于播放器 ...

  2. 6 shell内置命令

    知识点1:什么是shell内建命令? Shell 内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件. 内建命令与普通命令的性质是不一样的,内建命令并不是某个外部文件,只要在 ...

  3. ollvm混淆的某apk题目的逆向分析

    打开jadx,就发现了我们的老朋友数字壳 典型的类抽取壳,直接上fart脱就完事了,我这里使用的是fart的frida脚本,省去了刷机的步骤 这里的脱壳脚本,自行去github的寒冰大佬那边clone ...

  4. String、StringBuilder和StringBuffer的比较

    目录 1.String特性 1.1 不可变 1.2 字符串常量池 2.StringBuilder和StringBuffer 2.1 区别 2.2 应用场景 1.String特性 1.1 不可变 它是I ...

  5. 【Azure 环境】Azure通知中心(Notification Hub)使用百度推送平台解说

    问题描述 在通知中心的页面中显示支持BaiDu,介绍一下支持的是百度(Baidu)的什么吗?Azure的这个功能在国内使用的时候是否可以保证国内安卓手机的信息送达率? 问题解答 通知中心的页面中的Ba ...

  6. Java003-String字符串

    1.这两种定义有什么区别 /*** * 面试题:这两种定义方式有什么区别? * 如何证明? * @param args */ public static void main(String[] args ...

  7. 两万字Vue.js基础学习笔记

    Vue.js学习笔记 目录 Vue.js学习笔记 ES6语法 1.不一样的变量声明:const和let 2.模板字符串 3.箭头函数(Arrow Functions) 4. 函数的参数默认值 5.Sp ...

  8. Leetcode春季打卡第四天:994. 腐烂的橘子

    Leetcode春季打卡第四天:994. 腐烂的橘子 Leetcode春季打卡第四天:994. 腐烂的橘子 思路 思路是采用广度优先搜索,一层一层遍历. 首先先扫描矩阵,将坏橘子放进队列,记录正常橘子 ...

  9. python框架之Flask

    介绍:Flask是一个使用 Python 编写的轻量级 Web 应用框架.其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 . WSGl:Web Server Gateway ...

  10. odoo14在列表视图里添加自定义按钮

    static/js/xxxx.js 这里定义按钮odoo.define('add.tree.view.buttons', function (require) { "use strict&q ...