安卓开发——拍照裁剪并保存为头像报错:裁剪图片无法保存的

Posted RoeRoss

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓开发——拍照裁剪并保存为头像报错:裁剪图片无法保存的相关的知识,希望对你有一定的参考价值。

 

在做学校大创项目的安卓开发时,需要从相册获取图片或者拍照,然后裁剪保存为头像。由于我是第一次弄安卓开发,也对Android现在越来越多的权限限制不了解,debug过程真的是异常心塞啊。

  闲话不说(文末慢慢话痨),我开始是在网上找了一些代码打算用到项目上试试,但是连个拍照或者从相册选择图片都频繁报错(应该还是因为sd卡权限之类的吧),折腾了一晚上没有解决,第二天还是老老实实的看《第一行代码》,边学边写。在这里我简单梳理一下流程(关于裁剪后图片无法保存的问题的解释请直接跳到水平线之后):

  •   调用手机摄像头拍照:

 

 //创建file文件,用于存储相机拍下的照片,这里我命名为my_head_image.jpg,并将它放在
 //手机SD卡的应用关联缓存中。
              /*  File outputImage = new File(getExternalCacheDir(), "my_head_image.jpg");
               try {
                    if (outputImage.exists()) {
                        outputImage.delete();
                    }
                    outputImage.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
//将File对象转换为Uri对象,先进行系统版本的判定,android7.0以后的版本和之前的版本不
 //太一样
                if (Build.VERSION.SDK_INT >= 24) {
                    imageUri = FileProvider.getUriForFile(ChangeMyDetails.this, "com.example.write.fileprovider", outputImage);
                } else {
                    imageUri = Uri.fromFile(outputImage);*/
    File outputImage = new File(Environment.getExternalStorageDirectory(), "my_head_image.jpg");
    String path = outputImage.getAbsolutePath();
    Log.i("ChangeMyDetails", "outputImage路径为 "+path);
    try {
     if (outputImage.exists()) {
     outputImage.delete();
     }
     outputImage.createNewFile();
    } catch (IOException e) {
     e.printStackTrace();
    }

    imageUri = Uri.fromFile(outputImage);
  //启动相机程序
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);

  橙色部分的代码是《第一行代码》上的,如果拍照后的图片直接作为头像不裁剪的话这段是没问题的,但是你懂得,后来就崩了。在这里首先创建fFile对象,用于存放拍下的照片,并将它保存在SD卡的应用关联缓存目录下,调用getCacheDir()得到这个目录,具体路径是/sdcard/Android/data/<你的package name>/cache。为什么要放在这里呢? 因为从Android6.0系统开始,读写SD卡被视为危险权限,如何放在其他目录,都要在运行时进行权限处理,而使用应用关联目录则可以跳过这一步。注意:在这里我就种下了裁剪后无法保存的隐患。橙色下面的更正代码是后话了,因为还要涉及权限问题,我后面会讲,所以你看到这里欣喜的粘贴到你的项目里还是会报错的

  接着进行系统版本判断,FileProvider的getUriForFile()方法将File对象封装为Uri对象。getUriForFile()方法接收3个参数,第一个是要求传入的context对象,第二个可以是任意唯一的字符串(后面manifest.xml中注册<procider>的android:authority要用到),第三个是要封装的这个File对象。之所以添加这一步,因为Android7.0系统开始,直接使用本地Uri被认为是不安全的,会抛出FIleURIExposedException异常。FileProvider则是一种特殊的得内容提供器,可以选择性的将封装过的Uri共享给外部,更加安全。

  关于内容提供器,还要在manifest,xml中进行注册:

 

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.write.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>

 

  其中,android:authorities属性值必须与FileProvider.getUriForFile()方法中的第二个参数一致,另外,用<meta-data>来指定Uri的共享路径,并引用@xml/provider_paths资源,这个资源需要自己创建。

  右击res目录→New→Directory,创建一个xml目录,然后右击xml目录→New→File,创建一个provider_paths.xml的文件:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">

    <external-path name="my_images" path="."/>
