我的问题已经解决,查阅了几十篇相关文章,换了数种解决方案,最终解决,虽然不是按照本文来的,但关闭窗口的时候发现本文算是系统,特此转载。
最近在自己的项目里实现了一个头像选择的功能,就是先从相册里选取一张图片再调用系统的裁剪功能来制作头像,效果就像下面这样:
本以为很小的一个功能,却远远没有我想的那样简单,可以说每一步都暗藏玄机,下面就让我带大家看看这里面究竟有哪些坑。
Android 4.4之存储访问框架
首先,让我们从图片选择开始,使用隐式Intent跳转到图片选择:
<code class="java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">routeToGallery</span><span class="hljs-params">()</span> </span>{</code>
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(“image/*”);
startActivityForResult(intent, GALLERY_REQUSET_CODE);
}
在回调中处理返回的图片,继而跳转至图片裁剪:
<code class="java"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onActivityResult</span><span class="hljs-params">(<span class="hljs-keyword">int</span> requestCode, <span class="hljs-keyword">int</span> resultCode, Intent data)</span> </span>{</code>
if (requestCode == GALLERY_REQUSET_CODE && resultCode == RESULT_OK) {
String path = data.getData().getPath();
Bitmap image = BitmapFactory.decodeFile(path);
File faceFile;
try {
faceFile = saveBitmap(image);
} catch (IOException e) {
e.printStackTrace();
return;
}
Uri fileUri = Uri.fromFile(faceFile);
routeToCrop(fileUri); //跳转到图片裁剪
}
}
private void routeToCrop(Uri uri) {
Intent intent = new Intent(“com.android.camera.action.CROP”);
intent.setDataAndType(uri, “image/*”);
intent.putExtra(“crop”, true);
intent.putExtra(“aspectX”, 1);
intent.putExtra(“aspectY”, 1);
intent.putExtra(“outputX”, 150);
intent.putExtra(“outputY”, 150);
intent.putExtra(“return-data”, true);
startActivityForResult(intent, CROP_REQUEST_CODE);
}
private File saveBitmap(Bitmap bitmap) throws IOException {
File file = new File(getExternalCacheDir(), “face-cache”);
if (!file.exists()) file.createNewFile();
try (OutputStream out = new FileOutputStream(file)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
}
return file;
}
这一段代码看似正常,但问题就出在String path = data.getData().getPath();
这一句。这一段代码在Android 4.4以下是可以正常运行的,不过从Android 4.4开始这里获取到的将为一个无效的路径,这是为什么呢?
Android从4.4开始引入了一个概念:存储访问框架,简单来说就是Android提供了一个专门供用户访问资源的软件,将设备上所有可以访问资源的软件接口都整合到了一起,避免了用户只能选择一个特定软件的尴尬,在Android 4.4以下,我们发送刚才选取图片的隐式Intent,效果是这样的,需要用户去选择使用哪个应用:
而从Android 4.4开始,就变成了这样:
直接打开一个资源选取的软件(这个软件平时是隐藏的,不会显示在软件列表中),其中包含了访问设备上所有可访问资源软件的接口,这个改变极大的提高的用户操作的便捷性。
不过这也带来了一个问题,从Android 4.4开始,在onActivityResult()
方法的Intent
中所包含的uri
不再是file://
类型,而是变成了content://
类型,这也是为什么在Android 4.4以后调用data.getData.getPath()
获取到的结果是无效的。因此,我们必须对Android 4.4以上的版本进行特殊的处理:
<code class="java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">routeToGallery</span><span class="hljs-params">()</span> </span>{</code>
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(“image/*”);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
startActivityForResult(intent, GALLERY_REQUSET_CODE_KITKAT);
} else {
startActivityForResult(intent, GALLERY_REQUSET_CODE);
}
}
在回调中对不同版本分别进行处理:
<code class="java"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onActivityResult</span><span class="hljs-params">(<span class="hljs-keyword">int</span> requestCode, <span class="hljs-keyword">int</span> resultCode, Intent data)</span> </span>{</code>
switch (requestCode) {
case GALLERY_REQUSET_CODE:
handleGalleryResult(resultCode, data);
break;
case GALLERY_REQUSET_CODE_KITKAT:
handleGalleryKitKatResult(resultCode, data);
break;
}
}
private void handleGalleryResult(int resultCode, Intent data) {
// 跟之前一样
}
// Result uri is “content://” after Android 4.4
private void handleGalleryKitKatResult(int resultCode, Intent data) {
File faceFile;
try {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(contentUri, “r”);
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
faceFile = saveBitmap(image);
} catch (IOException e) {
e.printStackTrace();
return;
}
Uri fileUri = Uri.fromFile(faceFile);
routeToCrop(fileUri);
}
Android 7.0之FileProvider
完成了图片的选择功能,转眼又碰到了一个问题:
Android为了提高私有文件的安全性,从7.0开始对外传递file://
类型的uri
会触发FileUriExposedException
。因此,在分享私有文件时必须使用FileProvider
。
对Android的这一改变还不太了解的同学可以看一下这两篇文章:Android 7.0 行为变更和Setting Up File Sharing。
第一步
在manifest
文件中加入FileProvider
:
<code class="xml"><span class="hljs-tag"><<span class="hljs-name">manifest</span> <span class="hljs-attr">xmlns:android</span>=<span class="hljs-string">"http://schemas.android.com/apk/res/android"</span></span></code>
package=“gavinli.translator”>
<application
…>
<provider
android:name=“android.support.v4.content.FileProvider”
android:authorities=“gavinli.translator”
android:grantUriPermissions=“true”
android:exported=“false”>
<meta-data
android:name=“android.support.FILE_PROVIDER_PATHS”
android:resource=“@xml/filepaths” />
</provider>
…
</application>
</manifest>
第二步
在xml
文件夹中创建filepaths.xml
文件,并声明所要分享的文件目录:
<code class="xml"><span class="hljs-tag"><<span class="hljs-name">resources</span>></span></code>
<paths>
<external-cache-path name=“mycache” path=“./” />
</paths>
</resources>
这里的path
就代表你想要分享的文件目录,而name
就是具体显示在uri
中的信息,最终生成的uri
就像下面这样:
这种经过处理的uri
可以很好的隐藏掉实际的文件路径。
第三步
在代码中对Android 7.0以上的版本进行特殊处理:
<code class="java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handleGalleryKitKatResult</span><span class="hljs-params">(<span class="hljs-keyword">int</span> resultCode, Intent data)</span> </span>{</code>
…
Uri fileUri;
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Android 7.0 “file://” uri权限适配
fileUri = FileProvider.getUriForFile(this,
“gavinli.translator”, faceFile);
} else {
fileUri = Uri.fromFile(faceFile);
}
routeToCrop(fileUri);
}
这里传入的"gavinli.translator"
,需要与之前在manifest
文件中声明的android:authorities
一致。
第四步
在裁剪图片的Intent
中加入对该图片的访问权限:
<code class="java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">routeToCrop</span><span class="hljs-params">(Uri uri)</span> </span>{</code>
Intent intent = new Intent(“com.android.camera.action.CROP”);
intent.setDataAndType(uri, “image/*”);
// 加入访问权限
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
…
}
最后一步
在回调中获取裁剪后的图片:
<code class="java"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onActivityResult</span><span class="hljs-params">(<span class="hljs-keyword">int</span> requestCode, <span class="hljs-keyword">int</span> resultCode, Intent data)</span> </span>{</code>
switch (requestCode) {
…
case CROP_REQUEST_CODE:
Bundle bundle = data.getExtras();
Bitmap face = bundle.getParcelable(“data”);
break;
}
}
Intent的限制
你以为到这里就结束了吗?其实还远远没有。我们这里裁剪的图片是用作头像的,所以大小一般都比较小。可以当图片的大小变大后就会发现,每次裁剪后在Intent
中获取到的图片其实都是缩略图。
这是因为Android对Intent
中所包含数据的大小是有限制的,一般不能超过1M,否则应用就会崩溃,这就是Intent
中的图片数据只能是缩略图的原因。而解决的办法也很简单,我们需要给图片裁剪应用指定一个输出文件,用来存放裁剪后的图片:
<code class="cpp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">routeToCrop</span><span class="hljs-params">(Uri uri)</span> </span>{</code>
…
intent.putExtra(“return-data”, false);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
new File(getExternalCacheDir(), “face-cropped”)));
startActivityForResult(intent, CROP_REQUEST_CODE);
}
现在,在回调中的图片就不能再直接从Intent
中获取了,而是需要先拿到Intent
中的uri
,再使用uri
进行获取,具体的过程和之前处理uri
的方式一样,这里就不再赘述了。当然,直接从之前指定的文件中读取数据也是可以的。
Android 6.0之运行时权限
不知道大家发现了没有,之前保存图片的目录都是使用的Context.getExternalCacheDir()
,这个方法获取到的目录为/sdcard/Android/data/gavinli.translator/cache
,是应用专属的外部存储空间,不需要声明权限。而要想使用公共的存储空间,就势必要面对一个问题:Android 6.0的运行时权限。
首先,在manifest
文件中声明读取外置存储的权限:
<code class="xml"><span class="hljs-tag"><<span class="hljs-name">manifest</span> <span class="hljs-attr">xmlns:android</span>=<span class="hljs-string">"http://schemas.android.com/apk/res/android"</span></span></code>
package=“gavinli.translator”>
<uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE”/>
…
</manifest>
之后,在代码中加入运行时的权限申请:
<code class="java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">request</span><span class="hljs-params">()</span> </span>{</code>
String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
if(ContextCompat.checkSelfPermission(this, permisson)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(permissions, REQUEST_CODE);
} else {
// 存储图片
}
}
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
if(requestCode == REQUEST_CODE) {
if(grantResults[i] == PackageManager.PERMISSION_GRANTED) {
// 存储图片
}
}
}
后记
到这里,这一次的踩坑之旅就全部结束了,我们也看到了Android这几个版本以来一步步对权限的限制,虽然这对我们的开发产生一定的影响,但只要能提高用户的使用体验,这点困难又算的了什么呢?