Android 图像选取 图片剪裁 照相选图 照相裁剪 图像压缩 11 - 14更新

前言

已经完整打包成一个工具 , 添加了图像压缩和修改了图像剪裁功能 , 项目地址在这里

https://github.com/ocwvar/PicturePicker

本篇讲的是使用 “Intent.ACTION_PICK” 来选取图片并进行剪裁加载的操作 , 包括以下两个功能

  • 从本地相册读取图片进行剪裁
  • 从照相机获取图片进行剪裁

注意: 本篇使用一个工具类PickUriUtils 使Uri转换成文件路径 , 工具类在文章最后给出. 本文的Bitmap对象没有进行回收和缓存 , 在真正的使用中是需要进行相关的操作的 , 由于这里是演示 , 就不做多余的处理了.

技术注意点

1.返回值区别

因为我们的选图是通过第三方的选图应用来操作的 , 所以他们之间可能会有些区别 , 我们要特别注意!!
获取剪裁图片一般会有两个过程 , 第一步是获取图像 , 第二步是剪裁图像 , 每一步的返回数据会有以下的类型

  1. 返回Context Uri路径 —— 同时携带Bitmap对象
  2. 返回文件 Uri路径 —— 同时携带Bitmap对象
  3. 仅仅返回Bitmap对象

携带Bitmap对象需要”return-data” 为 true 的状态下 , 如果为 false ,在某些选图软件则什么东西都不会返回来 , 所以返回Bitmap对象是必须的 , 但我们可以选择性使用

2.文件解析异常

在某些图库裁剪之后生成的图像文件无法被 BitmapFactory.decodeFile 解析出来 , 但这个过程是没有异常产生的 , 这时候我们就只能使用到携带的 Bitmap 对象了.

3.选取&剪裁流程区别

在一些图库中 , 你选取完图像之后会启动自带的剪裁工具进行剪裁 或 启动第三方应用剪裁 , 这是最理想的状态. 但有的图库选取完了之后不会启动任何剪裁界面 , 而且直接返回一个Uri路径 , 这时候我们就需要单独对返回的图像进行剪裁.

开始编码

主要的注意点说完了 , 下面我们就开始正式编码! 我们一个个功能来做.剩下的一些细小的注意点我们边写边说.

1.使用到的参数变量

    //请求码
    private final int REQUEST_PERMISSION = 300;
    private final int REQUEST_CAMERA = 301;
    private final int REQUEST_LOCAL = 302;
    private final int REQUEST_CUT = 303;

    //请求参数
    //临时储存点1
    private final String TEMPSAVE_PATH = Environment.getExternalStorageDirectory().getPath()+"/temp.jpg";
    //临时储存点2
    private final String TEMPSAVE_PATH2 = Environment.getExternalStorageDirectory().getPath()+"/temp2.jpg";
    //剪裁图像的长
    private final int PICTURE_WIDTH = 200;
    //剪裁图像的高
    private final int PICTURE_HEIGHT = 200;

    //用于显示的ImageView
    ImageView shower;
    //用于储存得到的Bitmap对象
    Bitmap bitmap;

2.从图库选择图像

请求Intent构建

    /**
     * 启动图片选取界面
     */
    private void requestPickFromLocal(){

        //我们需要将选取到的图像
        File tempFile = new File(TEMPSAVE_PATH);
        try {
            //创建临时文件
            tempFile.createNewFile();
            Intent intent = new Intent(Intent.ACTION_PICK,null);
            //选取的是图像类型
            intent.setType("image/*");
            //请求剪裁 (不一定有卵用)
            intent.putExtra("crop", "true");
            //X轴剪裁比例 , 一般为 1
            intent.putExtra("aspectX", 1);
            //Y轴剪裁比例 , 一般为 1
            intent.putExtra("aspectY", 1);
            //X轴剪裁长度
            intent.putExtra("outputX", PICTURE_WIDTH);
            //Y轴剪裁长度
            intent.putExtra("outputY", PICTURE_HEIGHT);
            //是否将剪裁后的图像以Bitmap返回
            intent.putExtra("return-data", true);
            //是否允许缩放 (不一定有卵用)
            intent.putExtra("scale", true);
            //文件输出格式 (不一定有卵用)
            intent.putExtra("outputFormat" , Bitmap.CompressFormat.JPEG.toString());
            //不进行脸部识别 (不一定有卵用)
            intent.putExtra("noFaceDetection", true);
            //得到的文件对象存放的位置 , 以Uri路径
            intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(tempFile));
            startActivityForResult(intent, REQUEST_LOCAL);
        } catch (IOException e) {
            Toast.makeText(MainActivity.this, "无法创建临时文件", Toast.LENGTH_SHORT).show();
            tempFile = null;
        }
    }

