本文來源于實(shí)際項(xiàng)目遇到的需求。如果想要直接看源碼(實(shí)際項(xiàng)目是java所寫淋叶,但git上的demo是kotlin所寫胚吁,畢竟android目標(biāo)是將kotlin逐步替代java),訪問:https://github.com/life2smile/PhotoAlbum.git恋腕。切記這只是個(gè)demo。
一逆瑞、需求背景
需要掃描出系統(tǒng)中存在的視頻及圖片荠藤,并展示在宮格視圖中,同時(shí)圖片以其所在文件夾進(jìn)行分組區(qū)分(demo中并未實(shí)現(xiàn)获高,可自行實(shí)現(xiàn))哈肖。
二、目標(biāo)
(1)實(shí)現(xiàn)基本的相冊預(yù)覽功能念秧,包括視頻及圖片淤井。
(2)相冊按時(shí)間創(chuàng)建順序就近排序即新創(chuàng)建的在前面展示。
(3)視頻預(yù)覽展示播放時(shí)長摊趾。
最重要的是:
優(yōu)化掃描速度币狠、優(yōu)化掃描速度、優(yōu)化掃描速度砾层。漩绵。。
為什么強(qiáng)調(diào)優(yōu)化掃描速度肛炮?文章后面會(huì)講止吐。
三宝踪、實(shí)現(xiàn)方案
需求的難點(diǎn)在于既要獲取視頻又要獲取圖片,圖片的預(yù)覽可以很快獲取碍扔,但是視頻預(yù)覽相對要耗時(shí)些瘩燥,所以二者存在著天然的時(shí)間差,這里采用兩個(gè)線程任務(wù)來分別掃描圖片及視頻不同,最后先后合并到一個(gè)集合中厉膀,進(jìn)行數(shù)據(jù)渲染。
所以套鹅,這里首先要有一個(gè)統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)站蝠。眾所周知,android本身已經(jīng)存儲(chǔ)了相冊預(yù)覽的相關(guān)數(shù)據(jù)并通過ContentResolver暴露了查詢接口卓鹿,事實(shí)上這些數(shù)據(jù)有很多的公共性菱魔,比如創(chuàng)建時(shí)間、路徑等吟孙,因此這里可以抽象出一個(gè)多媒體數(shù)據(jù)結(jié)構(gòu) MediaData來進(jìn)行統(tǒng)一表示澜倦。除了這些共有字段外,還需要添加特定多媒體類型下的字段杰妓,比如視頻的duration藻治、相冊的經(jīng)緯度等,統(tǒng)一于MediaData巷挥。
下面逐步討論各模塊實(shí)現(xiàn)桩卵。
四、界面搭建
界面搭建的實(shí)現(xiàn)思路很簡單倍宾,使用RecyclerView + GridLayoutManager布局即可雏节。需要注意的地方是,我們想要的效果是各個(gè)宮格等分居中于屏幕高职,且大小一致钩乍。所以應(yīng)該首先獲取屏幕寬度,基于宮格的列數(shù)進(jìn)行等分怔锌,獲取到size就是每個(gè)宮格的高和寬寥粹。當(dāng)然這個(gè)只是常見的默認(rèn)宮格實(shí)現(xiàn)方案,有其他高寬定制需求的埃元,按照自己需求定制即可涝涤。
五、圖片掃描
首先亚情,抽出一個(gè)圖片掃描的工具類ImageScanHelper妄痪,具體完成的功能會(huì)在代碼架構(gòu)中闡述。
//kotlin中的單例寫法(再也不用糾結(jié)懶加載楞件、多線程下的java寫法了)
//這里當(dāng)然可以改成伴隨對象衫生,以實(shí)現(xiàn)和java static相匹配的方式。
object ImageScanHelper {
//start為對外暴露的掃描接口土浸,在相冊預(yù)覽的activity中罪针,觸發(fā)該方法調(diào)用
//形如:ImageScanHelper.start(this.getApplicationContext(), handler)。
//第一個(gè)參數(shù)為context黄伊,第二個(gè)為handler泪酱,目的是拿到掃描數(shù)據(jù)后通知主線程進(jìn)行ui更新。
fun start(context:Context, handler:Handler) {
//圖片掃描相對比較耗時(shí)还最,這里單獨(dú)開一個(gè)掃描線程
Thread{
doScan(context,handler)
}.start()
}
private fun doScan(context:Context, handler:Handler) {
//這里完成數(shù)據(jù)查詢墓阀,查詢結(jié)果可通過游標(biāo)cursor拿到
cursor = context.contentResolver.query(...)
parseData(cursor,handler)
}
private fun parseData(cursor:Cursor, handler:Handler) {
//遍歷數(shù)據(jù)拓轻,檢出我們需要的數(shù)據(jù)斯撮,并通過加入到imageList中。
do {
imageList.add(MediaData(id,createTime, ...))
}while(cursor.moveToNext())
//通過handler將數(shù)據(jù)傳遞給ui主線程進(jìn)行界面更新
val msg = Message()
msg.obj = imageList//這里的
msg.what =MediaType.MEDIA_TYPE_IMAGE
handler.sendMessage(msg)
}
六扶叉、視頻掃描
前面說過勿锅,這個(gè)是個(gè)難點(diǎn),原因在于視頻縮略圖的獲取枣氧。android中有多種方案可以獲取視頻縮略圖溢十,如通過MediaMetadataRetriever獲取視頻第一幀、通過ThumbnailUtils獲取第一幀等等达吞。這些方案完全能獲取到視頻縮略圖张弛,but,這些有個(gè)很大的弊端酪劫,就是這些都是非常耗時(shí)的方案吞鸭,用戶從進(jìn)到預(yù)覽界面開始,到真正看到視頻預(yù)覽的效果需要很長時(shí)間契耿,如果視頻數(shù)目較小還能接受瞒大,反之就慢到令人發(fā)指了。所以這些方案實(shí)際上并不可取搪桂。
那么有沒有更快的方案能獲取到視頻縮略圖透敌?當(dāng)然有,那就是查詢系統(tǒng)早就給我們保存好了的視頻縮略圖信息踢械,這樣就大大縮短了獲取速度酗电,但是這個(gè)方案依然存在弊端,那就是很多機(jī)型拿不到最新拍攝的視頻縮略圖内列,甚至有的機(jī)型除非重新啟動(dòng)手機(jī)撵术,才能看到新拍攝的視頻縮略圖,這顯然對用戶來說也是不可接受的话瞧。
那么還有沒有兼容性更好嫩与、掃描速度更快的手段獲取視頻縮略圖寝姿?
有!那就是結(jié)合上述兩種方案划滋。具體闡述如下:
(1)查詢手機(jī)已緩存的縮略圖饵筑,如果有則保存地址
(2)對于沒有縮略圖的視頻,人工生成縮略圖并緩存处坪。然后返回視頻縮略圖地址
實(shí)際上根资,對于沒有縮略圖的視頻畢竟是少數(shù),所以同窘,上述方案很接近單純掃描系統(tǒng)數(shù)據(jù)緩存的時(shí)間消耗玄帕。
代碼結(jié)構(gòu)描述如下:
//功能同圖片掃描
fun start(context:Context, handler:Handler) {
Thread{
doScan(context,handler)
}.start()
}
//功能同圖片掃描
private fun doScan(context:Context, handler:Handler) {
//這里先掃描視頻數(shù)據(jù)
cursor = context.contentResolver.query(...)
}
//功能同圖片掃描
private fun parseData(context:Context, cursor:Cursor, handler:Handler) {
do {
try {
//這里會(huì)根據(jù)拿到的視頻數(shù)據(jù),觸發(fā)一次視頻縮略圖的掃描
thumbCursor = context.contentResolver.query(...)
//獲取視頻縮略圖路徑(可能為空)想邦,如果有的話直接獲取裤纹,如果沒有則生成縮略圖
thumbNailPath = thumbNailPath.isNullOrEmpty().let {
//這里生成縮略圖
generateThumbNail(filePath)
}
//添加掃描出來的視頻及其縮略圖數(shù)據(jù)
videoList.add(MediaData(id, createTime, duration, albumName, filePath, thumbNailPath, mimeType,null,null))
}
}while (cursor.moveToNext())
//發(fā)送消息至ui線程,攜帶有掃描的視頻數(shù)據(jù)
val msg:Message =Message.obtain()
msg.obj = videoList
msg.what =MediaType.MEDIA_TYPE_VIDEO
handler.sendMessage(msg)
}
至此案狠,掃描視頻的代碼邏輯完成服傍。
七、數(shù)據(jù)合并
前面提到骂铁,圖片的掃描速度遠(yuǎn)遠(yuǎn)快于視頻掃描速度吹零,所以二者存在時(shí)間差,但數(shù)據(jù)最終要合并到一起并渲染拉庵。
其實(shí)到這里已經(jīng)很簡單了灿椅,因?yàn)槎哂泄餐臄?shù)據(jù)結(jié)構(gòu)MediaData,在將一個(gè)類型的數(shù)據(jù)添加到adapter中后,調(diào)用notifyDataSetChanged()即可钞支。
八茫蛹、保證時(shí)間有序
這個(gè)也很簡單,我們只需要在添加數(shù)據(jù)到adapter的時(shí)候?qū)ist進(jìn)行排序即可烁挟。
對于java來說婴洼,只需要MediaData實(shí)現(xiàn)compareTo方法,即可調(diào)用Collections.sort進(jìn)行排序撼嗓。
對于kotlin來說柬采,調(diào)用List.sort{}即可。
九且警、圖片壓縮
由于android對運(yùn)行的應(yīng)用有內(nèi)存限制(具體參考我的另一篇博客http://www.reibang.com/p/a06466971bff)粉捻,所以在處理圖片加載的時(shí)候要尤其注意,稍有不慎就有可能oom斑芜。常見的第三方圖片加載庫都有對圖片進(jìn)行過處理肩刃,這里由于我們采用的是原生控件,所以需要對圖片進(jìn)行處理。代碼如下:
class ImageResizeUtil {
companion object {
fun resize(path: String, w: Int, h: Int): Bitmap {//根據(jù)傳入的寬高進(jìn)行圖片裁剪
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options)
//獲取縮放比例盈包,主要在decode失敗的時(shí)候options測量的寬高值是-1沸呐,要考慮這種情況進(jìn)行處理
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)
}
}
}
十、過濾
前面掃描圖片和視頻的過程中有可能產(chǎn)生一些臟數(shù)據(jù)或者不符合我們需要的數(shù)據(jù)续语,所以這里要對數(shù)據(jù)進(jìn)行過濾垂谢。
很簡單我們采用過濾器模式即可厦画,首先抽象出一個(gè)過濾器接口:
//這里采用了泛型的設(shè)計(jì)疮茄,滿足各種數(shù)據(jù)傳入
interface IFilter<T> {
fun doFilter(t: T)
}
接著可以針對不同的類型實(shí)現(xiàn)過濾功能,比如過濾掉不符合大小的圖片(這里僅僅列舉個(gè)例子根暑,具體可以參考git代碼):
class ImageSizeFilter : IFilter<MutableList<MediaData>> {
override fun doFilter(list: MutableList<MediaData>) {
val iterator = list.iterator()//這里必須要采用迭代器刪除力试,避免遍歷的時(shí)候有數(shù)據(jù)改動(dòng)引起異常
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
最后排嫌,首尾呼應(yīng)畸裳,源碼地址見:https://github.com/life2smile/PhotoAlbum.git。再次強(qiáng)調(diào)淳地,代碼是基于kotlin寫的怖糊,如果想要java版的,自己可以參照邏輯實(shí)現(xiàn)一遍颇象,或者使用插件轉(zhuǎn)換一下即可伍伤。