自定義View:實現RecyclerView的item新增懸浮層的效果
前言
20天后,終於良心發現更新部落格了,又到了年底,好多的事情都要收尾,今天分享一個RecyclerView的容器類,幫助大家實現新增Item的浮層的效果。
首先看一下效果圖:
有人會問我:老鐵,你實現的這個東西有個卵用?如果你沒看明白,我們再看一張非常熟悉的應用場景:
正文
記得2年前在創業公司的時候,正是短視訊火爆的高峰期,公司也做了一款二次元的短視訊app,很可惜還沒上線就被腰斬了。當時就要求做了這個效果,雖然實現了,但是實現的方案實在是太low了。今天也算是彌補了這個遺憾。
實現思路一
在每一個Item中放入一個VideoPlayer,但是缺點太多:
可控性差:控制播放哪一個位置的視訊,視訊的停止和播放等等,都需要寫大量的邏輯;
記憶體風險高:播放器還是很佔用記憶體的,一個頁面持有多個播放器,很容易導致記憶體洩露;
可維護性差:adapter中不可避免的需要插入播放相關的內容,耦合性強,程式碼臃腫,後期不易維護。
當然這個方案也有優點,就是不用考慮列表的滑動問題,因為播放器就在item裡面。
PS:不得不說我當時用的就是這個思路,現在回想一下實在是太low比了。
實現思路二
實現VideoPlayerController類,單例模式,封裝視訊播放的相關邏輯,需要播放哪一個視訊,新增到指定的item中,不播放移除播放器。
優點:
解耦:將adapter和播放邏輯進行解耦,增強維護性。
優化記憶體,一個頁面僅持有一個播放器。
缺點:
滑動問題:只能適用於滑動停止的時候播放,可擴充套件性差。
效能問題:新增和移除View,都會重新測量Parent,可能會出現卡頓問題。
這是我偶然想到的一個實現思路,僅僅具有參考意義,不推薦使用。
實現思路三(最終方案)
通過控制一個浮層的顯示,隱藏和滑動,覆蓋列表中播放位置的item。
優點:
解耦:adapter完全不用寫播放邏輯,因為已經被分離到懸浮的View中;
效能:一個列表僅持有一個播放器,也不會涉及到View的測量相關的問題。
缺點:
如果硬要說缺點的話,就是要對列表的滑動控制很精確,熟悉各種api和監聽器。
這也是我最終確定的方案,也是目前想到的最完美的方案。
程式碼
我們為自定義View確命名為:FloatItemRecyclerView。
我們的目的是擴充套件RecyclerView,所以FloatItemRecyclerView是一個包裝擴充套件類,什麼是包裝擴充套件類呢?例如比較有名氣的開源框架:PtrClassicFrameLayout,他實現的功能是下拉重新整理功能,只要把需要下拉重新整理的View放到裡面去,就實現了重新整理功能,不影響View本身的功能,把對架構的影響降到最低。
開發中,我們的通用架構中往往會使用一些開源的或自定義的RecyclerView,這種設計就會很棒,哪裡需要套哪裡,十分瀟灑。
所以FloatItemRecyclerView內部需要持有一個RecyclerView型別的物件,我們通過泛型可以新增任意型別的RecyclerView的子類。
public class FloatItemRecyclerView<V extends RecyclerView> extends FrameLayout {
/**
* 要懸浮的View
*/
private View floatView;
/**
* recyclerView
*/
private V recyclerView;
/**
* 控制每一個item是否要顯示floatView
*/
private FloatViewShowHook<V> floatViewShowHook;
/**
* 根據item設定是否顯示浮動的View
*/
public interface FloatViewShowHook<V extends RecyclerView> {
/**
* 當前item是否要顯示floatView
*
* @param child itemView
* @param position 在列表中的位置
*/
boolean needShowFloatView(View child, int position);
V initVideoPlayRecyclerView();
}
}
通過指定FloatViewShowHook完成RecyclerView的新增和判斷RecyclerView的Item是否要顯示浮層。
然後需要新增OnScrollListener監聽RecyclerView的滑動狀態:
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) {
// 停止滑動
case 0:
View tempFirstChild = firstChild;
updateFloatScrollStopTranslateY();
// 如果firstChild沒有發生變化,回撥floatView滑動停止的監聽
if (tempFirstChild == firstChild) {
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollStopFloatView(floatView);
}
}
break;
// 開始滑動
case 1:
// 儲存第一個child
// getFirstChild();
updateFloatScrollStartTranslateY();
// showFloatView();
break;
// Fling
// 這裡有一個bug,如果手指在螢幕上快速滑動,但是手指並未離開,仍然有可能觸發Fling
// 所以這裡不對Fling狀態進行處理
// 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) {
// 停止滑動
case 0:
updateFloatScrollStopTranslateY();
break;
// 開始滑動
case 1:
updateFloatScrollStartTranslateY();
break;
// Fling
case 2:
updateFloatScrollStartTranslateY();
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollFlingFloatView(floatView);
}
break;
}
}
};
簡單的概括實現的邏輯:
- 靜止狀態:遍歷RecyclerView的child,通過配置的Hook,判斷child是否需要顯示浮層,找到跳出迴圈,通過這個child的位置,更新浮層的位置。
- 開始滑動:如果有顯示浮層的View,不停的重新整理浮層的位置,如果View已經劃出螢幕,重新找新的View。
- 慣性滑動:註釋上已經寫的很清楚了,不做處理。
如何判斷child被滑出了螢幕呢?可以通過設定監聽addOnChildAttachStateChangeListener,判斷正在被移除的View是否是顯示浮層的View。
// 監聽item的移除情況
recyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
if (view == firstChild && outScreen()) {
clearFirstChild();
}
}
});
這裡還額外判斷了outScreen(),這是因為onChildViewDetachedFromWindow被回撥的時候,實際上還沒有被remove掉,所以會存在判斷的誤差,導致浮層會閃爍的問題。
我們還得增加一個OnLayoutChangeListener,當設定adapter和資料發生變化的時候會得到這個回撥,我們可以重新判斷具體哪一個Child要顯示浮層。
// 設定OnLayoutChangeListener監聽,會在設定adapter和adapter.notifyXXX的時候回撥
// 所以我們要這裡做一些處理
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;
}
// 資料已經重新整理,找到需要顯示懸浮的Item
clearFirstChild();
// 找到第一個child
getFirstChild();
updateFloatScrollStartTranslateY();
showFloatView();
}
});
整體思路就是這麼簡單。實現一個這樣的效果,我們只需要一個300多行的類,下面貼出完整的程式碼:
/**
* Created by li.zhipeng on 2018/10/10.
* <p>
* RecyclerView包裝類
*/
public class FloatItemRecyclerView<V extends RecyclerView> extends FrameLayout {
/**
* 要懸浮的View
*/
private View floatView;
/**
* recyclerView
*/
private V recyclerView;
/**
* 當前的滑動狀態
*/
private int currentState = -1;
private View firstChild = null;
/**
* 懸浮View的顯示狀態監聽器
*/
private OnFloatViewShowListener onFloatViewShowListener;
/**
* 控制每一個item是否要顯示floatView
*/
private FloatViewShowHook<V> floatViewShowHook;
public FloatItemRecyclerView(@NonNull Context context) {
this(context, null);
}
public FloatItemRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FloatItemRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 設定懸浮的View
*/
public void setFloatView(View floatView) {
this.floatView = floatView;
if (floatView.getLayoutParams() == null) {
floatView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
addView(this.floatView);
this.floatView.setVisibility(View.GONE);
}
/**
* 必須設定FloatViewShowHook,完成View的初始化操作
*/
public void setFloatViewShowHook(FloatViewShowHook<V> floatViewShowHook) {
this.floatViewShowHook = floatViewShowHook;
recyclerView = floatViewShowHook.initVideoPlayRecyclerView();
addRecyclerView();
// 移動到前臺
if (floatView != null) {
bringChildToFront(floatView);
updateViewLayout(floatView, floatView.getLayoutParams());
}
}
@SuppressWarnings("unchecked")
private void addRecyclerView() {
addView(recyclerView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
// 設定滾動監聽
initOnScrollListener();
// 設定佈局監聽,當adapter資料發生改變的時候,需要做一些處理
initOnLayoutChangedListener();
// 監聽recyclerView的item滾動情況,判斷正在懸浮item是否已經移出了螢幕
initOnChildAttachStateChangeListener();
}
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) {
// 停止滑動
case 0:
View tempFirstChild = firstChild;
updateFloatScrollStopTranslateY();
// 如果firstChild沒有發生變化,回撥floatView滑動停止的監聽
if (tempFirstChild == firstChild) {
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollStopFloatView(floatView);
}
}
break;
// 開始滑動
case 1:
// 儲存第一個child
// getFirstChild();
updateFloatScrollStartTranslateY();
// showFloatView();
break;
// Fling
// 這裡有一個bug,如果手指在螢幕上快速滑動,但是手指並未離開,仍然有可能觸發Fling
// 所以這裡不對Fling狀態進行處理
// 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) {
// 停止滑動
case 0:
updateFloatScrollStopTranslateY();
break;
// 開始滑動
case 1:
updateFloatScrollStartTranslateY();
break;
// Fling
case 2:
updateFloatScrollStartTranslateY();
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollFlingFloatView(floatView);
}
break;
}
}
};
recyclerView.addOnScrollListener(myScrollerListener);
}
private void initOnChildAttachStateChangeListener() {
// 監聽item的移除情況
recyclerView.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
if (view == firstChild && outScreen()) {
clearFirstChild();
}
}
});
}
private void initOnLayoutChangedListener() {
// 設定OnLayoutChangeListener監聽,會在設定adapter和adapter.notifyXXX的時候回撥
// 所以我們要這裡做一些處理
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;
}
// 資料已經重新整理,找到需要顯示懸浮的Item
clearFirstChild();
// 找到第一個child
getFirstChild();
updateFloatScrollStartTranslateY();
showFloatView();
}
});
}
/**
* 手動計算應該播放視訊的child
*/
public void findChildToPlay() {
if (firstChild == null) {
updateFloatScrollStopTranslateY();
// 回撥顯示狀態的監聽器
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onShowFloatView(floatView,
recyclerView.getChildAdapterPosition(firstChild));
}
return;
}
// 獲取fistChild在列表中的位置
int position = recyclerView.getChildAdapterPosition(firstChild);
// 判斷是否允許播放
if (floatViewShowHook.needShowFloatView(firstChild, position)) {
updateFloatScrollStartTranslateY();
showFloatView();
// 回撥顯示狀態的監聽器
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onShowFloatView(floatView,
recyclerView.getChildAdapterPosition(firstChild));
}
} else {
// 回撥隱藏狀態的監聽器
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onHideFloatView(floatView);
}
}
}
/**
* 判斷item是否已經畫出了螢幕
*/
private boolean outScreen() {
return recyclerView.getChildAdapterPosition(firstChild) != -1;
}
/**
* 找到第一個要顯示懸浮item的
*/
private void getFirstChild() {
if (firstChild != null) {
return;
}
int childPos = calculateShowFloatViewPosition();
if (childPos != -1) {
firstChild = recyclerView.getChildAt(childPos);
// 回撥顯示狀態的監聽器
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onShowFloatView(floatView,
recyclerView.getChildAdapterPosition(firstChild));
}
}
}
/**
* 計算需要顯示floatView的位置
*/
private int calculateShowFloatViewPosition() {
// 如果沒有設定floatViewShowHook,預設返回第一個Child
if (floatViewShowHook == null) {
return 0;
}
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);
// 判斷這個child是否需要顯示
if (child != null && floatViewShowHook.needShowFloatView(child, firstVisiblePosition + i)) {
return i;
}
}
// -1 表示沒有需要顯示floatView的item
return -1;
}
private void showFloatView() {
if (firstChild != null) {
floatView.post(new Runnable() {
@Override
public void run() {
floatView.setVisibility(View.VISIBLE);
}
});
}
}
private void hideFloatView() {
if (firstChild != null) {
floatView.setVisibility(View.GONE);
}
}
private void updateFloatScrollStartTranslateY() {
if (firstChild != null) {
int translateY = firstChild.getTop();
floatView.setTranslationY(translateY);
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onScrollFloatView(floatView);
}
}
}
private void updateFloatScrollStopTranslateY() {
if (firstChild == null) {
getFirstChild();
}
updateFloatScrollStartTranslateY();
showFloatView();
}
public V getRecyclerView() {
return recyclerView;
}
/**
* 清除floatView依賴的item,並隱藏floatView
*/
public void clearFirstChild() {
hideFloatView();
firstChild = null;
// 回撥監聽器
if (onFloatViewShowListener != null) {
onFloatViewShowListener.onHideFloatView(floatView);
}
}
public void setAdapter(RecyclerView.Adapter adapter) {
recyclerView.setAdapter(adapter);
}
public void setOnFloatViewShowListener(OnFloatViewShowListener onFloatViewShowListener) {
this.onFloatViewShowListener = onFloatViewShowListener;
}
/**
* 顯示狀態的回撥監聽器
*/
public interface OnFloatViewShowListener {
/**
* FloatView被顯示
*/
void onShowFloatView(View floatView, int position);
/**
* FloatView被隱藏
*/
void onHideFloatView(View floatView);
/**
* FloatView被移動
*/
void onScrollFloatView(View floatView);
/**
* FloatView被處於Fling狀態
*/
void onScrollFlingFloatView(View floatView);
/**
* FloatView由滾動變為靜止狀態
*/
void onScrollStopFloatView(View floatView);
}
/**
* 根據item設定是否顯示浮動的View
*/
public interface FloatViewShowHook<V extends RecyclerView> {
/**
* 當前item是否要顯示floatView
*
* @param child itemView
* @param position 在列表中的位置
*/
boolean needShowFloatView(View child, int position);
V initVideoPlayRecyclerView();
}
}
Demo例項:
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());
效果就是第一張圖,這裡就不重複貼出來了。
總結
以上就是今天分享的內容,希望對大家今後的學習工作有所幫助。本來想釋出到jcenter上,不過似乎gradle 4.6和bintray外掛不相容,只能暫時上傳到github上,大家可以下載檢視具體內容。