处理从图库操作后得到的数据

    /**
     * 处理本地选取图片后的结果
     * @param intent    返回的结果 Intent
     * @return  0:失败 1:成功 2:还需要进行剪裁处理
     */
    private int handlePickFromLocal(Intent intent){
        if(intent == null){
            //Intent 为空 , 则这次的请求失败
            return 0;
        }else if (intent.getData() != null){
            // Intent 携带有 Uri 路径 , 我们优先使用它
            String resultPath = PickUriUtils.getPath(MainActivity.this , intent.getData());

            if (resultPath.equals(TEMPSAVE_PATH)){
                //如果返回的路径和指定的临时路径相同 , 则说明图片已经剪裁过 , 否则没有
                this.bitmap = BitmapFactory.decodeFile(TEMPSAVE_PATH);
                if (this.bitmap == null){
                    //Uri读取失败 , 尝试通过Bitmap读取
                    if (intent.hasExtra("data")){
                        //如果存在数据 , 则读取 , 否则当作失败
                        this.bitmap = intent.getParcelableExtra("data");
                        if (this.bitmap != null){
                            //读取成功
                            return 1;
                        }else {
                            //读取失败
                            return 0;
                        }
                    }else {
                        //没有附带数据 , 操作失败
                        return 0;
                    }
                }else {
                    //Uri读取成功
                    return 1;
                }
            }else {
                //进行剪裁之前需要将图像复制到临时文件 , 否则会直接剪裁原始文件并保存 , 导致原始文件被修改
                if (copyFile(resultPath , TEMPSAVE_PATH)){
                    cropImageFromURI(Uri.fromFile(new File(TEMPSAVE_PATH)));
                    return 2;
                }else {
                    return 0;
                }
            }
        }else if (intent.hasExtra("data")){
            //如果 Intent 携带有 Bitmap 对象 , 我们则直接拿出使用

            /**
             * 这个方式虽然直接 , 但是对内存使用并不友好 , 有可能得到的 Bitmap 对象很大
             * 但是有的图像软件裁剪后不会返回 Uri 数据 , 只会返回剪裁后的 Bitmap 对象
             *
             * 比如: 快图浏览 , OPPO自带图库
             */

            this.bitmap = intent.getParcelableExtra("data");
            return 1;
        }else {
            return 0;
        }
    }

3.单独剪裁图像

请求Intent构建

    /**
     * 剪裁图像
     * @param uri   图像Uri
     */
    private void cropImageFromURI(Uri uri){
        //这里我们使用2号临时文件地址 , 因为可能需要剪裁的文件已经存在1号地址 , 如果使用同一个地址会导致剪裁失败
        File tempFile = new File(TEMPSAVE_PATH2);
        try {
            tempFile.createNewFile();
            Intent intent = new Intent("com.android.camera.action.CROP");
            //传入的第一个参数是要剪裁的文件Uri路径
            intent.setDataAndType(uri, "image/*");
            intent.putExtra("crop", "true");
            intent.putExtra("aspectX", 1);
            intent.putExtra("aspectY", 1);
            intent.putExtra("return-data", true);
            intent.putExtra("scale", true);
            intent.putExtra("outputX", PICTURE_WIDTH);
            intent.putExtra("outputY", PICTURE_HEIGHT);
            //剪裁后输出到的位置
            intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(tempFile));
            startActivityForResult(intent, REQUEST_CUT);
        } catch (IOException e) {
            Toast.makeText(MainActivity.this, "无法创建临时文件", Toast.LENGTH_SHORT).show();
            tempFile = null;
        }
    }

处理剪裁后的数据

     /**
     * 处理剪裁后的数据
     * @param resultCode    返回来的 resultCode
     * @param intent    返回来的 intent
     * @return  执行结果
     */
    private boolean handleCropFromPic(int resultCode , Intent intent){
        if (resultCode == 0){
            //图像裁剪失败
            return false;
        }else if (intent.getData() != null){
            //返回来的是 Uri 路径 , 解析后直接加载即可 , 其文件路径为我们在请求Intent中设定的路径 TEMPSAVE_PATH2
            this.bitmap = BitmapFactory.decodeFile(TEMPSAVE_PATH2);
            if (this.bitmap == null){
                //Uri读取失败 , 尝试读取 Bitmap 对象
                if (intent.hasExtra("data")){
                    //如果存在数据 , 则读取 , 否则当作失败
                    this.bitmap = intent.getParcelableExtra("data");
                }else {
                    //不存在Bitmap数据 , 读取失败
                    return false;
                }
            }
            return this.bitmap != null && !this.bitmap.isRecycled();
        }else if (intent.hasExtra("data")){
            //返回来的是 Bitmap 对象 , 直接加载即可
            this.bitmap = intent.getParcelableExtra("data");
            return this.bitmap != null  && !this.bitmap.isRecycled();
        }else {
            //其他状态 , 为失败
            return false;
        }
    }

