一、序
我之前發(fā)布了個(gè)圖片加載框架都办,在JCenter關(guān)閉后嫡锌,“閉關(guān)修煉”,想著改好了出個(gè)2.0版本琳钉。
后來覺得僅增加功能和改進(jìn)實(shí)現(xiàn)不夠势木,得補(bǔ)充一下用例。
相冊列表的加載就是很好的用例歌懒,然后在Github找了一圈啦桌,沒有找到滿意的,有的甚至好幾年沒維護(hù)了,于是就自己寫了一個(gè)甫男。
相比于圖片加載且改,相冊加載在Github上要多很多。
圖片加載的input/output比較規(guī)范板驳,不涉及UI布局又跛;
而相冊則不然,幾乎每個(gè)APP都會有自己獨(dú)特的需求若治,有自己的UI風(fēng)格慨蓝。
因此,相冊庫很難做到通用于大部分APP端幼。
我所實(shí)現(xiàn)的這個(gè)也一樣礼烈,并非以實(shí)現(xiàn)通用的相冊組件為目的,而是作為一個(gè)樣例婆跑,以供參考此熬。
二、 需求描述
網(wǎng)上不少相冊的開源庫洽蛀,都是照微信相冊來搭的界面摹迷,我也是跟著這么做吧,要是說涉及侵權(quán)什么的郊供,那些前輩應(yīng)該先比我收到通知……
主要是自己也不會UI設(shè)計(jì)峡碉,不找個(gè)參照對象怕實(shí)現(xiàn)的太難看。
話說回來驮审,要是真的涉及侵權(quán)鲫寄,請聯(lián)系我處理。
相冊所要實(shí)現(xiàn)的功能疯淫,概括來說地来,就是顯示相冊列表,點(diǎn)擊縮略圖選中熙掺,點(diǎn)擊完成結(jié)束選擇未斑,返回選擇結(jié)果。
需求細(xì)節(jié)币绩,包括但不限于以下列表:
- 實(shí)現(xiàn)目錄列表蜡秽,相冊列表,預(yù)覽頁面缆镣;
- 支持單選/多選芽突;
- 支持顯示選擇順序和限定選擇數(shù)量;
- 支持自定義篩選條件董瞻;
- 支持自定義目錄排序寞蚌;
- 支持“原圖”選項(xiàng);
- 支持再次進(jìn)入相冊時(shí)傳入已經(jīng)選中的圖片/視頻;
- 支持切換出APP外拍照或刪除照片后挟秤,回到相冊時(shí)自動刷新壹哺;
效果如圖:
三、API設(shè)計(jì)
由于不同的頁面可能需求不一樣煞聪,所以可以將需求參數(shù)封裝到”Request“中斗躏;
對于通用的選項(xiàng),以及相冊組件的全局配置昔脯,可以更封裝到“Config"中啄糙。
而Request/Config最好是用鏈?zhǔn)紸PI去設(shè)置參數(shù),鏈?zhǔn)紸PI尤其適合參數(shù)是“可選項(xiàng)”的場景云稚。
3.1 全局設(shè)置
EasyAlbum.config()
.setImageLoader(GlideImageLoader)
.setDefaultFolderComparator { o1, o2 -> o1.name.compareTo(o2.name)}
GlideImageLoader是相冊組件定義的ImageLoader接口的實(shí)現(xiàn)類隧饼。
public interface ImageLoader {
void loadPreview(MediaData data, ImageView imageView, boolean asBitmap);
void loadThumbnail(MediaData data, ImageView imageView, boolean asBitmap);
}
不同的APP使用的圖片加載框架不一樣,所以相冊組件最好不要強(qiáng)依賴圖片加載框架静陈,而是暴露接口給調(diào)用者燕雁。
當(dāng)然,對于整個(gè)APP而言鲸拥,不建議定義這樣的ImageLoader類拐格,因?yàn)檎麄€(gè)APP使用圖片加載的地方很多,
要定義這樣類的話刑赶,要么重載很多方法捏浊,要么就是參數(shù)列表很長,也就喪失了鏈?zhǔn)紸PI的優(yōu)點(diǎn)撞叨。
關(guān)于目錄排序金踪,EasyAlbum中定義的默認(rèn)排序是按照更新時(shí)間(取最新的圖片的更新時(shí)間)排序。
上面例子中舉例的是按目錄名排序牵敷。
如果需要某個(gè)目錄(比方說‘Camera’)胡岔,排在“圖片和視頻”之后,可以這樣定義:
private val priorityFolderComparator = Comparator<Folder> { o1, o2 ->
val priorityFolder = "Camera"
if (o1.name == priorityFolder) -1
else if (o2.name == priorityFolder) 1
else o1.name.compareTo(o2.name)
}
出個(gè)思考題:
如果需要“優(yōu)先排序”的不只一個(gè)目錄枷餐,比如希望“Camera"第一優(yōu)先靶瘸,"Screenshots"第二優(yōu)先,“Pictures"第三優(yōu)先……
改如何定義Comparator毛肋?
3.2 啟動相冊
EasyAlbum啟動相冊以from起頭奕锌,以start結(jié)束。
EasyAlbum.from(this)
.setFilter(TestMediaFilter(option))
.setSelectedLimit(selectLimit)
.setOverLimitCallback(overLimitCallback)
.setSelectedList(mediaAdapter?.getData())
.setAllString(option.text)
.enableOriginal()
.start { result ->
mediaAdapter?.setData(result.selectedList)
}
具體到實(shí)現(xiàn)村生,就是from返回 Request, Request的start方法啟動相冊頁(AlbumActivity)。
public class EasyAlbum {
public static AlbumRequest from(@NonNull Context context) {
return new AlbumRequest(context);
}
}
public final class AlbumRequest {
private WeakReference<Context> contextRef;
AlbumRequest(Context context) {
this.contextRef = new WeakReference<>(context);
}
// ...其他參數(shù)..
public void start(ResultCallback callback) {
Session.init(this, callback, selectedList);
if (contextRef != null) {
Context context = contextRef.get();
if (context != null) {
context.startActivity(new Intent(context, AlbumActivity.class));
}
contextRef = null;
}
}
}
啟動AlbumActivity饼丘,就涉及傳參和結(jié)果返回趁桃。
有兩種思路:
- 通過intent傳參數(shù)到AlbumActivity, 用startActivityForResult啟動,通過onActivityResult接收。
- 通過靜態(tài)變量傳遞參數(shù)卫病,通過Callback回調(diào)結(jié)果油啤。
第一種方法,需要所有的參數(shù)都能放入Intent, 基礎(chǔ)數(shù)據(jù)可以傳蟀苛,自定義數(shù)據(jù)類可以實(shí)現(xiàn)Parcelable,
但那對于接口的實(shí)現(xiàn)益咬,就沒辦法放 intent 了,到頭來還是要走靜態(tài)變量帜平。
因此幽告,干脆就都走靜態(tài)變量傳遞好了。
這個(gè)方案可行的前提是裆甩, AlbumActivity是封閉的冗锁,不會在跳轉(zhuǎn)其他Activity。
在這個(gè)前提下嗤栓,App不會同一個(gè)時(shí)刻打開多個(gè)AlbumActivity冻河,不需要擔(dān)心共享變量相互干擾的情況。
然后就是茉帅,在Activity結(jié)束時(shí)叨叙,做好清理工作。
可以將“啟動相冊-選擇圖片-結(jié)束相冊”抽象為一次“Session”, 在相冊結(jié)束時(shí)堪澎,執(zhí)行一下clear操作擂错。
final class Session {
static AlbumRequest request;
static AlbumResult result;
private static ResultCallback resultCallback;
static void init(AlbumRequest req, ResultCallback callback, List<MediaData> selectedList) {
request = req;
resultCallback = callback;
result = new AlbumResult();
if (selectedList != null) {
result.selectedList.addAll(selectedList);
}
}
static void clear() {
if (request != null) {
request.clear();
request = null;
resultCallback = null;
result = null;
}
}
}
四、媒體文件加載
媒體文件加載似乎很簡答全封,就調(diào)ContentResolver query一下的事马昙,但要做到盡量完備,需要考慮的細(xì)節(jié)還是不少的刹悴。
4.1 MediaStore API
查詢媒體數(shù)據(jù)庫行楞,需走ContentResolver的qurey方法:
public final Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder,
CancellationSignal cancellationSignal) {
}
媒體數(shù)據(jù)庫記錄了各種媒體類型,要過濾其中的“圖片”和“視頻”土匀,有兩種方法:
1子房、用SDK定義好的MediaStore.Video和MediaStore.Images的Uri。
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2就轧、直接讀取"content://external", 通過MEDIA_TYPE字段過濾证杭。
private static final Uri CONTENT_URI = MediaStore.Files.getContentUri("external");
private static final String TYPE_SELECTION = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
+ " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
+ ")";
如果需要同時(shí)讀取圖片和視頻,第2種方法更省事一些妒御。
至于查詢的字段解愤,視需求而定。
以下是比較常見的字段:
private static final String[] PROJECTIONS = new String[]{
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.Video.Media.DURATION,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
MediaStore.Images.Media.ORIENTATION
};
DURATION, SIZE, WIDTH, HEIGHT乎莉,ORIENTATION等字段有可能是無效的(0或者null),
如果是無效的送讲,可以去從文件本身獲取奸笤,但讀文件比較耗時(shí),
所以可以先嘗試從MediaStore讀取哼鬓,畢竟是都訪問到這條記錄了监右,從空間局部原理來說便斥,讀取這些字段是順便的事情捧请,代價(jià)要比另外讀文件本身低很多桃熄。
當(dāng)然腾誉,如果確實(shí)不需要這些信息畴栖,可以直接不讀取胀莹。
4.2 數(shù)據(jù)包裝
數(shù)據(jù)查詢出來荒澡,需要定義Entity來包裝數(shù)據(jù)橡淆。
public final class MediaData implements Comparable<MediaData> {
private static final String BASE_VIDEO_URI = "content://media/external/video/media/";
private static final String BASE_IMAGE_URI = "content://media/external/images/media/";
static final byte ROTATE_UNKNOWN = -1;
static final byte ROTATE_NO = 0;
static final byte ROTATE_YES = 1;
public final boolean isVideo;
public final int mediaId;
public final String parent;
public final String name;
public final long modifiedTime; // in seconds
public String mime;
long fileSize;
int duration;
int width;
int height;
byte rotate = ROTATE_UNKNOWN;
public String getPath() {
return parent + name;
}
public Uri getUri() {
String baseUri = isVideo ? BASE_VIDEO_URI : BASE_IMAGE_URI;
return Uri.parse(baseUri + mediaId);
}
public int getRealWidth() {
if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
fillData();
}
return rotate != ROTATE_YES ? width : height;
}
public int getRealHeight() {
if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
fillData();
}
return rotate != ROTATE_YES ? height : width;
}
// ......
}
4.2.1 數(shù)據(jù)共享
字段的定義中予跌,沒有直接定義path字段搏色,而是定義了parent和name,因?yàn)閳D片/視頻文件可能有成千上萬個(gè)券册,但是目錄大概率不會超過3位數(shù)频轿,所以,我們可以通過復(fù)用parent來節(jié)約內(nèi)存烁焙。
同理航邢,mime也可以復(fù)用。
截取部分查詢的代碼:
int count = cursor.getCount();
List<MediaData> list = new ArrayList<>(count);
while (cursor.moveToNext()) {
String path = cursor.getString(IDX_DATA);
String parent = parentPool.getOrAdd(Utils.getParentPath(path));
String name = Utils.getFileName(path);
String mime = mimePool.getOrAdd(cursor.getString(IDX_MIME_TYPE));
// ......
}
復(fù)用字符串骄蝇,可以用HashMap來做膳殷,我這邊是仿照HashMap寫了一個(gè)專用的類來實(shí)現(xiàn)。
getOrAdd方法九火,傳入一個(gè)字符串赚窃,如果容器中已經(jīng)有這個(gè)字符串,返回容器保存的字符串岔激,
否則勒极,保存當(dāng)前字符串并返回。
如此虑鼎,所有的MediaData共用相同parent和mime字符串對象辱匿。
4.2.2 處理無效數(shù)據(jù)
前面提到,從MediaStore讀取的數(shù)據(jù)炫彩,有部分是無效的匾七,
因此,這些可能無效的字段不要直接public, 而是提供get方法江兢,并在返回之前檢查數(shù)據(jù)的有效性昨忆,如果數(shù)據(jù)無效則讀文件獲取數(shù)據(jù)。
當(dāng)然杉允,讀文件是耗時(shí)操作邑贴,雖然一般情況下時(shí)間是可控的限府,但是最好還是放IO線程去訪問比較保險(xiǎn)。
也有比較折中的做法:
- 數(shù)據(jù)只是用作參考痢缎,有的話更好,沒有也沒關(guān)系世澜。
如果是這樣的話独旷,提供不做檢查直接返回?cái)?shù)據(jù)的方法:
public int getWidth() {
return rotate != ROTATE_YES ? width : height;
}
public int getHeight() {
return rotate != ROTATE_YES ? height : width;
}
- 數(shù)據(jù)比較重要,但也不至于沒有就不行寥裂。
這種case嵌洼,當(dāng)數(shù)據(jù)無效時(shí),可以嘗試是讀取封恰,但是加個(gè)timeout, 在規(guī)定時(shí)間內(nèi)沒有完成讀取則直接返回麻养。
public int getDuration() {
if (isVideo && duration == 0) {
checkData();
}
return duration;
}
void checkData() {
if (!hadFillData) {
FutureTask<Boolean> future = new FutureTask<>(this::fillData);
try {
// Limit the time for filling extra info, in case of ANR.
AlbumConfig.getExecutor().execute(future);
future.get(300, TimeUnit.MILLISECONDS);
} catch (Throwable ignore) {
}
}
}
4.3 數(shù)據(jù)加載
數(shù)據(jù)加載部分是最影響相冊體驗(yàn)的因素之一。
等待時(shí)間诺舔、數(shù)據(jù)刷新鳖昌,數(shù)據(jù)有效性等都會影響相冊的交互。
4.3.1 緩存MediaData
媒體庫查詢是一個(gè)綜合IO讀取和CPU密集計(jì)算的操作低飒,文件少的時(shí)候還好许昨,一旦文件比較多,耗時(shí)幾秒鐘也是有的褥赊。
如果用戶每次打開相冊都要等幾秒鐘才刷出數(shù)據(jù)糕档,那體驗(yàn)就太糟糕了。
加個(gè)MediaData的緩存拌喉,再次進(jìn)入相冊時(shí)速那,就不需要再次讀所有字段了,
只需讀取MediaStore的ID字段尿背,然后結(jié)合緩存端仰,做下Diff, 已刪除的移除出緩存,新增的根據(jù)ID檢索其記錄残家,創(chuàng)建MediaData添加到緩存榆俺。
再次進(jìn)入相冊,即使有增刪也不會太多坞淮。
緩存MediaData的好處不僅僅是加速再次查詢MediaStore茴晋,還可以減少對象的創(chuàng)建,不需要每次查詢都重新創(chuàng)建MediaData對象回窘;
另外诺擅,前面也提到,MediaData部分字段有的是無效的啡直,在無效時(shí)需要讀取原文件獲取烁涌,緩存MediaData可免去再次讀文件獲取數(shù)據(jù)的時(shí)間(如果對象是讀取MediaStore重新創(chuàng)建的苍碟,就又回到無效的狀態(tài)了)。
還有就是撮执,有緩存的話微峰,就可以做preload了。
當(dāng)然這個(gè)得看APP是否有這個(gè)需求抒钱,如果APP是媒體相關(guān)的蜓肆,大概率要訪問相冊的,可以考慮preload谋币。
做緩存的代價(jià)就是要占用些內(nèi)存仗扬,這也是前面MediaData為什么復(fù)用parent和mime的原因。
緩存是空間換時(shí)間蕾额,復(fù)用對象是時(shí)間換空間早芭,總體而言這個(gè)對沖是賺的,因?yàn)樽x取IO更耗時(shí)诅蝶。
另外退个,如果有必要,可以提供clearCache接口秤涩,在適當(dāng)?shù)臅r(shí)機(jī)清空緩存帜乞。
4.3.2 組裝結(jié)果
相冊的UI層需要是根據(jù)Request的查詢條件過濾后的MediaData, 并以目錄為分組,按更新時(shí)間降序排列的數(shù)據(jù)筐眷。
緩存的MediaData并非查詢的終點(diǎn)黎烈,但卻提供了一個(gè)好的起點(diǎn)。
在有緩存好的MediaData列表的前提下匀谣,可直接根據(jù)MediaData列表做過濾照棋,排序和分組,
而不需要每次都將過濾條件拼接SQL到數(shù)據(jù)庫中查詢武翎,而且相比于拼接SQL烈炭,在上層直接根據(jù)MediaData過濾要更加靈活。
下面是EasyAlbum基于MediaData緩存的查詢:
private static List<Folder> makeResult(AlbumRequest request) {
AlbumRequest.MediaFilter filter = request.filter;
ArrayList<MediaData> totalList = new ArrayList<>(mediaCache.size());
if (filter == null) {
totalList.addAll(mediaCache.values());
} else {
// 根據(jù)filter過濾MediaData
for (MediaData item : mediaCache.values()) {
if (filter.accept(item)) {
totalList.add(item);
}
}
}
// 先對所有MediaData排序宝恶,后面分組后就不需要繼續(xù)在分組內(nèi)排序了
// 因?yàn)榉纸M時(shí)是按順序放到分組列表的符隙。
Collections.sort(totalList);
Map<String, ArrayList<MediaData>> groupMap = new HashMap<>();
for (MediaData item : totalList) {
String parent = item.parent;
ArrayList<MediaData> subList = groupMap.get(parent);
if (subList == null) {
subList = new ArrayList<>();
groupMap.put(parent, subList);
}
subList.add(item);
}
final List<Folder> result = new ArrayList<>(groupMap.size() + 1);
for (Map.Entry<String, ArrayList<MediaData>> entry : groupMap.entrySet()) {
String folderName = Utils.getFileName(entry.getKey());
result.add(new Folder(folderName, entry.getValue()));
}
// 對目錄排序
Collections.sort(result, request.folderComparator);
// 最后,總列表放在最前
result.add(0, new Folder(request.getAllString(), totalList));
return result;
}
MediaFilter的定義如下:
public interface MediaFilter {
boolean accept(MediaData media);
// To identify the filter
String tag();
}
基于MediaData緩存列表的查詢雖然比基于數(shù)據(jù)庫的查詢快不少垫毙,但是當(dāng)文件很多時(shí)霹疫,也還是要花一些時(shí)間的。
所以我們可以再加一個(gè)緩存:緩存最終結(jié)果综芥。
再加一個(gè)結(jié)果緩存丽蝎,只是增加了些容器,容器指向的對象(MediaData)是之前MediaData緩存列表所引用的對象膀藐,所以代價(jià)還好屠阻。
再次進(jìn)入相冊時(shí)红省,可以先直接取結(jié)果顯示,然后再去檢查MediaStore相對于緩存有沒有變更国觉,有則刷新緩存和UI吧恃,否則直接返回。
APP可能有多個(gè)地方需要相冊麻诀,不同地方查詢條件可能不一樣蚜枢,所以MediaFilter定義了tag接口,用來區(qū)分不同的查詢针饥。
4.3.3 加載流程
流程圖如下:
注意,以上的“結(jié)果”是提供給相冊頁面顯示的數(shù)據(jù)需频,并非相冊返回給調(diào)用者的“已選中的媒體”丁眼。
做了兩層緩存,加載流程是會復(fù)雜一些的昭殉。
但好處也是顯而易見的苞七,增加了結(jié)果緩存之后,再次啟動相冊就基本是“秒開”了挪丢。
查詢過程是在后臺線程中執(zhí)行的蹂风,結(jié)果通過handler發(fā)送給AlbumActivity。
圖中還有一些小處理沒畫出來乾蓬。
比如惠啄,首次加載,在發(fā)送結(jié)果給相冊界面之后任内,還會繼續(xù)執(zhí)行一個(gè)“檢查文件是否已刪除”的操作撵渡。
針對的是這么一種情況:MediaStore中的記錄,DATA字段所對應(yīng)的文件不存在死嗦。
我自己的設(shè)備上是沒有出現(xiàn)過這種case, 我也是聽前輩講的趋距,或許他們遇到過。
如果確實(shí)有設(shè)備存在這樣的情況越除,的確應(yīng)該檢查一下节腐,否則相冊滑動到這些“文件不存在”的記錄時(shí),會只看到一片黑摘盆,稍微影響體驗(yàn)翼雀。
但由于我自己沒有具體考證,所以在EasyAblum的全局配置中留了option, 可以設(shè)置不執(zhí)行骡澈。
關(guān)于這點(diǎn)大家按具體情況自行評估锅纺。
加載流程一般在進(jìn)入相冊頁時(shí)啟動。
考慮到用戶在瀏覽相冊時(shí)肋殴,有時(shí)候可能會切換出去拍照或者刪除照片囤锉,
可以在onResume的時(shí)候也啟動一下加載流程坦弟,檢查是否有媒體文件增刪。
五官地、相冊列表
5.1 媒體縮略圖
Android系統(tǒng)對相冊文件提供了獲取縮略圖的API酿傍,通過該API獲取圖片要比直接讀取媒體文件本身要快很多。
一些圖片加載框架中有實(shí)現(xiàn)相關(guān)邏輯驱入,比如Glide的實(shí)現(xiàn)了MediaStoreImageThumbLoader和MediaStoreVideoThumbLoader赤炒,但是所用API比較舊,在我的設(shè)備(Android 10)上已經(jīng)不生效了亏较。
如果使用Glide的朋友可以自行實(shí)現(xiàn)ModelLoader和ResourceDecoder來處理莺褒。
EasyAlbum的Demo中有實(shí)現(xiàn),感興趣的朋友可以參考一下雪情。
5.2 列表布局
相冊列表的item通常是正方形遵岩,如果用RecycleView布局,最好能讓每一列都等寬巡通。
下面這個(gè)ItemDecoration的實(shí)現(xiàn)是其中一種方法:
public class GridItemDecoration extends RecyclerView.ItemDecoration {
private final int n; // 列的數(shù)量
private final int space; // 列與列之間的間隔
private final int part; // 每一列應(yīng)該分?jǐn)偠嗌匍g隔
public GridItemDecoration(int n, int space) {
this.n = n;
this.space = space;
// 總間隔:space * (n - 1) 尘执,等分n份
part = space * (n - 1) / n;
}
@Override
public void getItemOffsets(
@NonNull Rect outRect,
@NonNull View view,
@NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
int position = parent.getChildLayoutPosition(view);
int column = position % n;
outRect.left = Math.round(part * column / (float) (n - 1));
outRect.right = part - outRect.left;
outRect.top = 0;
outRect.bottom = space;
}
}
起原理就是將所有space加起來,等分為n份宴凉,每個(gè)item分?jǐn)?份誊锭。
其中第i列(index從0開始)的左邊部分的間隔的計(jì)算公式為:space * i / n 。
比方說colomn = 4, 那么就有3個(gè)space; 如果每個(gè)space=4px, 則每個(gè)item分?jǐn)? * (4-1)/ 4 = 3px弥锄。
第1個(gè)item, left=0px, right = 3px丧靡;
第2個(gè)item, left=1px, right = 2px;
第3個(gè)item, left=2px, right =1px籽暇;
第4個(gè)item, left=3px, right =0px窘行。
于是,每個(gè)間隔看起來都是4px, 且每個(gè)item的left+right都是相等的图仓,所以留給view的寬度是相等的罐盔。
效果如下圖:
有的地方是這么去分配left和right的:
outRect.left = column == 0 ? 0 : space / 2;
outRect.right = column == (n - 1) ? 0 : space / 2;
這樣能讓每個(gè)間隔的大小相等,但是view本身的寬度就不相等了救崔。
效果如下圖:
左右兩個(gè)item分別比中間的item多了2px惶看。
這2px看上去不多,但是可能會導(dǎo)致列表變更(增刪)時(shí)六孵,圖片框架的緩存失效纬黎。
例如:
如果刪除了最接近的一張照片,原第2-4列會移動到1-3列劫窒,原第1列會移動到第4列本今。
于是第2列的寬度從266變?yōu)?88,第4列的寬度從288變?yōu)?66,
而圖片加載框架的target寬高是緩存key的計(jì)算要素之一冠息,寬度變了挪凑,就不能命中之前的緩存了。
六逛艰、后序
相冊的實(shí)現(xiàn)可簡單可復(fù)雜躏碳,我見過的最簡單的是直接在主線程查詢媒體數(shù)據(jù)庫的……
本文從各個(gè)方面分享了一些相冊實(shí)現(xiàn)的經(jīng)驗(yàn),尤其是相冊加載部分散怖。
目前這個(gè)時(shí)代菇绵,手機(jī)存幾千上萬張圖片是很常見的,優(yōu)化好相冊的加載镇眷,能提升不少用戶體驗(yàn)咬最。
項(xiàng)目已發(fā)布到 Github 和 Maven Central:
Githun地址:
https://github.com/BillyWei01/EasyAlbum
下載方式:
implementation 'io.github.billywei01:easyalbum:1.0.6'
歡迎各位朋友一鍵三連!
哦不欠动,Github沒有一鍵三連丹诀。
歡迎各位朋友star, folk, 提issue/PR!