</paths>

  其中,external-path 就是用来指定Uri共享的,name属性值自定义就可以了,path属性为空表示整个SD卡进行共享。

 

  •   裁剪照片:

   拍照时,使用startActivityForResult(intent, TAKE_PHOTO)来启动活动,因此拍完照会有结果返回到onActivityResult()方法中,拍照成功后执行接下来的裁剪。 onActivityResult()方法中还有从相册选择图片、裁剪成功后返回执行的操作,我就一起贴出来了。

 


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        //用户没有进行有效的操作,返回
        if (requestCode == RESULT_CANCELED) {
            Toast.makeText(getApplication(), "取消", Toast.LENGTH_LONG).show();
            return;
        }
        switch (requestCode) {
            case FROM_GALLERY:
                if (resultCode == RESULT_OK) {
                    if (Build.VERSION.SDK_INT >= 19) {
                        //4.4以上系统使用
                        handleImageOnKitKat(data);
                    } else {
                        handleImageBeforeKitKat(data);
                    }
                }
                break;
            case TAKE_PHOTO://   裁剪照片
                    cropRawPhoto(imageUri);         
                break;
            case RESULT_REQUEST_CODE: 
                if (cropImgUri !=null) {
                    try {
                        Bitmap headImage = BitmapFactory.decodeStream(getContentResolver().openInputStream(cropImgUri));
                        headImageButton.setImageBitmap(headImage);

                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }else {
                    Toast.makeText(this,"cropImgUri为空!",Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }

public void cropRawPhoto(Uri uri) {
//创建file文件,用于存储剪裁后的照片
File cropImage = new File(Environment.getExternalStorageDirectory(), "crop_image.jpg");
String path = cropImage.getAbsolutePath();
try {
if (cropImage.exists()) {
cropImage.delete();
}
cropImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
cropImgUri = Uri.fromFile(cropImage);
Intent intent = new Intent("com.android.camera.action.CROP");
//设置源地址uri
intent.setDataAndType(imageUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 200);
intent.putExtra("outputY", 200);
intent.putExtra("scale", true);
//设置目的地址uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, cropImgUri);
//设置图片格式
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("return-data", false);
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent, RESULT_REQUEST_CODE);
}
 

   startActivityForResult(intent, RESULT_REQUEST_CODE);执行后,跳转到onActivityResult()中执行case RESULT_REQUEST_CODE:部分,代码已经贴出来了。就是调用BitmapFactory.decodeStream()方法将cropImage解析为bitmap对象。

  然后,开始运行。

  那么,问题来了(猜测是:由于裁剪后的图片保存到Cache里会耗费大量内存,Android是不允许你这样做的):

  这里有一篇一篇博文进行了解释:http://www.cnblogs.com/tianzhijiexian/p/4059006.html

 


 

  最开始,我是把相机拍下的照片和裁剪后的照片都存在关联应用缓存里,就是前面橙色部分代码的操作贴上的代码是我后来改正过的没问题的代码)。启动相机程序拍照并存储是正常的,图片也保存了。但是,裁剪之后的图片无法保存到Cache目录里,我沿着/sdcard/Android/data/<你的package name>/cache路径打开看了看,是0kb。

 

 

 

  本来最开始我就怀疑这个Cache存储可能会有问题,但是又想,拍下照片都可以好好保存为什么裁剪后的就不能保存呢?这不公平啊!于是乎,我着手改其他的我也怀疑的地方,在网上搜寻相关解答折腾很久还是解决不了。最后,我决定验证最后一个猜想:裁剪后的图片以某种诡异不明的方式,无法保存到Cache里面。说干就干:

  step1:把File路径换成普通的,也就是把File outputImage = new File(getExternalCacheDir(), "my_head_image.jpg")换成File outputImage = new File(Environment.getExternalStorageDirectory(), "my_head_image.jpg");同理更改File cropImage = new File(Environment.getExternalStorageDirectory(), "crop_image.jpg");

  step2:在manifest.xml中注册权限:<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>  

   step3:进行到这里,我运行了一次,还是异常,应该还是权限问题没有处理完,我在onCreate()方法里添加了这样一段:

StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
        StrictMode.setVmPolicy(builder.build());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            builder.detectFileUriExposure();
        }

 

   再运行,It works!


 

文末唠叨

  关于StrictMode我就不细讲了(心累+懒)。

  你懂得,在网上找解决bug的方法需要技巧、运气、时间,兜了一大圈,对于我来说,我在网上找solution八成都会花掉大堆时间,很多问题那都是别人遇到的麻烦和解决方法,对自己不一定适用,但是自己还是得作死的去多尝试,然后折腾一下午或者一晚上,心想着还不如在这个时间里换种方式浪费生命,比如看剧、睡觉和朋友闲聊、以及吃……

  对于安卓开发来说,太久之前的solution可能并不适用于现在了,比如现在越来越严格的权限问题。

  有时候,在网上瞎找,不如好好看书,搞清楚到底是怎么一个流程,哪里会出错,反而会更快一些解决问题,也更有收获。

  总而言之,要高效率解决问题,还是得清楚整个代码的流程。

  好了,现在我要换种方式浪费生命了……

 

 

 

 

以上是关于安卓开发——拍照裁剪并保存为头像报错:裁剪图片无法保存的的主要内容,如果未能解决你的问题,请参考以下文章

android 怎么裁剪drawable

音乐平台项目的几个问题总结(头像裁剪以及跨页面播放音乐)

Android开发技巧——定制仿微信图片裁剪控件

基于vue-cropper的上传裁剪后的头像

调用 android 系统拍照结合 android-crop 裁剪图片

android 怎么裁剪drawable