以上就是两大功能 , 获取图像&剪裁 下面我们吧一些其他地方补完

4.获取数据以及剩余操作

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);


        switch (requestCode){
            //图像获取请求
            case REQUEST_LOCAL:

                switch (handlePickFromLocal(data)){
                    //处理失败
                    case 0:
                        Toast.makeText(MainActivity.this, "返回数据无效", Toast.LENGTH_SHORT).show();
                        break;
                    //处理成功 , 显示图像
                    case 1:
                        onGotBitmap(this.bitmap);
                        break;
                    //还需要单独剪裁 , 内部已经处理 , 外部就不作处理了
                    case 2:
                    default:
                        break;
                }
                break;
            //单独图像剪裁请求
            case REQUEST_CUT:
                if (!handleCropFromPic(resultCode , data)){
                    Toast.makeText(MainActivity.this, "剪裁无效", Toast.LENGTH_SHORT).show();
                }else {
                    //剪裁处理成功 , 显示图像
                    onGotBitmap(this.bitmap);
                }
                break;
            //照相请求 (我们最后再说这个)
            case REQUEST_CAMERA:
                handlePickFromCamera();
                break;
            default:
                Toast.makeText(MainActivity.this, "未定义操作", Toast.LENGTH_SHORT).show();
                break;
        }
    }

显示图像

    /**
     * 最终获取到图像的时候
     * @param bitmap    得到的Bitmap
     */
    private void onGotBitmap(Bitmap bitmap){
        //显示Bitmap
        shower.setImageBitmap(bitmap);

        //清除临时文件
        new File(TEMPSAVE_PATH).delete();
        new File(TEMPSAVE_PATH2).delete();
    }

复制文件方法

    /**
     * 复制文件
     * @param sourcePosition    源文件路径
     * @param targetPosition    目的地路径
     * @return  执行结果
     */
    private boolean copyFile(String sourcePosition , String targetPosition){
        File sourceFile = new File(sourcePosition);
        File targetFile = new File(targetPosition);
        targetFile.delete();
        try {
            targetFile.createNewFile();
            InputStream inputStream = new FileInputStream(sourceFile);
            OutputStream outputStream = new FileOutputStream(targetFile);
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1){
                outputStream.write(buffer,0,length);
            }
            inputStream.close();
            outputStream.flush();
            outputStream.close();
            return true;
        } catch (Exception e) {
            Log.e("复制文件异常", ""+e );
            return false;
        }
    }

5.照相图像处理

为啥我们最后再说这个 , 因为…这个东西没什么可以说的 , 因为它仅仅是获取图像之后单独进行剪裁而已

请求Intent构建

    /**
     * 启动摄像头获取图像
     */
    private void requestPickFromCamera(){
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        //将拍照得到的图像 , 存储在临时点1
        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(TEMPSAVE_PATH)));
        startActivityForResult(intent, REQUEST_CAMERA);
    }

处理得到的数据

    /**
     * 处理拍照后得到的数据
     */
    private void handlePickFromCamera(){
        /**
         * 拍照后得到照片对象会保存在我们请求中的指定路径内 TEMPSAVE_PATH .
         * 如果设置了带数据返回 "return-data" 则会在Intent中返回一个Bitmap对象
         * 但一般非常不建议这么使用 , 因为一般这个Bitmap对象会很大
         */

        File savedFile = new File(TEMPSAVE_PATH);
        if (savedFile.exists() && savedFile.length() > 0){
            //如果得到的文件存在并且有效 (长度大于0) 我们就将这个对象进行剪裁
            cropImageFromURI(Uri.fromFile(savedFile));
        }else {
            //文件无效 , 操作失败
            Toast.makeText(MainActivity.this, "无效照片文件 , 请检查储存空间是否已满", Toast.LENGTH_SHORT).show();
        }

    }

6.结尾 & 工具类

代码复制粘贴即可使用

public class PickUriUtils {

    /**
     * Get a file path from a Uri. This will get the the path for Storage Access
     * Framework Documents, as well as the _data field for the MediaStore and
     * other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @author paulburke
     */
    public static String getPath(final Context context, final Uri uri) {

        // DocumentProvider
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }

                // TODO handle non-primary volumes
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {

                final String id = DocumentsContract.getDocumentId(uri);
                final Uri contentUri = ContentUris.withAppendedId(
                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

                return getDataColumn(context, contentUri, null, null);
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[] {
                        split[1]
                };

                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }

        return null;
    }

