参考资料
我也想过直接整,但是还是先基于MediaStore 把增删改查写了,因为他本质上是不难的技术,只是说不能经常写,所以比较生疏,从业务场景上讲,查的的场景大于写的场景,而且查询就直接涉及到性能问题,所以通过图片等查询,可以很方便的打好查询的基础。这次的代码直接看PictureSelector 这是一个高星库,还是可以从里面学习到很多东西的。但是我们主要是看图片查询相关的代码。
正文
这个库的实现了蛮多功能的,比如说:
- 分页查询
- 目录列表
- 可选的git
- 视频和图片一起查询
- 一次性查询等
- 指定查询目录,这个是指APP位于外置卡android/data/包名 下的目录,所以通过file 获取MIME type
- 降序/升序
那么我们一点点基于功能去拆解这个的库的实现思路。
如何实现查询全部和分页查询?
scss
mLoader = selectorConfig.isPageStrategy
? new LocalMediaPageLoader(getAppContext(), selectorConfig)
: new LocalMediaLoader(getAppContext(), selectorConfig);
可以看到,分为了两个类。LocalMediaLoader 作为查询全部的loader,LocalMediaPageLoader作为分页查询的loader。
如何做到[(图片、音频、视频一起查询?),(可选gif),(指定目录)]
这个就是技术方案的选择了,那么我们基于LocalMediaLoader,去查找如何实现的。基于上一篇的mediaStore,可以发现他图片查询和视频查询的URL是两个,但是mideaStore 支持查询文件,而恰好我们知道图片和文件的MIME type ,我们再次回顾下query()的入参:
Uri uri
:这是查询请求的URI。它标识着要查询的数据,并且是唯一标识符。比如,如果你正在查询联系人,那么你可能使用ContactsContract.Contacts.CONTENT_URI
作为你的URI。String[] projection
:这是一个字符串数组,表示你希望查询哪些列。如果你不指定任何列,那么会返回所有的列。如果你想返回特定的列,你可以指定这些列的名称。String selection
:这是一个可选参数,表示查询中的筛选条件。这与SQL语句中的WHERE子句类似。如果你不提供这个参数,那么所有的记录都将被返回。String[] selectionArgs
:这是与selection
参数一起使用的参数。它是一个字符串数组,可以替换selection
中的占位符。String sortOrder
:这是可选参数,表示返回记录的排序方式。这与SQL语句中的ORDER BY子句类似。如果你不提供这个参数,那么记录的排序方式将由ContentProvider决定。
那么我们就可以写一个基于文件的条件查询,同样的gif 文件也有MIME type 那就是 image/gif,最后是目录,我们知道有目录参数,所以我们子需要判断包含关系即可。
查询所有不包括GIF
- selection: (media_type=? AND (mime_type!='image/gif') OR media_type=? AND 0 <= duration and duration <= 9223372036854775807) AND 0 <= _size and _size <= 9223372036854775807
- selectionArgs:["1","3"]
查询图片不包括gif
- selection: media_type=? AND (mime_type!='image/gif') AND 0 <= _size and _size <= 9223372036854775807
- selectionArgs: ["1"]
查询视频不包括gif
- selection:media_type=? AND 0 <= duration and duration <= 9223372036854775807
- selectionArgs:["3"]
查询音频不包括gif
- selection : media_type=? AND 0 <= duration and duration <= 9223372036854775807
- selectionArgs:["2"]
查询指定目录与查询目录
我们结合query() 可查询到字段,发现有这两个字段。
json
"bucket_id": "这是包含项目的存储桶的ID。",
"bucket_display_name": "这是包含项目的存储桶的显示名称。",
结合的查询拼接,我们可以直接在selection中和selectionArgs 中添加筛选条件,比如我们想要查询 Pictures 中的图片:
- selection:media_type=? AND bucket_display_name = ?
- selectionArgs:["1","Pictures"]
遇到的问题,我在模拟器上查询到的uri 只有等于 MediaStore.Files.getContentUri("external") 才没有崩溃,奔溃的原因大致为:
- 这个URI查询到的数据里面没有这个字段。
但是,通过代码可以发现这个库使用的是bucket_id 作为查询参数。所以文件或者图片所在目录就是bucket_display_name这个字段。筛选就基于这个处理 bucket_id 处理。
总结
通过上面的参数可以看到,这个组件对于文件类型的判断通过media_type进行了区分。同时约束了文件大小duration。通过源码:
ini
public interface FileColumns extends MediaColumns {
String MEDIA_TYPE = "media_type";
int MEDIA_TYPE_AUDIO = 2;
int MEDIA_TYPE_DOCUMENT = 6;
int MEDIA_TYPE_IMAGE = 1;
int MEDIA_TYPE_NONE = 0;
int MEDIA_TYPE_PLAYLIST = 4;
int MEDIA_TYPE_SUBTITLE = 5;
int MEDIA_TYPE_VIDEO = 3;
String MIME_TYPE = "mime_type";
String PARENT = "parent";
}
我们可以看到,FileColumns定义了很多种类的。AND (mime_type!='image/gif')
表示mime_type 不等于gif,所以这个也是排除gif的语句。可以看到再全部模式下,media从逻辑上是筛选不出来音频的。
例如:图片的URI就没有这个字段,就会导致SQL 拼接的时候发生错误。
如何做到降序或升序?
答案依旧再query() 的入参里面,那就是sortOrder。分别对应下面两个值:
- MediaStore.MediaColumns.DATE_MODIFIED + " DESC"
- MediaStore.MediaColumns.DATE_MODIFIED + " ASC"
DATE_MODIFIED作为文件的最后修改时间。
在SQL中,ASC是ascending的缩写,表示升序排列,即从小到大排序。例如,在查询语句中使用"ORDER BY column_name ASC"将按照指定列(column_name)的升序进行排序。
DESC是descending的缩写,表示降序排列,即从大到小排序。例如,在查询语句中使用"ORDER BY column_name DESC"将按照指定列(column_name)的降序进行排序。
如何通过Query() 查询所有相册目录
在LocalMediaLoader未分页的查询模式中,通过query() 查询数据的同时就把相册目录查询出来了。在分页模式下,也没有感觉有什么特殊的点,有点懵。
如何通过bucket_id 查询到相册的首张图作为封面
分页查询和全部查询到在通过bucket_id 查询第一张图片都是类似的,通过源码可以看到在这个时候查询到字段只有:
arduino
new String[]{
MediaStore.Files.FileColumns._ID,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.DATA}
但是查询的入参发生了变更,在Android R(30) 及其以上的版本,构建了一个Bundle,而在其他版本则还是通过selection、selectionArgs、sortOrder这3个参数提供筛选。
bundle 作为参数查询
通过下列函数提供了一个bundle 对象
arduino
public static Bundle createQueryArgsBundle(String selection, String[] selectionArgs, int limitCount, int offset, String orderBy) {
Bundle queryArgs = new Bundle();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, orderBy);
if (SdkVersionUtils.isR()) {
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_LIMIT, limitCount + " offset " + offset);
}
}
return queryArgs;
}
调用:
scss
data = getContext().getContentResolver().query(QUERY_URI, new String[]{
MediaStore.Files.FileColumns._ID,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.DATA}, queryArgs, null);
总结
可以看到,bundle 也是基于selection、selectionArgs、sortOrder作为参数,只是说封装了下,这里的查询条件和上面的查询拼接是一个道理。当获取到Cursor后直接读取第一项即可,但是对于返回封面图片的地址还是有系统版本上的差异的。
当系统版本大于等于29(Android Q)的时候。传入了id 和mimeType,返回了一个URI
ini
public static String getRealPathUri(long id, String mimeType) {
Uri contentUri;
if (PictureMimeType.isHasImage(mimeType)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if (PictureMimeType.isHasVideo(mimeType)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if (PictureMimeType.isHasAudio(mimeType)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
} else {
contentUri = MediaStore.Files.getContentUri("external");
}
return ContentUris.withAppendedId(contentUri, id).toString();
}
其他情况返回的是_data。可以看到这里使用了一个ContentUris.withAppendedId(contentUri, id) 获取URI,同时进行了系统版本的判断,不同的版本采用了不同的策略。
query() 查询文件时查询了哪些字段
我们知道当 projection 为空的时候,查询的是所有的字段。而且不同的Android 版本字段有些差异。同时不同的URI查询到的数据的字段也有差异。 所以这个库使用:MediaStore.Files.getContentUri("external") 查询文件。
这两个字段的区别在于COUNT,但是没有看明白有啥用。
查询全部字段
arduino
protected static final String[] PROJECTION = {
MediaStore.Files.FileColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
COLUMN_DURATION,
MediaStore.MediaColumns.SIZE,
COLUMN_BUCKET_DISPLAY_NAME,
MediaStore.MediaColumns.DISPLAY_NAME,
COLUMN_BUCKET_ID,
MediaStore.MediaColumns.DATE_ADDED,
COLUMN_ORIENTATION};
分页查询字段
arduino
protected static final String[] ALL_PROJECTION = {
MediaStore.Files.FileColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
COLUMN_DURATION,
MediaStore.MediaColumns.SIZE,
COLUMN_BUCKET_DISPLAY_NAME,
MediaStore.MediaColumns.DISPLAY_NAME,
COLUMN_BUCKET_ID,
MediaStore.MediaColumns.DATE_ADDED,
COLUMN_ORIENTATION,
"COUNT(*) AS " + COLUMN_COUNT};
总结
整体整下来,还是可以学到不少的东西的。比如:
- Android 不同版本的查询写法。
- 不同的URI 查询出来的字段是不一致的,不同的系统版本也不一致,所以选择字段得慎重。
- query() 条件查询,排序等
- 通过id 转URI等
当然了,还有很多细碎的知识点,就不罗列了。当然了,还有一个坑,分页没有描述,但是熟悉sqlite做分页的都知道,这玩意简单,就是条件查询。所以就没有描述了。 这个讲道理,从应用层的角度上来说,写到这里已经写无可写了。我们知道MediaStore 支持查询文件,而且PictureSelector 是基于文件查询做的图片查询。 那么,我们写任何一个文件查询都可以参考他的思路,无非就是mime type 不一样而已。