图片选择器在手机应用中屡见不鲜,设置头像、聊天传图等常见类似场景都需要使用。为了保持不同设备上体验的一致性和较好的兼容性,比较稳妥的做法是在应用内自实现相机拍照、相册选图和图片裁剪功能。但是,这个实现过程比较复杂,费时费力。更多时候,或者说在项目初期,我们都会选择直接调用系统提供的这些功能来完成一个图片选择器。然而,由于安卓设备的多样性,总会遇到各种各样的兼容问题。本文就来总结总结,调用系统相机、相册和裁剪功能实现图片选择器的过程中,我们需要注意的一些地方。
示例代码
这里简单使用一个示例代码,演示调用系统相机或相册,获取图片,然后使用系统裁剪功能处理图片,并显示到一个 ImageButton 视图里面:
|
|
效果如图(不同设备,系统功能呈现有所不同):
看似完美,你以为上述代码就能结束了的话,那就大错特错啦!这里面还有一些兼容问题要处理,还有一些地方需要特殊说明。
拍照图片存储问题
调用系统相机实现拍照功能的核心代码如下:
|
|
其中 MediaStore.EXTRA_OUTPUT
数据表示,拍照所得图片保存到指定目录下的文件(一般会在 SD 卡中创建当前应用的目录,并创建临时文件保存图片)。然后,在 onActivityResult 方法中根据文件路径获取图片。
如果不为 intent 添加该数据的话,将在 onActivityResult 的 intent 对象中返回一个 Bitmap 对象,通过如下代码获取:
|
|
值得注意的是,这里的 Bitmap 对象是拍照所得图片的一个缩略图,尺寸很小!系统这么做也是充分考虑到应用的内存占用问题。试想一下,如今手机设备中高清相机拍出来的照片,一张图的大小高达十几兆,如果返回这么大的图片,内存占用相当严重,何况很多时候知识临时使用而已。所以,调用系统相机时,一般都会添加 MediaStore.EXTRA_OUTPUT
参数,避免返回 Bitmap 对象。当然,这么做也能保证应用产生的数据,包括文件,都能存储在应用目录下,方便清理缓存时统一清除。
拍照图片旋转问题
部分手机,比如三星手机,调用系统相机拍照所得的照片可能会发生自动旋转问题,常见为旋转 90°。所以,要求我们在拍照之后,使用图片之前,判断图片是否发生过旋转,如果是,要将照片旋转回来。
这是获取图片旋转角度的代码:
|
|
这是根据指定角度旋转图片的代码:
|
|
横竖屏切换问题
在部分手机,调用系统拍照功能时,可能会发生横竖屏切换过程,导致返回应用时当前 Activity 发生销毁重建,各个生命周期又重新走了一遍。此时,一些应用内的变量数据可能丢失,使用时容易发生空值异常,进而导致 app 崩溃退出。
为了避免这种现象,我们需要在 AndroidManifest.xml 文件的对应 <activity>
标签中添加属性:
|
|
这样,当发生屏幕旋转时,不会导致 Activity 销毁重建,而是执行 onConfigurationChanged()
方法:
|
|
调用系统相册 type 问题
调用系统相册选择图片时,如果为 Intent 设置 “image/*” 类型的 type 的话,如:
|
|
当你选择完图片返回到当前应用并解析 Content Uri 为 File Uri 时,在部分机型(如小米手机)上,getContentResolver().query()
方法会返回 null 值,导致出现崩溃问题。这一点,需要额外注意一下。
调用系统裁剪问题
示例中调用系统裁剪的代码如下:
|
|
可以看出,调用系统裁剪功能,需要设置一些 Extra 参数,很多人容易在这里产生疑惑,不知如何取舍,如何设值。这里列举一下常用的 Extra 名字、值类型和作用:
- crop:String 类型数据,发送裁剪信号
- aspectX 和 aspectY:int 类型数据,设置裁剪框的 X 与 Y 值比例
- outputX 和 outputY:int 类型数据,设置裁剪输出的图片大小
- scale:boolean 类型数据,设置是否支持裁剪缩放
- return-data:boolean 类型数据,设置是否在 onActivityResult 方法的 intent 值中返回 Bitmap 对象
- MediaStore.EXTRA_OUTPUT:Uri 类型数据,设置是否将裁剪结果保存到指定文件中
需要注意的是:
第一,设置 return-data 参数为 true 时,返回的 Bitmap 对象也为缩略图,获取方式与前面所述相机拍照获取 Bitmap 的方式一致;
第二,调用系统相册并裁剪时,如果使用MediaStore.EXTRA_OUTPUT参数,Uri 尽量不要设置为源文件对应的 Uri 值,另做保存,不损坏系统相册中的源图文件;
第三,根据经验,outputX 与 outputY 值设置太大时,容易出现卡屏现象;
第四,可以不设置 outputX 与 outputY 参数,使用户根据自身按比例自由裁剪,就像示例代码这样。
setImageURI() 注意事项
你可能会用到 setImageURI()
方法给 ImageView 设置图片内容,这里也有一个地方需要注意。我们先看一下这个方法的源码:
|
|
可以看到,这里的 uri 参数在内部持有缓存变量,当多次调用该方法而 uri 参数值不变时,图片展示内容不变。问题就在这,如果你多次拍照或裁剪保存的图片文件路径相同时,虽然每次处理过后实际存储的文件内容发生变化,但由于路径相同,uri 参数一致,导致多次调用 setImageURI()
设置图片内容时,ImageView 显示内容不变!这也是为什么示例代码中我用时间戳处理图片文件名的原因所在,保证每次存储的图片路径不同。
根据 Uri 获取文件地址
有时候,我们需要根据 Uri 获取文件路径。比如如果你不需要使用裁剪功能的话,调用系统相册选择图片后返回的就是一个 Uri 对象,我们需要从这个 Uri 对象中解析出对应的图片文件路径,便于上传至服务器等后续处理。
比如,这个 Uri 对象可能是:
content://media/external/images/media/3066
很多朋友相信有过这样的经验,使用 toString() 或者 getPath() 方法获取 Uri 对象所对应的文件路径,其实这是错误的!通过 getPath() 获取的结果字符串是:
media/external/images/media/3066
而正确的获取方式是:
|
|
其对应的文件路径应该是这个样子的:
/storage/emulated/0/Pictures/Screenshots/S70302-131606.jpg
注意:通过调用系统相册选择图片的方式返回的是一个内容 URI(content:),可以通过上述代码获取文件地址;而使用 MediaStore.EXTRA_OUTPUT 参数为指定拍照所得文件的保存地址返回的 URI 则是一个文件 URI(file:),不能通过上述代码获取文件地址,直接使用 getPath() 方法即可,如:
|
|
关于 ContentResolver 相关介绍,可以参考官方文档:内容提供程序基础知识。
Base64 文件编码处理
现在很多网络框架内部都做了封装处理,上传图片时只需要传递一个文件路径即可。但是,少数情况下,根据服务器需要,我们要对图片文件字节流编码后再上传。这是使用 Base64 编码并根据字节数组获取字符串的处理过程:
|
|
zip 压缩文件处理
当上传多张图片至服务器时,为了提升传输效率,往往会采用 zip 格式压缩处理。这里提供一个递归压缩代码,方便大家有需要的时候借鉴参考:
|
|
添加系统权限
说了这么多,别忘了在 AndroidManifest.xml 文件中添加系统权限(前面示例代码中没有考虑到 Android 6.0 运行时权限的问题,实际使用时注意添加处理):
|
|
拓展文章: Android 7.0 系统进一步限制应用的文件访问权限,更多细节请访问我的另一篇文章:
关于 Android 7.0 适配中 FileProvider 部分的总结
参考链接:关于调用系统相册的知识点,你还可以访问这篇文章:
How to get Picasa images using the Image Picker on Android devices running any OS version