适配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) file file
Android7到Android10以下(不包括Android10) content file

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

适配介绍

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

<manifest xmlns:android="//schemas.android.com/apk/res/android"
      xmlns:tools="//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

 

Tags: