适配Android4.4~Android11,调用系统相机,系统相册,系统图片裁剪,转换文件(对图片进行上传等操作)
前言
最近Android对于文件的许多方法进行了修改,网络上又没有对Android4到Android11关于系统相机、系统相册和系统裁剪的适配方案,我花了几天事件总结了一下,先上源码
先对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,调用系统相机,系统相册,系统图片裁剪,转换文件(对图片进行上传等操作)的更多相关文章
- iOS开发 调用系统相机和相册
调用系统相机和相册 (iPad,iPhone)打开相机:(iPad,iPhone)//先设定sourceType为相机,然后判断相机是否可用(ipod)没相机,不可用将sourceType设定为相片库 ...
- iOS开发 调用系统相机和相册 分类: ios技术 2015-03-30 15:52 65人阅读 评论(0) 收藏
调用系统相机和相册 (iPad,iPhone) 打开相机:(iPad,iPhone) //先设定sourceType为相机,然后判断相机是否可用(ipod)没相机,不可用将sourceType设定为 ...
- Android相机、相册获取图片显示并保存到SD卡
Android相机.相册获取图片显示并保存到SD卡 [复制链接] 电梯直达 楼主 发表于 2013-3-13 19:51:43 | 只看该作者 |只看大图 本帖最后由 happy小妖同学 ...
- iOS 从相机或相册获取图片并裁剪
今天遇到一个用户头像上传的问题,需要从相册或者相机中读取图片.代码很简单,抽取关键部分,如下: //load user image - (void)UesrImageClicked { UIActio ...
- vue实现PC端调用摄像头拍照人脸录入、移动端调用手机前置摄像头人脸录入、及图片旋转矫正、压缩上传base64格式/文件格式
进入正题 1. PC端调用摄像头拍照上传base64格式到后台,这个没什么花里胡哨的骚操作,直接看代码 (canvas + video) <template> <div> &l ...
- Android调用系统相机和相册并解决data为空,OOM,图片角度不对的问题
最近公司项目用到手机拍照的问题,好不容易在网上copy了一些代码,但是运行起来一大堆bug,先是三星手机上运行程序直接崩掉,debug了一下原来是onActivityResult中data返回为空,找 ...
- C#/.net 通过js调用系统相机进行拍照,图片无损压缩后进行二维码识别
这两天撸了一个需求,通过 JS 调用手机后置相机,进行拍照扫码.前台实现调用手机相机,然后截取图片并上传到后台的功能.后台接收传过来的图片后,通过调用开源二维码识别库 ZXing 进行二维码数据解析 ...
- MUI 单个图片上传预览(拍照+系统相册):先选择->预览->上传提交
1 html部分 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> < ...
- Anndroid 使用相机或相册打开图片
安卓操作相机or相册 笔者做这方面测试的时候,没遇到什么大坑基本上,需要注意的有两点 1. 使用相册打开读取图片需要使用运行时权限,而且还是要在AndroidManifest.xml中进行权限声明 ...
随机推荐
- acwing 4 多重背包问题 I
多重背包 有 n种物品 一共有 m大小的背包,每种物品的价值 大小 个数 为 s[i],v[i],num[i]; #include<bits/stdc++.h>//cmhao #defin ...
- 获取 Windows 密码「GitHub 热点速览 v.21.28」
作者:HelloGitHub-小鱼干 安全问题一直是 GitHub 的一大热点,因为数据安全问题诞生的各类自托管服务便是.而本周周榜上的 2 个和安全主题相关的项目,有些不同.mimikatz 是个老 ...
- php解决约瑟夫环
今天偶遇一道算法题 "约瑟夫环"是一个数学的应用问题:一群猴子排成一圈,按1,2,-,n依次编号.然后从第1只开始数,数到第m只,把它踢出圈,从它后面再开始数, 再数到第m只,在把 ...
- 解决数据库连接池连接mysql时,每隔8小时mysql自动断开所有连接的问题
解决数据库连接池连接mysql时,每隔8小时mysql自动断开所有连接的问题 最近有个问题非常讨厌,我们的工程中使用自己的连接池连接mysql数据库,可mysql数据库每隔8小时就会自动断开所有链接, ...
- libzip开发笔记(二):libzip库介绍、ubuntu平台编译和工程模板
前言 Qt使用一些压缩解压功能,选择libzip库,libzip库比较原始,也是很多其他库的基础支撑库,编译过了windows版本,有需求编译一个ubuntu版本的,交叉编译需求的同样可参照本文章 ...
- C语言:extern应用
前面我们都是将所有的代码写到一个源文件里面,对于小程序,代码不过几百行,这或许无可厚非,但当程序膨胀代码到几千行甚至上万行后,就应该考虑将代码分散到多个文件中,否则代码的阅读和维护将成为一件痛苦的事情 ...
- CF1330B题解
题意: 给定一个长为 \(n\) 序列 \(a\) ,问是否能分成两个排列,并输出方案 (排列:从 \(1-n\) 中选取不同的 \(n\) 个元素组成的序列) 思路: 观察数据范围可以猜出,这题 \ ...
- 牛客OI测试赛2
题目链接:https://www.nowcoder.com/acm/contest/185#question A.无序组数 暴力求出A和B的因子,注意二元组是无序的,因此还要考虑有些因子在A和B中都存 ...
- 答读者问(1):非模式物种找marker;如何根据marker定义细胞类型
下午花了两个小时回答读者的疑问,觉得可以记录下来,也许能帮到一部分人. 第一位读者做的是非模式物种的单细胞. 一开始以为是想问我非模式物种的marker基因在哪儿找,读者朋友也提到了blast 研究的 ...
- ecshop二次开发笔记--订单表结构ecs_order_info说明
-- 表的结构 `ecs_order_info` CREATE TABLE IF NOT EXISTS `ecs_order_info` ( `order_id` mediumint(8) uns ...