本文来源于实际项目遇到的需求。如果想要直接看源码(实际项目是java所写,但git上的demo是kotlin所写,毕竟android目标是将kotlin逐步替代java),访问:https://github.com/life2smile/PhotoAlbum.git。切记这只是个demo。
一、需求背景
需要扫描出系统中存在的视频及图片,并展示在宫格视图中,同时图片以其所在文件夹进行分组区分(demo中并未实现,可自行实现)。
二、目标
(1)实现基本的相册预览功能,包括视频及图片。
(2)相册按时间创建顺序就近排序即新创建的在前面展示。
(3)视频预览展示播放时长。
最重要的是:
优化扫描速度、优化扫描速度、优化扫描速度。。。
为什么强调优化扫描速度?文章后面会讲。
三、实现方案
需求的难点在于既要获取视频又要获取图片,图片的预览可以很快获取,但是视频预览相对要耗时些,所以二者存在着天然的时间差,这里采用两个线程任务来分别扫描图片及视频,最后先后合并到一个集合中,进行数据渲染。
所以,这里首先要有一个统一的数据结构。众所周知,android本身已经存储了相册预览的相关数据并通过ContentResolver暴露了查询接口,事实上这些数据有很多的公共性,比如创建时间、路径等,因此这里可以抽象出一个多媒体数据结构 MediaData来进行统一表示。除了这些共有字段外,还需要添加特定多媒体类型下的字段,比如视频的duration、相册的经纬度等,统一于MediaData。
下面逐步讨论各模块实现。
四、界面搭建
界面搭建的实现思路很简单,使用RecyclerView + GridLayoutManager布局即可。需要注意的地方是,我们想要的效果是各个宫格等分居中于屏幕,且大小一致。所以应该首先获取屏幕宽度,基于宫格的列数进行等分,获取到size就是每个宫格的高和宽。当然这个只是常见的默认宫格实现方案,有其他高宽定制需求的,按照自己需求定制即可。
五、图片扫描
首先,抽出一个图片扫描的工具类ImageScanHelper,具体完成的功能会在代码架构中阐述。
//kotlin中的单例写法(再也不用纠结懒加载、多线程下的java写法了)
//这里当然可以改成伴随对象,以实现和java static相匹配的方式。
object ImageScanHelper {
//start为对外暴露的扫描接口,在相册预览的activity中,触发该方法调用
//形如:ImageScanHelper.start(this.getApplicationContext(), handler)。
//第一个参数为context,第二个为handler,目的是拿到扫描数据后通知主线程进行ui更新。
fun start(context:Context, handler:Handler) {
//图片扫描相对比较耗时,这里单独开一个扫描线程
Thread{
doScan(context,handler)
}.start()
}
private fun doScan(context:Context, handler:Handler) {
//这里完成数据查询,查询结果可通过游标cursor拿到
cursor = context.contentResolver.query(...)
parseData(cursor,handler)
}
private fun parseData(cursor:Cursor, handler:Handler) {
//遍历数据,检出我们需要的数据,并通过加入到imageList中。
do {
imageList.add(MediaData(id,createTime, ...))
}while(cursor.moveToNext())
//通过handler将数据传递给ui主线程进行界面更新
val msg = Message()
msg.obj = imageList//这里的
msg.what =MediaType.MEDIA_TYPE_IMAGE
handler.sendMessage(msg)
}
六、视频扫描
前面说过,这个是个难点,原因在于视频缩略图的获取。android中有多种方案可以获取视频缩略图,如通过MediaMetadataRetriever获取视频第一帧、通过ThumbnailUtils获取第一帧等等。这些方案完全能获取到视频缩略图,but,这些有个很大的弊端,就是这些都是非常耗时的方案,用户从进到预览界面开始,到真正看到视频预览的效果需要很长时间,如果视频数目较小还能接受,反之就慢到令人发指了。所以这些方案实际上并不可取。
那么有没有更快的方案能获取到视频缩略图?当然有,那就是查询系统早就给我们保存好了的视频缩略图信息,这样就大大缩短了获取速度,但是这个方案依然存在弊端,那就是很多机型拿不到最新拍摄的视频缩略图,甚至有的机型除非重新启动手机,才能看到新拍摄的视频缩略图,这显然对用户来说也是不可接受的。
那么还有没有兼容性更好、扫描速度更快的手段获取视频缩略图?
有!那就是结合上述两种方案。具体阐述如下:
(1)查询手机已缓存的缩略图,如果有则保存地址
(2)对于没有缩略图的视频,人工生成缩略图并缓存。然后返回视频缩略图地址
实际上,对于没有缩略图的视频毕竟是少数,所以,上述方案很接近单纯扫描系统数据缓存的时间消耗。
代码结构描述如下:
//功能同图片扫描
fun start(context:Context, handler:Handler) {
Thread{
doScan(context,handler)
}.start()
}
//功能同图片扫描
private fun doScan(context:Context, handler:Handler) {
//这里先扫描视频数据
cursor = context.contentResolver.query(...)
}
//功能同图片扫描
private fun parseData(context:Context, cursor:Cursor, handler:Handler) {
do {
try {
//这里会根据拿到的视频数据,触发一次视频缩略图的扫描
thumbCursor = context.contentResolver.query(...)
//获取视频缩略图路径(可能为空),如果有的话直接获取,如果没有则生成缩略图
thumbNailPath = thumbNailPath.isNullOrEmpty().let {
//这里生成缩略图
generateThumbNail(filePath)
}
//添加扫描出来的视频及其缩略图数据
videoList.add(MediaData(id, createTime, duration, albumName, filePath, thumbNailPath, mimeType,null,null))
}
}while (cursor.moveToNext())
//发送消息至ui线程,携带有扫描的视频数据
val msg:Message =Message.obtain()
msg.obj = videoList
msg.what =MediaType.MEDIA_TYPE_VIDEO
handler.sendMessage(msg)
}
至此,扫描视频的代码逻辑完成。
七、数据合并
前面提到,图片的扫描速度远远快于视频扫描速度,所以二者存在时间差,但数据最终要合并到一起并渲染。
其实到这里已经很简单了,因为二者有共同的数据结构MediaData,在将一个类型的数据添加到adapter中后,调用notifyDataSetChanged()即可。
八、保证时间有序
这个也很简单,我们只需要在添加数据到adapter的时候对list进行排序即可。
对于java来说,只需要MediaData实现compareTo方法,即可调用Collections.sort进行排序。
对于kotlin来说,调用List.sort{}即可。
九、图片压缩
由于android对运行的应用有内存限制(具体参考我的另一篇博客https://www.jianshu.com/p/a06466971bff),所以在处理图片加载的时候要尤其注意,稍有不慎就有可能oom。常见的第三方图片加载库都有对图片进行过处理,这里由于我们采用的是原生控件,所以需要对图片进行处理。代码如下:
class ImageResizeUtil {
companion object {
fun resize(path: String, w: Int, h: Int): Bitmap {//根据传入的宽高进行图片裁剪
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options)
//获取缩放比例,主要在decode失败的时候options测量的宽高值是-1,要考虑这种情况进行处理
options.inSampleSize = Math.max(1, Math.ceil(Math.max(
options.outWidth / w, options.outHeight / h
).toDouble()).toInt())
options.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, options)
}
}
}
十、过滤
前面扫描图片和视频的过程中有可能产生一些脏数据或者不符合我们需要的数据,所以这里要对数据进行过滤。
很简单我们采用过滤器模式即可,首先抽象出一个过滤器接口:
//这里采用了泛型的设计,满足各种数据传入
interface IFilter<T> {
fun doFilter(t: T)
}
接着可以针对不同的类型实现过滤功能,比如过滤掉不符合大小的图片(这里仅仅列举个例子,具体可以参考git代码):
class ImageSizeFilter : IFilter<MutableList<MediaData>> {
override fun doFilter(list: MutableList<MediaData>) {
val iterator = list.iterator()//这里必须要采用迭代器删除,避免遍历的时候有数据改动引起异常
while (iterator.hasNext()) {
val mediaData: MediaData = iterator.next()
val options: BitmapFactory.Options = BitmapFactory.Options()
BitmapFactory.decodeFile(mediaData.filePath, options)
if (options.outWidth <= 50 || options.outHeight <= 50) {
iterator.remove()
}
}
}
}
十一、The End
最后,首尾呼应,源码地址见:https://github.com/life2smile/PhotoAlbum.git。再次强调,代码是基于kotlin写的,如果想要java版的,自己可以参照逻辑实现一遍,或者使用插件转换一下即可。