概述
Android上图片涉及到的要点:
- 基础
- 自定义相机拍照
- 调用系统相机拍照
- 图片选择
- 裁剪
- 压缩
- 上传
- 服务端处理与下载
- 显示与内存-普通图与超大图
- 文件夹管理
- 内置图片管理与包大小
基础
-
exif :使用开源库Android-Exif-Extended或者support包里的ExifInterface. 建议前者,内部时直接读文件头,没有api的限制.
Android-Exif-Extended示例:
public native static boolean nativeCompress(Bitmap bitmap,int quality,String outPath);
public static boolean compressOringinal(String srcPath,int quality,String outPath){
Bitmap bitmap = BitmapFactory.decodeFile(srcPath);
boolean success = nativeCompress(bitmap,quality,outPath);//libjpeg压缩
if(success){
ExifInterface exif = new ExifInterface();
try {
exif.readExif( srcPath, ExifInterface.Options.OPTION_ALL );
exif.writeExif( outPath );//全部exif信息拷到新文件那儿去
} catch (IOException e) {
e.printStackTrace();
}finally {
return true;
}
}
return success;
}
复制代码
自定义相机拍照
camera api是Android兼容性问题最大的地方,没有之一.所以,不要作死去用原生的,或者自己从头开始写一套. 直接用开源的.
当然,原生api的使用还是要掌握的,有几个比较好的博客供参考:
Android相机开发系列:很全面
Android多媒体之Camera的相关操作:相当于一个概述
注意点:
- preview和takepicture两套不同的操作,不同的supportedSize. size的选择需要与预览的surfaceview相配合,避免拉伸.
- 前置摄像头的takepicture的自拍镜像问题
- 图像矩阵横屏90°.方向旋转可以用纯java数组转换,也可以用opencv 的api,或者renderscript,或者利用bitmap中转,性能最好的是renderscript.
- 最终图像质量:如果对图片质量(清晰度,亮度)有所要求,可以使用preview的api来采集多帧,取评价最高的一帧作为最终的结果. 评价标准方面,可以基于opencv开发一套对清晰度和亮度判断的算法. 更牛的就是多帧合成,顶级实践就是谷歌,华为,小米的超级夜景算法.
调用系统相机拍照
注意兼容性:
- 构建intent时指定文件保存位置,避免有些机型拿不到默认存储位置的返回
- Uri 在7.0以上系统的兼容
- 8.0以上系统,即使是调用系统相机拍照,也需要请求权限,否则crash
标准的intent
Intent takeIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri photoUri = getMediaFileUri(TYPE_TAKE_PHOTO);
takeIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(takeIntent, CODE_TAKE_PHOTO);
复制代码
Uri兼容性
intent读写权限都加上,不管任何版本:
调用系统拍照,Intent需要加上读和写权限. 如果没有写权限,部分4.4以下手机会crash:
```
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
或者这种实现:
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
```
复制代码
7.0以上兼容性的实现
抄官方demo谁都会,但后果就是导致容易跟第三方库内部实现冲突. 这里的重点在于与其他已内部实现Uri兼容性的第三方库的兼容:
//官方demo:原封不动搬过来,就可能会有跟第三方库冲突.因为别人一般也是原封不动搬过来的
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
//正确示范:
<provider
android:name="org.devio.takephoto.permission.TFileProvider"//name要自己继承v4里的fileprovider
android:authorities="${applicationId}.takephoto.fileprovider"//自己附加字符串,后面要提供解析方法
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths_takephoto" />//记得加后缀.尤其作为库提供时,防止被同名覆盖.
</provider>
//xml里:
<paths>
<!--外置SD卡-->
<root-path
path=""
name="camera_photos" />
//如果是库,一般只提供上面一个就够了,下面的由app自己的module去提供.
<!--Context.getFilesDir()-->
<files-path
name="files"
path=""/>
<!--Context.getCacheDir()-->
<cache-path
name="cache"
path=""/>
<!--Environment.getExternalStorageDirectory()-->
<external-path
name="external"
path=""/>
<!--Context.getExternalFilesDir(null)-->
<external-files-path
name="external_file_path"
path=""/>
<!--Context.getExternalCacheDir()--> <!--直到support-v4:25.0.0才支持-->
<external-cache-path
name="external_cache_path"
path=""/>
</paths>
//然后提供解析这个uri的方式(sd卡):
public static String parseOwnUri(Context context, Uri uri) {
if (uri == null) {
return null;
}
String path;
if (TextUtils.equals(uri.getAuthority(), context.getPackageName() + ".takephoto.fileprovider") {
path = new File(uri.getPath().replace("camera_photos/", "")).getAbsolutePath();
} else {
path = uri.getPath();
}
return path;
}
复制代码
规避
本质上还是因为从7.0开始强制执行sctrickmode,也可以写代码让它不执行:只是不建议这样做罢了.
/过滤URI检查
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
复制代码
图片选择
可以调用系统intent去图库里选一张图,但是不同手机的图库选择千差万别,返回的uri依据手机厂商和版本的不同而情况不同,有的是contentprovider形式,有的是file uri形式,Android 7.0以上以下还不一致. 另外,系统intent不支持多选. 搞来搞去,不如自己查media center数据库,自己做ui. 基本上成熟的app都是通过这种形式实现,网上开源库也一大把.
图片裁剪
有系统intent,但是不同手机千差万别,大多数很差劲.果断用开源的. 开源库中,ucrop秒杀其他裁剪库: github.com/Yalantis/uC…
当然,也仍有不足之处: 选择区域后裁剪该区域,不是基于原图来裁,而是基于界面上预览图来裁剪,所以会自带压缩效果.
图片压缩
- 尺寸压缩与质量压缩:
对于手机图片来说,80%质量和100%质量并无肉眼可见的明显区别,而文件大小相差近一倍.完全没有必要设为100%的质量,而如果是相机拍摄的原图而非进行尺寸压缩后的图,70%的质量已经足够.
所以,一般来说: 经过尺寸压缩的图,可设置质量80%. 原图大小,可将质量设置为70%. 两者都可用下面的libjpeg来压缩.
- libjpeg-turbo库在Android上的压缩优化
Bitmap.compress->skia->libjpeg->file,file->libjpeg->skia->bitmap 所有的问题都可以从这里找到答案.
- 同样分辨率的图片,Android图片文件比ios大: Android上skia库调用libjpeg时使用的是霍夫曼定长编码而非动态的霍夫曼编码.
- 多次压缩的图片发绿问题: Android skia库的bug: RGB转YUV时是int转float操作,google工程师为了加快运算速度在进行了位移操作,会导致细微精度丢失,多次压缩,解压再压缩操作会导致图片渐渐发绿. 参考
- inSampleSize 只接受2的n次幂: 因为libjpeg解压时就只支持这样的传入
参考:www.voidcn.com/article/p-w…
彩蛋: 自己写的图片压缩工具app
- 模仿微信压缩效果的库: Luban
拍照/图片选择-图片裁剪-压缩 的整合
结合app业务的二次封装技巧: 透明fragment接收onactivityResult,达到最终静态方法+回调的一行代码调用的效果:
TakePhotoUtil.startPickOne(fragmentActivity, isForCamera, new TakeOnePhotoListener() {
@Override
public void onSuccess(String usableFilePath) {
showImg(usableFilePath);
}
@Override
public void onFail(String filePath, String msg) {
MyToast.errorBigL(msg);
//toTakePhotoMode();
}
@Override
public void onCancel() {
// toTakePhotoMode();
}
});
复制代码
ps. 透明fragment的封装技巧参考: RxPermissionsFragment
图片上传
- 控制图片源,尽量小
- 多图并发,结合rxjava
- 顶层封装,api友好
服务端处理与下载
- CDN缓存
- 提供服务端处理能力:resize,裁剪,模糊.七牛和阿里云的图片存储服务均提供了此类功能.客户端通过url后添加参数,获得比原图小的图片,节省流量.
示例: 78re52.com1.z0.glb.clouddn.com/resource/go…
代码示例:QiniuUtils
客户端显示
-
普通图片: fresco/glide. 一般是自己封装一层,便于切换,以及做一些全局性的处理. ImageLoader
-
对fresco/glide/picasso的优缺点的讨论:mPaaS 3.0 多媒体组件发布 | 支付宝百亿级图片组件 XMedia 锤炼之路(图片缓存篇)
-
大图: 分区域解码 subsampling-scale-image-view
文件夹管理
- 项目中提供统一的获取方法,删除方法
- cache使用的注意事项:存储不足时会被系统优先清空
内置图片管理与包大小
- tinypng/tinyjpg先压缩一遍
- 用不到透明通道的png统一转换成jpg
- 大一些的图,可以放到服务端,首次进入app时下载到files文件夹.