2017年2月份,笔者为了一个项目搞了几天的相册,项目比较急,所以应付了事。昨天突然想起要把这个坑填上,所以“重操旧业”吧。
说到相册,我们首先拥有打开系统文件的权限,其次要获取系统相册中的图片。众所周知,要获取一张图片资源,你没有该图片的“资源路径”是万万行不通的,
在Google Android 4.4 [KitKat] 之前路径返回的相册路径[真实的路径]是这个【content://media/external/images/media/53470】;当Google Android 4.4 [KitKat] 出来
的时候,一种新的相册路径[姑且称为假的路径]是这个[content://com.android.providers.media.documents/document/image%3A48974] ,也就是说Google 推出的原
生Android路径是两种。
然而中国作为全世界最大的Android 手机厂商国家,基于Android 开放的源代码 ,定制属于自己的一套Android系统 ,为了凸显各自手机系统的优点和特性,加大
市场的竞争优势,一份由Google 给出的 Android 源代码 被改的“面目全非” 。当然,这种现象在互联网发展的历史上又不是没有出现过。比如:OSI [开放系统互联] 当时在
Internet发展早期,致力余研究一种标准协议,以用来屏蔽不同硬件厂商的硬件之间存在差异,提出了分层概念模型和接口的概念,当时TCP/IP算是“义军”突起,强势以
“方便用户[硬件厂商]”的宗旨,抢占市场大量份额,导致OSI这个真正应用于Internet,在这里我们不讨论OSI协议和TCP/IP协议的异同。
由此我们可知道,真正符合用户需求,便利用户才能在当今互联网+物联网时代拔得头筹。
下面我们说说中国各大Android手机厂商之间Android 系统相册返回的图片路径:
Google Android 4.4 之前:content://media/external/images/media/53470
Google Android 4.4 时:content://com.android.providers.media.documents/document/image%3A48974
诺基亚6 Android 7.0 [图库]: content://media/external/images/media/3781
诺基亚6 Android 7.0 [文件管理]:content://com.fihtdc.filemanager.provider/shared_files/ storage/emulated/0/%E8%A1%A8%E6%83%85%E7%8E%8B%E5%9B%BD/
%E6%89%98%E9%A9%AC%E6%96%AF%E5%B0%8F%E7%81%AB%E8%BD%A6/%E6%89%98%E9%A9%AC%E6%96%
AF%E5% B0%8F%E7%81%AB%E8%BD%A6/ 006uBje5ly1fd3z5c0pb3j306e064q2z_1.jpg
小米 3 :file:///storage/emulated/0/DCIM/Camera/IMG_20160325_233243.jpg
小米 5 Android 7.0 :content://com.miui.gallery.open/raw/%2Fstorage%2Femulated%2F0%2FDCIM%2FCamera%2FIMG_20170707_161721.jpg
content://com.android.fileexplorer.fileprovider/external_files/%E8%A1%A8%E6%83%85%E7%8E%8B%E5%9B%BD/e78319cc
7f0000010128350da8d32327.gif
其他的就不一一列举了,相信路径还是存在差异的,针对不同的路径,我们需要写不同的方法来解析相对相应的路径以获取图片资源。你想一下,
系统相册给你的解析方法就那几个,真的能解析到每一个返回的图片路径吗?不说不同牌子的手机系统存在其他的差异,我只能说这是很不现实的。
我们能不能有一种好的解决方法,统一处理这些存在差异的路径,然后提供一个方法,直接获取图片资源。显然这个想法以来于系统相册是行不通的,
我能想到的是既然系统的相册行不通,我就自己写一个 自定义相册库 ,全局搜索文件系统内的图片[这里的全局搜索文件其实Android系统已经帮我们
做了,其实当你启动Android的时候,手机内部会启动想用的程序帮你检查你手机内部内部的文件,然后以数据库表格的形式存储这些数据信息,包括资源
路径,当然这里面的肯定会有优化算法的,并不是每一次启动Android手机都会全局扫描系统文件吧,不然会严重影响用户体验,这个我们是可以证明的:
当你为一台新手机重装系统,第一回启动手机是,是不是觉得特别慢,更新系统重启是不是是是不是特别慢?想想就知道Android手机系统在进行一些初始
化操作,其中文件检索和数据收集肯定会操作的,只是隐藏掉没有给用户提示罢了]。既然全局搜索文件的工作系统已经帮我们做了,我们就可以直接跳过这
一步骤,我们需要需要做的就是统一图片路径,就像设计模式一书中讲到的适配器模式一样【你把最原始的数据给我,经过适配器处理后,给你想要的数据】。
稍加整理,【搞成SDK,这样就可以在各个项目中用了】。
这样处理之后,这种由系统相册导致图片路径的差异就直接被我们屏蔽掉了。仔细想一想,上面的描述是不是想和有分层概念模型和接口的影子呢?
那么问题来了?自定义相册库行不行的通呢?我记得以前和一位实验室的同学在讨论,QQ和微信是怎么处理的相册,那么神奇,几乎适配所有的Android手机,
直到昨天和一位大神聊天,自己又长知识了。QQ和微信告诉我们,这是行的通的,而且QQ和微信也是这么做的。
1:我们先不谈“自定义相册库”,我们看看网上那类雷同的文章,没有突出问题的实质,只是阐述了问题的表象。直接上代码
public class AlbumUtil {
/**
* 获取文件路径
*/
@TargetApi(19)
public static String getFilePath(Context context, Uri uri ,Intent intent){
boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0] ;
if ("primary".equalsIgnoreCase(type) == true) {
return "${Environment.getExternalStorageDirectory()}/${split[1]}" ;
}
} else if (isDownloadsDocument(uri)) {
//DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri) ;
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null) ;
} else if (isMediaDocument(uri)) {
//MediaProvider
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image" == type) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video" == type) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio" == type) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?" ;
final String[] selectionArgs = new String[] { split[1] };
return getDataColumn(context, contentUri, selection, selectionArgs) ;
}// MediaProvider
// DownloadsProvider
} else if ("content".equalsIgnoreCase(uri.getScheme()) == true) {
// MediaStore (and general)
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
return getDataColumn(context, uri, null, null);
} else if ("file".equalsIgnoreCase(uri.getScheme()) == true) {
// File
return uri.getPath() ;
}else if (DocumentsContract.isDocumentUri(context, uri) == false){
getImagePath(context,uri,intent);
}
return null ;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
* @param context The context.
* *
* @param uri The Uri to query.
* *
* @param selection (Optional) Filter used in the query.
* *
* @param selectionArgs (Optional) Selection arguments used in the query.
* *
* @return The value of the _data column, which is typically a file path.
*/
private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs){
Cursor cursor = null ;
final String column = "_data";
final String[] projection = { column };
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
if (cursor == null){
return uri.getPath();
}
} finally {
if (cursor != null)
cursor.close();
return null ;
}
//return null ;
}
/**
* @param uri The Uri to check.
* *
* @return Whether the Uri authority is ExternalStorageProvider.
*/
private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* *
* @return Whether the Uri authority is DownloadsProvider.
*/
private static boolean isDownloadsDocument(Uri uri){
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* *
* @return Whether the Uri authority is MediaProvider.
*/
private static boolean isMediaDocument(Uri uri){
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
//content://com.android.providers.media.documents/document/image%3A48974
/**
* @param uri The Uri to check.
* *
* @return Whether the Uri authority is Google Photos.
*/
private static boolean isGooglePhotosUri(Uri uri){
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
/**
* 获取图片路径
*/
public static String getImagePath(Context context, Uri uri, Intent data){
Uri selectedImage = data.getData();
//Log.e(TAG, selectedImage.toString());
if (selectedImage != null) {
String uriStr = selectedImage.toString();
String path = uriStr.substring(10, uriStr.length());
if (path.startsWith("com.sec.android.gallery3d")) {
Log.e("Method selectImage", "It's auto backup pic path:" + selectedImage.toString());
return null ;
}
}
final String[] filePathColumn = { MediaStore.Images.Media.DATA };
Cursor cursor = context.getContentResolver().query(selectedImage, filePathColumn, null, null, null);
String mImgPath ;
if (cursor != null){
cursor.moveToFirst();
int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
String picturePath = cursor.getString(columnIndex);
mImgPath = picturePath;
cursor.close();
}else
{
mImgPath = selectedImage.getPath();
}
return mImgPath;
}
}
//使用intent调用系统提供的相册功能,使用startActivityForResult是为了获取用户选择的图片
//只打开图库
//Intent getAlbum = new Intent(Intent.ACTION_PICK,android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
// getAlbum.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,"image/*");
// startActivityForResult(getAlbum, IMAGE_CODE);
//打开了图库、图片、最近图片
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);//ACTION_OPEN_DOCUMENT
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/jpeg");
if(android.os.Build.VERSION.SDK_INT>=android.os.Build.VERSION_CODES.KITKAT){
startActivityForResult(intent, SELECT_PIC_KITKAT);
}else{
startActivityForResult(intent, IMAGE_CODE);
}
这里的SELECT_PIC_KITKAT和IMAGE_CODE值可以随便取,他们的作痛主要是在onActivityResult(int requestCode ,int resultCode,Intent intent)方法
里面区分到底调用哪个方法来通过Uri uri 来获得 期望“统一”不存在差异的图片路径资源 ,可惜针对的主体是系统相册,不是自定义的相册库,也只能“哭了”。
protected void onActivityResult(int requestCode, int resultCode, Intent data){
//此处的 RESULT_OK 是系统自定义得一个常量,-1表示获取数据成功
if (resultCode == -1){
Bitmap bitmap = null;
//外界的程序访问ContentProvider所提供数据 可以通过ContentResolver接口
ContentResolver resolver = getContentResolver();
//此处的用于判断接收的Activity是不是你想要的那个,IMAGE_CODE为自定义的
if (requestCode == IMAGE_CODE || requestCode==SELECT_PIC_KITKAT) {
Uri uri = data.getData();
System.out.println("phone's path:"+uri.toString());
String path = "" ;
if (requestCode == IMAGE_CODE) {
path = SDCardDataPickUtil.getFilePath(AlbumActivity.this, uri, data);
}
else if (requestCode==SELECT_PIC_KITKAT) {
path = SDCardDataPickUtil.getFilePath(AlbumActivity.this, uri, data);
if (path == null || "".equals(path))
path = SDCardDataPickUtil.getImagePath(AlbumActivity.this, uri, data);
}
mPath = path ;
editor.putString("path",path);
editor.commit();
imgPath.setText(path);
imgShow.setImageBitmap(loadFromSdCard(path));
}
}else{
return;
}
}
上面这几段段代码,相信你在网上看到太多次了,导致不知道原版是谁[我听说是有人从国外翻译+自己的理解]?我个人观点是第一个吃螃蟹的人别人会夸他勇气可嘉,
第二个乃至以后吃螃蟹的人就不会有人这么夸了,除非有人提出一些独特见解,才有可能收到部分人的夸奖。
2、我们谈一谈 自定义相册库 , 大家可参考这篇文章,我99%还是出自这篇文章 。Android 自定义本地图片加载库,仿微信相册
下面是真片文章作者的思路:
总结一下微信的本地图片加载有以下几个特点,也是提高用户体验的关键点
1、缩略图挨个加载,一个一个加载完毕,直到屏幕所有缩略图都加载完成
2、不等当前屏的所有缩略图加载完,迅速向下滑,滑动停止时立即加载停止页面的图片
3、已经加载成功的缩略图,不管滑出去多远,滑回来的时候不需要重新加载
4、在相册以外的环境中,需要让imageView的宽高比例随图片的宽高比例自动伸缩,而且要在图片加载完毕之前就要预留占位空间
为了满足上面几个要求,主要采用以下几个方法:
0、为了防止图片加载出来OOM,需要对分辨率和颜色的位数进行缩小到合适范围,同时采用LRU [操作胸痛中一种页面置换算法:最近最久为使用] 缓存
1、采用一个定长线程池,线程池的大小等于CPU的数量+1,把所有缩略图加载任务都交给线程池执行,以获得最快的加载效率。
2、在用户快速滑动的时候,没有加载完毕的划走了的图片立即停止加载,将所占线程让出来,让新的加载任务执行。
3、已经加载成功的缩略图,保存到sd卡中,下次再滑动回来的时候,直接从sd卡加载以前保存好的小图,不经过线程池。
4、对于三星这样的手机,其图片全都是宽度大于高度,方向用exif进行记录,图片加载器要读出exif的方向信息,然后通过矩阵进行旋转
代码我就不着办过来了,大家有兴趣可以点击“Android 自定义本地图片相册库,仿微信
这是该文章在Github上的源代码链接:https://github.com/AlexZhuo/AlxImageLoader
最后笔者在重点说一下:
既然我们的程序要和相同文件程序进行数据交互,Android给我们提供了一个强大的组件 “内容提供器” ,不了解的读者可以取百度,这里笔者只做简要的概述。
内容提供器(Content Provider)主要用于在不同应用程序之间实现数据共享的功能,他提供那个了一套完整的机制,允许一个程序访问另外一个程序中的数据,
同时还能保证被访问数据的安全性。
用法有两种:1、使用现有的内容给提供器来读取和操作相应程序中的数据【Android自带的内容提供器:电话簿、短信、媒体、日历等程序】
2、创建自己的内容提供器给我们的应用程序的数据提供外部访问接口
具体的介绍请看官网:https://developer.android.com/guide/topics/providers/content-providers.html 【应该需要翻墙吧】
读者需要了解的是:ContentResolver 的基本用法[【https://developer.android.com/reference/android/content/ContentResolver.html】 和ContentResolver自带的增删改查方法,笔者认为这篇文章对ContenTResolver的查询方法介绍比较详细 Android 学习笔记 Contacts (一)ContentResolver query 参数详解
如果读者对 projection 【投影】词语感到困惑的话,可以取看下数据库相关的数据,比如【数据库系统概论 第五版】,书中对这个词语有非常详细的描述。
下面是 自定义相册库 最重要的代码:
/**
* 从系统相册里面取出图片的uri
*/
public static void get500PhotoFromLocalStorage(final Context context, final LookUpPhotosCallback completeCallback) {
new AlxMultiTask<Void,Void,ArrayList<SelectPhotoEntity>>(){
@Override
protected ArrayList<SelectPhotoEntity> doInBackground(Void... params) {
ArrayList<SelectPhotoEntity> allPhotoArrayList = new ArrayList<>();
Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver mContentResolver = context.getContentResolver();//得到内容处理者实例
String sortOrder = MediaStore.Images.Media.DATE_MODIFIED + " desc";//设置拍摄日期为倒序
Log.i("Alex","准备查找图片");
// 只查询jpeg和png的图片
Cursor mCursor = mContentResolver.query(mImageUri, new String[]{MediaStore.Images.Media.DATA},
MediaStore.Images.Media.MIME_TYPE + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?",
new String[]{"image/jpeg", "image/png"}, sortOrder+" limit 500");
if (mCursor == null) return allPhotoArrayList;
int size = mCursor.getCount();
Log.i("Alex","查到的size是"+size);
if (size == 0) return allPhotoArrayList;
for (int i = 0; i < size; i++) {//遍历全部图片
mCursor.moveToPosition(i);
String path = mCursor.getString(0);// 获取图片的路径
SelectPhotoEntity entity = new SelectPhotoEntity();
entity.url = path;//将图片的uri放到对象里去
allPhotoArrayList.add(entity);
}
mCursor.close();
return allPhotoArrayList;
}
没错,这个方法是从 系统相册中 取出图片的URL,此时的URL格式是没有多大差异的。看到这里读者可能会有疑惑,这不是瞎扯嘛,请注意,这里的得到的路径是
真是的路径,都是这种路径【/storage/emulated/***/***/文件名.后缀名】,而手机系统自带相册最初资源的路径也是种格式,只不过是当当我们选中图片后
返回给我们的资源的路径不同,导致存在很大差异,就如文章刚开头提到的哪集中路径。所以我们 自定义相册库 的目的在意统一资源路径,说白了就是保持最原始的资
源路径[/storage/emulated] ,其实 自定义相册库 就相当余一个 中间件 ,确保存储区的资源通过原始路径能提供给用户[这里的用户指的是开发人员]使用,就不比再为
资源路径不统一的差异而苦恼了。