*本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布
前言
又到了年底,好多的事情都要收尾,今天分享一個(gè)RecyclerView的包裝擴(kuò)展類,幫助大家實(shí)現(xiàn)添加Item的浮層的效果揩慕。
首先看一下效果圖:
有人會問我:老鐵加叁,你實(shí)現(xiàn)的這個(gè)東西有個(gè)卵用?如果你沒看明白蔗怠,我們再看一張非常熟悉的應(yīng)用場景:
正文
記得2年前在創(chuàng)業(yè)公司的時(shí)候墩弯,正是短視頻火爆的高峰期,公司也做了一款二次元的短視頻app寞射,很可惜還沒上線就被腰斬了渔工。當(dāng)時(shí)就要求做了這個(gè)效果,雖然實(shí)現(xiàn)了桥温,但是實(shí)現(xiàn)的方案實(shí)在是太low了引矩。今天也是彌補(bǔ)了這個(gè)遺憾。
實(shí)現(xiàn)思路一
在每一個(gè)Item中放入一個(gè)VideoPlayer侵浸,但是缺點(diǎn)太多:
可控性差:控制播放哪一個(gè)位置的視頻旺韭,視頻的停止和播放等等,都需要寫大量的邏輯掏觉;
內(nèi)存風(fēng)險(xiǎn)高:播放器還是很占用內(nèi)存的茂翔,一個(gè)頁面持有多個(gè)播放器,很容易導(dǎo)致內(nèi)存泄露履腋;
可維護(hù)性差:adapter中不可避免的需要插入播放相關(guān)的內(nèi)容珊燎,耦合性強(qiáng),代碼臃腫遵湖,后期不易維護(hù)悔政。
當(dāng)然這個(gè)方案也有優(yōu)點(diǎn),就是不用考慮列表的滑動(dòng)問題延旧,因?yàn)椴シ牌骶驮趇tem里面谋国。
PS:不得不說我當(dāng)時(shí)用的就是這個(gè)思路,現(xiàn)在回想一下實(shí)在是太low比了迁沫。
實(shí)現(xiàn)思路二
實(shí)現(xiàn)VideoPlayerController類芦瘾,單例模式捌蚊,封裝視頻播放的相關(guān)邏輯,需要播放哪一個(gè)視頻近弟,添加到指定的item中缅糟,不播放移除播放器。
優(yōu)點(diǎn):
解耦:將adapter和播放邏輯進(jìn)行解耦祷愉,增強(qiáng)維護(hù)性窗宦。
優(yōu)化內(nèi)存,一個(gè)頁面僅持有一個(gè)播放器二鳄。
缺點(diǎn):
滑動(dòng)問題:只能適用于滑動(dòng)停止的時(shí)候播放赴涵,可擴(kuò)展性差。
性能問題:添加和移除View订讼,都會重新測量Parent髓窜,可能會出現(xiàn)卡頓問題。
這是我偶然想到的一個(gè)實(shí)現(xiàn)思路欺殿,僅僅具有參考意義纱烘,不推薦使用。
實(shí)現(xiàn)思路三(最終方案)
通過控制一個(gè)浮層的顯示祈餐,隱藏和滑動(dòng)擂啥,覆蓋列表中播放位置的item。
優(yōu)點(diǎn):
解耦:adapter完全不用寫播放邏輯帆阳,因?yàn)橐呀?jīng)被分離到懸浮的View中哺壶;
性能:一個(gè)列表僅持有一個(gè)播放器,也不會涉及到View的測量相關(guān)的問題蜒谤。
缺點(diǎn):
如果硬要說缺點(diǎn)的話山宾,就是要對列表的滑動(dòng)控制很精確,熟悉各種api和監(jiān)聽器鳍徽。
這也是我最終確定的方案资锰,也是目前想到的最完美的方案。
代碼
我們?yōu)樽远xView確命名為:FloatItemRecyclerView阶祭。
我們的目的是擴(kuò)展RecyclerView绷杜,所以FloatItemRecyclerView的定位是一個(gè)包裝擴(kuò)展類,什么是包裝擴(kuò)展類呢濒募?
例如比較有名氣的開源框架:PtrClassicFrameLayout鞭盟,他實(shí)現(xiàn)的功能是下拉刷新功能,只要把需要下拉刷新的View放到里面去瑰剃,就實(shí)現(xiàn)了刷新功能齿诉,不影響View本身的功能,把對架構(gòu)的影響降到最低。
開發(fā)中粤剧,我們的通用架構(gòu)中往往會使用一些開源的或自定義的RecyclerView歇竟,這種設(shè)計(jì)就會很棒,哪里需要套哪里抵恋,十分瀟灑焕议。
所以FloatItemRecyclerView內(nèi)部需要持有一個(gè)RecyclerView類型的對象,我們通過泛型可以添加任意類型的RecyclerView的子類馋记。
public class FloatItemRecyclerView<V extends RecyclerView> extends FrameLayout {
/**
* 要懸浮的View
*/
private View floatView;
/**
* recyclerView
*/
private V recyclerView;
/**
* 控制每一個(gè)item是否要顯示floatView
*/
private FloatViewShowHook<V> floatViewShowHook;
/**
* 根據(jù)item設(shè)置是否顯示浮動(dòng)的View
*/
public interface FloatViewShowHook<V extends RecyclerView> {
/**
* 當(dāng)前item是否要顯示floatView
*
* @param child itemView
* @param position 在列表中的位置
*/
boolean needShowFloatView(View child, int position);
V initFloatItemRecyclerView();
}
}
我們需要通過設(shè)置FloatViewShowHook完成的初始化工作:
initFloatItemRecyclerView:添加指定類型的RecyclerView,你需要自己設(shè)置LayoutManager和其他屬性懊烤。
needShowFloatView:判斷RecyclerView的某一個(gè)child是否需要顯示浮層梯醒。如果你對RecyclerAdapter添加了Header或者Footer,別忘了對position做加減處理腌紧。你可以根據(jù)child 的位置或者通過position得到對應(yīng)的數(shù)據(jù)茸习,判斷是否要顯示浮層,例如圖片和視頻混合的列表壁肋,可以實(shí)現(xiàn)圖片不添加浮層号胚,而視頻需要浮層播放的效果。
然后需要添加OnScrollListener監(jiān)聽RecyclerView的滑動(dòng)狀態(tài):
private void initOnScrollListener() {
RecyclerView.OnScrollListener myScrollerListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (floatView == null) {
return;
}
currentState = newState;
switch (newState) {
// 停止滑動(dòng)
case 0:
// 對正在顯示的浮層的child做個(gè)副本浸遗,為了判斷顯示浮層的child是否發(fā)現(xiàn)了變化
View tempFirstChild = needFloatChild;
// 更新浮層的位置猫胁,覆蓋child
updateFloatScrollStopTranslateY();
// 如果firstChild沒有發(fā)生變化,回調(diào)floatView滑動(dòng)停止的監(jiān)聽
if (tempFirstChild == needFloatChild) {
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollStopFloatView(floatView);
}
}
break;
// 開始滑動(dòng)
case 1:
// 更新浮層的位置
updateFloatScrollStartTranslateY();
break;
// Fling
// 這里有一個(gè)bug跛锌,如果手指在屏幕上快速滑動(dòng)弃秆,但是手指并未離開,仍然有可能觸發(fā)Fling
// 所以這里不對Fling狀態(tài)進(jìn)行處理
// case 2:
// hideFloatView();
// break;
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (floatView == null) {
return;
}
switch (currentState) {
// 停止滑動(dòng)
case 0:
updateFloatScrollStopTranslateY();
break;
// 開始滑動(dòng)
case 1:
updateFloatScrollStartTranslateY();
break;
// Fling
case 2:
updateFloatScrollStartTranslateY();
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollFlingFloatView(floatView);
}
break;
}
}
};
recyclerView.addOnScrollListener(myScrollerListener);
}
簡單的概括實(shí)現(xiàn)的邏輯:
- 靜止?fàn)顟B(tài):遍歷RecyclerView的child髓帽,通過配置的Hook菠赚,判斷child是否需要顯示浮層,找到則跳出循環(huán)郑藏,通過這個(gè)child的位置衡查,更新浮層的位置。
- 開始滑動(dòng):如果有顯示浮層的child必盖,不停的刷新浮層的位置拌牲。
- 慣性滑動(dòng):注釋上已經(jīng)寫的很清楚了,不做處理歌粥。
對于child是否顯示浮層的判斷過程:
/**
* 計(jì)算需要顯示floatView的位置
*
* @return 如果找到RecyclerView中對應(yīng)的child们拙,返回child的位置,否則發(fā)揮-1阁吝,表示沒有要顯示浮層的child
*/
private int calculateShowFloatViewPosition() {
// 如果沒有設(shè)置floatViewShowHook砚婆,默認(rèn)返回-1
if (floatViewShowHook == null) {
return -1;
}
int firstVisiblePosition;
if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
firstVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
} else {
throw new IllegalArgumentException("only support LinearLayoutManager!!!");
}
int childCount = recyclerView.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = recyclerView.getChildAt(i);
// 判斷這個(gè)child是否需要顯示
if (child != null && floatViewShowHook.needShowFloatView(child, firstVisiblePosition + i)) {
return i;
}
}
// -1 表示沒有需要顯示floatView的item
return -1;
}
如何判斷child被滑出了屏幕呢?可以通過設(shè)置監(jiān)聽addOnChildAttachStateChangeListener,判斷正在被移除的View是否是顯示浮層的View装盯。
// 監(jiān)聽item的移除情況
recyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
// 判斷child是否被移除
// 請注意:回調(diào)onChildViewDetachedFromWindow時(shí)并沒有真正移除這個(gè)child
// 所以這里增加一個(gè)判斷:floatChildInScreen是否正在被adapter使用坷虑,防止浮層閃爍
if (view == needFloatChild && floatChildInScreen()) {
clearFirstChild();
}
}
});
/**
* 判斷item是否正在顯示內(nèi)容
*/
private boolean floatChildInScreen() {
return recyclerView.getChildAdapterPosition(needFloatChild) != -1;
}
這里還額外判斷了floatChildInScreen(),這是因?yàn)榻?jīng)測試發(fā)現(xiàn)埂奈,在滾動(dòng)的時(shí)候RecyclerView可能會執(zhí)行onLayout迄损,在onLayout時(shí),又會把所有的child調(diào)用remove账磺,然后回調(diào)onChildViewDetachedFromWindow芹敌,最終刷新adapter,從而導(dǎo)致浮層閃爍的問題垮抗。
通過查看源碼發(fā)現(xiàn)氏捞,dispatchChildDetached負(fù)責(zé)分發(fā)onChildViewDetachedFromWindow,然后才真正移除child:
所以我們可以增加判斷:要被移除的正在顯示浮層child冒版,如果正在被adapter使用液茎,我們不去隱藏顯示浮層,這樣就避免了浮層閃爍的問題辞嗡。具體隱藏閃爍的原因還不清楚捆等,可能跟我與PtrClassicFrameLayout一起使用有關(guān)。
我們還得增加一個(gè)OnLayoutChangeListener续室,當(dāng)設(shè)置adapter和數(shù)據(jù)發(fā)生變化的時(shí)候會得到這個(gè)回調(diào)栋烤,我們可以重新判斷具體哪一個(gè)Child要顯示浮層。
// 設(shè)置OnLayoutChangeListener監(jiān)聽挺狰,會在設(shè)置adapter和adapter.notifyXXX的時(shí)候回調(diào)
// 所以我們要這里判斷顯示浮層的child
recyclerView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (recyclerView.getAdapter() == null) {
return;
}
// 數(shù)據(jù)已經(jīng)刷新班缎,找到需要顯示懸浮的Item
clearFirstChild();
// 找到第一個(gè)child
getFirstChild();
updateFloatScrollStartTranslateY();
showFloatView();
}
});
整體思路就是這么簡單,如果你需要這樣的效果她渴,你只需要添加如下代碼:
FloatItemRecyclerView<RecyclerView> recyclerView = findViewById(R.id.recycler_view);
recyclerView.setFloatViewShowHook(this);
recyclerView.setFloatView(getLayoutInflater().inflate(R.layout.float_view, (ViewGroup) getWindow().getDecorView(), false));
recyclerView.setOnFloatViewShowListener(this);
recyclerView.setAdapter(new MyAdapter());
// 手動(dòng)查詢要顯示浮層的child
recyclerView.findChildToPlay()
效果就是第一張圖达址,這里就不重復(fù)貼出來了。
最后
突然想起在網(wǎng)上看到的一個(gè)段子:一個(gè)Android開發(fā)程序員趁耗,因?yàn)椴粫褂肦ecyclerView面試被拒了沉唠。
無論這個(gè)段子的是真是假,可見熟練使用RecyclerView已經(jīng)變得非常重要苛败。我們要開發(fā)一個(gè)列表满葛,是選擇ListView還是RecyclerView呢?簡單說一下我的經(jīng)驗(yàn):
1罢屈、
開發(fā)通用架構(gòu)推薦使用RecyclerView嘀韧,否則你可能要維護(hù)多套不同樣式的庫。(列表缠捌,網(wǎng)格锄贷,瀑布流译蒂,自定義等,便于擴(kuò)展)
2谊却、
僅僅是開發(fā)一個(gè)列表柔昼,推薦使用RecyclerView,如果產(chǎn)品經(jīng)理心情好炎辨,換成瀑布流怎么辦捕透。(便于維護(hù))
3、
如果開發(fā)自定義View碴萧,且列表需要Header或Footer:如果和項(xiàng)目耦合性較強(qiáng)乙嘀,且已經(jīng)有擴(kuò)展好的RecyclerView.Adapter,可以優(yōu)先考慮使用RecyclerView破喻;如果是想寫開源庫虎谢,ListView可以優(yōu)先考慮,因?yàn)檫x擇RecyclerView需要捆綁一個(gè)可以添加Header和Footer的Adapter低缩,需要慎重考慮嘉冒。
之后我會再做一個(gè)ListView的版本曹货,方便大家使用咆繁。
以上就是今天分享的內(nèi)容,希望對大家今后的學(xué)習(xí)工作有所幫助顶籽。本來想發(fā)布到j(luò)center上玩般,不過似乎gradle 4.6和bintray插件不兼容,只能暫時(shí)上傳到github上礼饱,大家可以下載查看具體內(nèi)容坏为。