    /**
     * Get the value of the data column for this Uri. This is useful for
     * MediaStore Uris, and other file-based ContentProviders.
     *
     * @param context The context.
     * @param uri The Uri to query.
     * @param selection (Optional) Filter used in the query.
     * @param selectionArgs (Optional) Selection arguments used in the query.
     * @return The value of the _data column, which is typically a file path.
     */
    public static String getDataColumn(Context context, Uri uri, String selection,
                                       String[] selectionArgs) {

        Cursor cursor = null;
        final String column = "_data";
        final String[] projection = {
                column
        };

        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                    null);
            if (cursor != null && cursor.moveToFirst()) {
                final int column_index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(column_index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }


    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is ExternalStorageProvider.
     */
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is DownloadsProvider.
     */
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }

    /**
     * @param uri The Uri to check.
     * @return Whether the Uri authority is MediaProvider.
     */
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }

}

我会在文章下部评论贴上工程的下载地址 , 有需要的同学可以下载 , 使用Android Studio 导入即可

6-19更新内容

我同学的一部华为手机 , 在选取本地图片的时候发现没有数据返回 , 既没有Bitmap对象 , 也没有Uri路径.
最后我发现虽然什么都没有返回来 , 但是已经在当初 Intent 内指定的位置存放有剪切后的图片了.

以下为更新的代码块 , 同样也会更新工程文件的下载地址

else if (new File(TEMPSAVE_PATH).exists()){
            /**
             * 有可能虽然Intent没有返回路径 , 但是可能会已经把我们要的图像处理完成后存放到我们当初指定的位置内了
             * 有的手机内置图库不会返回路径 同时也不会返回Bitmap数据 , 即使设置了 "return-data", true
             *
             * 比如: 华为荣耀 SCL-CL00 , OS: EMUI 3.1
             *
             */

            if (new File(TEMPSAVE_PATH).length() > 0){
                //如果文件大小大于 0 , 则开始从文件解析图像
                this.bitmap = BitmapFactory.decodeFile(TEMPSAVE_PATH);
                if (this.bitmap != null){
                    //如果解析成功
                    return 1;
                }else {
                    //如果解析失败
                    return 0;
                }
            }else {
                //如果文件虽然存在 , 但是并没有内容 , 则吧这个文件清除 , 同时表示这次读取失败
                new File(TEMPSAVE_PATH).delete();
                return 0;
            }
        }

11-14更新内容

( 完整的代码请看Git项目中的代码 点我看 , 项目中逻辑部分基本重新完善了一遍)
在处理图像剪裁部分还有欠缺 , 我们应当请求带Crop剪切的时候计算好裁剪的比例

intent.putExtra("aspectX", CROP_WIDTH_RATION);  //X轴比例
intent.putExtra("aspectY", CROP_HEIGHT_RATION); //Y轴比例
intent.putExtra("outputX", CROP_WIDTH);         //输出图像分辨率的X轴长度 (Width)
intent.putExtra("outputY", CROP_HEIGHT);        //输出图像分辨率的Y轴长度 (Height)

这个X Y轴比例就是我们平常所说的那些16:9之类的 , 怎么算? 看下面的方法:

//我们首先算出 X Y 长度的最大公约数
private int maxCommonDivisor(int width, int height) {
    if (width < height) {// 保证m>n,若m
          int temp = width;
           width = height;
          height = temp;
    }
    if (width % height == 0) {// 若余数为0,返回最大公约数
           return height;
    } else { // 否则,进行递归,把n赋给m,把余数赋给n
          return maxCommonDivisor(height, width % height);
    }
}

//然后分别除到X Y的长度上
final int maxCommonDivisor = maxCommonDivisor(CROP_WIDTH, CROP_HEIGHT);
CROP_WIDTH_RATION = CROP_WIDTH / maxCommonDivisor;
CROP_HEIGHT_RATION = CROP_HEIGHT / maxCommonDivisor;

//最后在Intent里面设置上参数 就行啦!
intent.putExtra("crop", "true");
intent.putExtra("aspectX", CROP_WIDTH_RATION);
intent.putExtra("aspectY", CROP_HEIGHT_RATION);
intent.putExtra("outputX", CROP_WIDTH);
intent.putExtra("outputY", CROP_HEIGHT); 

关于图像压缩

图像压缩其实是项目中有做的 , 不过当初感觉没几行代码就没贴上来讲 , 不过今天都更新了 , 就把这个也说了吧,关于降低图像质量 有两个地方可以调整.

//在Intent传递的时候设置成有损存储格式
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());

//在Bitmap对象保存成本地对象的时候  COMPRESS_VALUE 代表压缩值 , 0~100 , 100为质量最好
//压缩仅在 JPEG 保存的时候才有效 PNG 是无损的并有透明通道的  WEBP我还没有使用过
bitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESS_VALUE, new FileOutputStream(saveFile));

你可能感兴趣的:(Android)