在做学校大创项目的安卓开发时,需要从相册获取图片或者拍照,然后裁剪保存为头像。由于我是第一次弄安卓开发,也对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中进行注册:
<providerandroid:name="android.support.v4.content.FileProvider"android:authorities="com.example.write.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid: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="/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", pressFormat.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是不允许你这样做的):
这里有一篇一篇博文进行了解释:/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可能并不适用于现在了,比如现在越来越严格的权限问题。
有时候,在网上瞎找,不如好好看书,搞清楚到底是怎么一个流程,哪里会出错,反而会更快一些解决问题,也更有收获。
总而言之,要高效率解决问题,还是得清楚整个代码的流程。
好了,现在我要换种方式浪费生命了……