android快速获取系统中的图片和视频

本文来源于实际项目遇到的需求。如果想要直接看源码(实际项目是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版的,自己可以参照逻辑实现一遍,或者使用插件转换一下即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,085评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,714评论 2 59
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,979评论 3 119
  • 前文中提到的当初那篇长文。 静落的雪花铺陈了视野,一步步轻压出曾经来过的痕,方有人知,他的旅途。 初冬,初雪,他独...
    愚书愚己阅读 215评论 0 0
  • 感谢大家的喜欢与关注,我已经长久不在简书写作了,最新的自我提升、情感、读书等文章,有兴趣可以在豆瓣关注:艾莉森王,...
    艾莉森王阅读 1,993评论 0 4