阅读 618

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

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

前言

  最近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)filefile
Android7到Android10以下(不包括Android10)contentfile

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

适配介绍

在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);
}

复制代码

相册回调:

复制代码

@Overrideprotected 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;
    }
}

复制代码

裁剪回调:

复制代码

@Overrideprotected 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成功成功成功拍照、图库、裁剪均可

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

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

服务器评测 http://www.cncsto.com/ 

服务器测评 http://www.cncsto.com/ 

站长资源 https://www.cscnn.com/ 


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