*本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
前言
又到了年底,好多的事情都要收尾,今天分享一個(gè)RecyclerView的包裝擴(kuò)展類,幫助大家實(shí)現(xiàn)添加Item的浮層的效果。
首先看一下效果圖:

有人會(huì)問(wèn)我:老鐵,你實(shí)現(xiàn)的這個(gè)東西有個(gè)卵用?如果你沒(méi)看明白,我們?cè)倏匆粡埛浅J煜さ膽?yīng)用場(chǎng)景:

正文
記得2年前在創(chuàng)業(yè)公司的時(shí)候,正是短視頻火爆的高峰期,公司也做了一款二次元的短視頻app,很可惜還沒(méi)上線就被腰斬了。當(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è)頁(yè)面持有多個(gè)播放器,很容易導(dǎo)致內(nèi)存泄露;
可維護(hù)性差:adapter中不可避免的需要插入播放相關(guān)的內(nèi)容,耦合性強(qiáng),代碼臃腫,后期不易維護(hù)。
當(dāng)然這個(gè)方案也有優(yōu)點(diǎn),就是不用考慮列表的滑動(dòng)問(wèn)題,因?yàn)椴シ牌骶驮趇tem里面。
PS:不得不說(shuō)我當(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è)頁(yè)面僅持有一個(gè)播放器。
缺點(diǎn):
滑動(dòng)問(wèn)題:只能適用于滑動(dòng)停止的時(shí)候播放,可擴(kuò)展性差。
性能問(wèn)題:添加和移除View,都會(huì)重新測(cè)量Parent,可能會(huì)出現(xiàn)卡頓問(wèn)題。
這是我偶然想到的一個(gè)實(shí)現(xiàn)思路,僅僅具有參考意義,不推薦使用。
實(shí)現(xiàn)思路三(最終方案)
通過(guò)控制一個(gè)浮層的顯示,隱藏和滑動(dòng),覆蓋列表中播放位置的item。
優(yōu)點(diǎn):
解耦:adapter完全不用寫播放邏輯,因?yàn)橐呀?jīng)被分離到懸浮的View中;
性能:一個(gè)列表僅持有一個(gè)播放器,也不會(huì)涉及到View的測(cè)量相關(guān)的問(wèn)題。
缺點(diǎn):
如果硬要說(shuō)缺點(diǎn)的話,就是要對(duì)列表的滑動(dòng)控制很精確,熟悉各種api和監(jiān)聽(tīng)器。
這也是我最終確定的方案,也是目前想到的最完美的方案。
代碼
我們?yōu)樽远xView確命名為:FloatItemRecyclerView。
我們的目的是擴(kuò)展RecyclerView,所以FloatItemRecyclerView的定位是一個(gè)包裝擴(kuò)展類,什么是包裝擴(kuò)展類呢?
例如比較有名氣的開(kāi)源框架:PtrClassicFrameLayout,他實(shí)現(xiàn)的功能是下拉刷新功能,只要把需要下拉刷新的View放到里面去,就實(shí)現(xiàn)了刷新功能,不影響View本身的功能,把對(duì)架構(gòu)的影響降到最低。
開(kāi)發(fā)中,我們的通用架構(gòu)中往往會(huì)使用一些開(kāi)源的或自定義的RecyclerView,這種設(shè)計(jì)就會(huì)很棒,哪里需要套哪里,十分瀟灑。
所以FloatItemRecyclerView內(nèi)部需要持有一個(gè)RecyclerView類型的對(duì)象,我們通過(guò)泛型可以添加任意類型的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();
}
}
我們需要通過(guò)設(shè)置FloatViewShowHook完成的初始化工作:
initFloatItemRecyclerView:添加指定類型的RecyclerView,你需要自己設(shè)置LayoutManager和其他屬性。
needShowFloatView:判斷RecyclerView的某一個(gè)child是否需要顯示浮層。如果你對(duì)RecyclerAdapter添加了Header或者Footer,別忘了對(duì)position做加減處理。你可以根據(jù)child 的位置或者通過(guò)position得到對(duì)應(yīng)的數(shù)據(jù),判斷是否要顯示浮層,例如圖片和視頻混合的列表,可以實(shí)現(xiàn)圖片不添加浮層,而視頻需要浮層播放的效果。
然后需要添加OnScrollListener監(jiān)聽(tīng)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:
// 對(duì)正在顯示的浮層的child做個(gè)副本,為了判斷顯示浮層的child是否發(fā)現(xiàn)了變化
View tempFirstChild = needFloatChild;
// 更新浮層的位置,覆蓋child
updateFloatScrollStopTranslateY();
// 如果firstChild沒(méi)有發(fā)生變化,回調(diào)floatView滑動(dòng)停止的監(jiān)聽(tīng)
if (tempFirstChild == needFloatChild) {
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollStopFloatView(floatView);
}
}
break;
// 開(kāi)始滑動(dòng)
case 1:
// 更新浮層的位置
updateFloatScrollStartTranslateY();
break;
// Fling
// 這里有一個(gè)bug,如果手指在屏幕上快速滑動(dòng),但是手指并未離開(kāi),仍然有可能觸發(fā)Fling
// 所以這里不對(duì)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;
// 開(kāi)始滑動(dòng)
case 1:
updateFloatScrollStartTranslateY();
break;
// Fling
case 2:
updateFloatScrollStartTranslateY();
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollFlingFloatView(floatView);
}
break;
}
}
};
recyclerView.addOnScrollListener(myScrollerListener);
}
簡(jiǎn)單的概括實(shí)現(xiàn)的邏輯:
- 靜止?fàn)顟B(tài):遍歷RecyclerView的child,通過(guò)配置的Hook,判斷child是否需要顯示浮層,找到則跳出循環(huán),通過(guò)這個(gè)child的位置,更新浮層的位置。
- 開(kāi)始滑動(dòng):如果有顯示浮層的child,不停的刷新浮層的位置。
- 慣性滑動(dòng):注釋上已經(jīng)寫的很清楚了,不做處理。
對(duì)于child是否顯示浮層的判斷過(guò)程:
/**
* 計(jì)算需要顯示floatView的位置
*
* @return 如果找到RecyclerView中對(duì)應(yīng)的child,返回child的位置,否則發(fā)揮-1,表示沒(méi)有要顯示浮層的child
*/
private int calculateShowFloatViewPosition() {
// 如果沒(méi)有設(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 表示沒(méi)有需要顯示floatView的item
return -1;
}
如何判斷child被滑出了屏幕呢?可以通過(guò)設(shè)置監(jiān)聽(tīng)addOnChildAttachStateChangeListener,判斷正在被移除的View是否是顯示浮層的View。
// 監(jiān)聽(tīng)item的移除情況
recyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
// 判斷child是否被移除
// 請(qǐng)注意:回調(diào)onChildViewDetachedFromWindow時(shí)并沒(méi)有真正移除這個(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)測(cè)試發(fā)現(xiàn),在滾動(dòng)的時(shí)候RecyclerView可能會(huì)執(zhí)行onLayout,在onLayout時(shí),又會(huì)把所有的child調(diào)用remove,然后回調(diào)onChildViewDetachedFromWindow,最終刷新adapter,從而導(dǎo)致浮層閃爍的問(wèn)題。
通過(guò)查看源碼發(fā)現(xiàn),dispatchChildDetached負(fù)責(zé)分發(fā)onChildViewDetachedFromWindow,然后才真正移除child:
所以我們可以增加判斷:要被移除的正在顯示浮層child,如果正在被adapter使用,我們不去隱藏顯示浮層,這樣就避免了浮層閃爍的問(wèn)題。具體隱藏閃爍的原因還不清楚,可能跟我與PtrClassicFrameLayout一起使用有關(guān)。
我們還得增加一個(gè)OnLayoutChangeListener,當(dāng)設(shè)置adapter和數(shù)據(jù)發(fā)生變化的時(shí)候會(huì)得到這個(gè)回調(diào),我們可以重新判斷具體哪一個(gè)Child要顯示浮層。
// 設(shè)置OnLayoutChangeListener監(jiān)聽(tīng),會(huì)在設(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();
}
});
整體思路就是這么簡(jiǎn)單,如果你需要這樣的效果,你只需要添加如下代碼:
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ù)貼出來(lái)了。
最后
突然想起在網(wǎng)上看到的一個(gè)段子:一個(gè)Android開(kāi)發(fā)程序員,因?yàn)椴粫?huì)使用RecyclerView面試被拒了。
無(wú)論這個(gè)段子的是真是假,可見(jiàn)熟練使用RecyclerView已經(jīng)變得非常重要。我們要開(kāi)發(fā)一個(gè)列表,是選擇ListView還是RecyclerView呢?簡(jiǎn)單說(shuō)一下我的經(jīng)驗(yàn):
1、
開(kāi)發(fā)通用架構(gòu)推薦使用RecyclerView,否則你可能要維護(hù)多套不同樣式的庫(kù)。(列表,網(wǎng)格,瀑布流,自定義等,便于擴(kuò)展)
2、
僅僅是開(kāi)發(fā)一個(gè)列表,推薦使用RecyclerView,如果產(chǎn)品經(jīng)理心情好,換成瀑布流怎么辦。(便于維護(hù))
3、
如果開(kāi)發(fā)自定義View,且列表需要Header或Footer:如果和項(xiàng)目耦合性較強(qiáng),且已經(jīng)有擴(kuò)展好的RecyclerView.Adapter,可以優(yōu)先考慮使用RecyclerView;如果是想寫開(kāi)源庫(kù),ListView可以優(yōu)先考慮,因?yàn)檫x擇RecyclerView需要捆綁一個(gè)可以添加Header和Footer的Adapter,需要慎重考慮。
之后我會(huì)再做一個(gè)ListView的版本,方便大家使用。
以上就是今天分享的內(nèi)容,希望對(duì)大家今后的學(xué)習(xí)工作有所幫助。本來(lái)想發(fā)布到j(luò)center上,不過(guò)似乎gradle 4.6和bintray插件不兼容,只能暫時(shí)上傳到github上,大家可以下載查看具體內(nèi)容。