之前我在GitHub上開源了一個可以實現RecyclerView列表分組的通用Adapter: GroupedRecyclerViewAdapter。有一些朋友在使用的時候給我反饋,希望能實現頭部懸浮吸頂的效果。我當初設計GroupedRecyclerViewAdapter的初衷,是想要實現一個能方便管理RecyclerView多種item型別的Adapter,特別是能實現兩級列表的Adapter,因為這樣的需求在開發中很常見。所以當初我並沒有考慮頭部懸浮的功能。直到接到這些使用者的反饋,我才開始考慮新增這樣的功能。不過想來也確實應該新增這樣的功能,因為頭部懸浮一般出現在兩級分組的列表,而我的GroupedRecyclerViewAdapter本來就已經實現了兩級分組的列表,再新增個頭部懸浮的功能也很合理啊。
為了給RecyclerView實現頭部懸浮的功能,我在GroupedRecyclerViewAdapter框架裡新增了一個StickyHeaderLayout控制元件,由StickyHeaderLayout實現頭部懸浮效果並且管理懸浮吸頂的View。下面我會給出StickyHeaderLayout原始碼,我在原始碼中對StickyHeaderLayout的實現有了比較詳細的註釋,相信大家能很好的理解。由於StickyHeaderLayout是對GroupedRecyclerViewAdapter的功能擴充,它跟GroupedRecyclerViewAdapter密切相關。所以你在閱讀它的原始碼前,需要先了解GroupedRecyclerViewAdapter,而且StickyHeaderLayout也是要與GroupedRecyclerViewAdapter一起使用的。要想了解GroupedRecyclerViewAdapter,請看我的另一篇文章:《Android 可分組的RecyclerViewAdapter
》。如果你只是想使用它的功能,而不需要了解它的實現原理,也可以直接訪問我的GitHub。
StickyHeaderLayout的原始碼:
/**
* Depiction:頭部吸頂佈局。只要用StickyHeaderLayout包裹{@link RecyclerView},
* 並且使用{@link GroupedRecyclerViewAdapter},就可以實現列表頭部吸頂功能。
* StickyHeaderLayout只能包裹RecyclerView,而且只能包裹一個RecyclerView。
* <p>
* Author:donkingliang
* Dat:2017/11/14
*/
public class StickyHeaderLayout extends FrameLayout {
private Context mContext;
private RecyclerView mRecyclerView;
//吸頂容器,用於承載吸頂佈局。
private FrameLayout mStickyLayout;
//儲存吸頂佈局的快取池。它以列表組頭的viewType為key,ViewHolder為value對吸頂佈局進行儲存和回收複用。
private final SparseArray<BaseViewHolder> mStickyViews = new SparseArray<>();
//用於在吸頂佈局中儲存viewType的key。
private final int VIEW_TAG_TYPE = -101;
//用於在吸頂佈局中儲存ViewHolder的key。
private final int VIEW_TAG_HOLDER = -102;
//記錄當前吸頂的組。
private int mCurrentStickyGroup = -1;
//是否吸頂。
private boolean isSticky = true;
public StickyHeaderLayout(@NonNull Context context) {
super(context);
mContext = context;
}
public StickyHeaderLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
public StickyHeaderLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (getChildCount() > 0 || !(child instanceof RecyclerView)) {
//外界只能向StickyHeaderLayout新增一個RecyclerView,而且只能新增RecyclerView。
throw new IllegalArgumentException("StickyHeaderLayout can host only one direct child --> RecyclerView");
}
super.addView(child, index, params);
mRecyclerView = (RecyclerView) child;
addOnScrollListener();
addStickyLayout();
}
/**
* 新增滾動監聽
*/
private void addOnScrollListener() {
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 在滾動的時候,需要不斷的更新吸頂佈局。
if (isSticky) {
updateStickyView();
}
}
});
}
/**
* 新增吸頂容器
*/
private void addStickyLayout() {
mStickyLayout = new FrameLayout(mContext);
LayoutParams lp = new LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT);
mStickyLayout.setLayoutParams(lp);
super.addView(mStickyLayout, 1, lp);
}
/**
* 更新吸頂佈局。
*/
private void updateStickyView() {
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
//只有RecyclerView的adapter是GroupedRecyclerViewAdapter的時候,才會新增吸頂佈局。
if (adapter instanceof GroupedRecyclerViewAdapter) {
GroupedRecyclerViewAdapter gAdapter = (GroupedRecyclerViewAdapter) adapter;
//獲取列表顯示的第一個項。
int firstVisibleItem = getFirstVisibleItem();
//通過顯示的第一個項的position獲取它所在的組。
int groupPosition = gAdapter.getGroupPositionForPosition(firstVisibleItem);
//如果當前吸頂的組頭不是我們要吸頂的組頭,就更新吸頂佈局。這樣做可以避免頻繁的更新吸頂佈局。
if (mCurrentStickyGroup != groupPosition) {
mCurrentStickyGroup = groupPosition;
//通過groupPosition獲取當前組的組頭position。這個組頭就是我們需要吸頂的佈局。
int groupHeaderPosition = gAdapter.getPositionForGroupHeader(groupPosition);
if (groupHeaderPosition != -1) {
//獲取吸頂佈局的viewType。
int viewType = gAdapter.getItemViewType(groupHeaderPosition);
//如果當前的吸頂佈局的型別和我們需要的一樣,就直接獲取它的ViewHolder,否則就回收。
BaseViewHolder holder = recycleStickyView(viewType);
//標誌holder是否是從當前吸頂佈局取出來的。
boolean flag = holder != null;
if (holder == null) {
//從快取池中獲取吸頂佈局。
holder = getStickyViewByType(viewType);
}
if (holder == null) {
//如果沒有從快取池中獲取到吸頂佈局,則通過GroupedRecyclerViewAdapter建立。
holder = gAdapter.onCreateViewHolder(mStickyLayout, viewType);
holder.itemView.setTag(VIEW_TAG_TYPE, viewType);
holder.itemView.setTag(VIEW_TAG_HOLDER, holder);
}
//通過GroupedRecyclerViewAdapter更新吸頂佈局的資料。
//這樣可以保證吸頂佈局的顯示效果跟列表中的組頭保持一致。
gAdapter.onBindViewHolder(holder, groupHeaderPosition);
//如果holder不是從當前吸頂佈局取出來的,就需要把吸頂佈局新增到容器裡。
if (!flag) {
mStickyLayout.addView(holder.itemView);
}
} else {
//如果當前組沒有組頭,則不顯示吸頂佈局。
//回收舊的吸頂佈局。
recycle();
}
}
//這是是處理第一次開啟時,吸頂佈局已經新增到StickyLayout,但StickyLayout的高依然為0的情況。
if (mStickyLayout.getChildCount() > 0 && mStickyLayout.getHeight() == 0) {
mStickyLayout.requestLayout();
}
//設定mStickyLayout的Y偏移量。
mStickyLayout.setTranslationY(calculateOffset(gAdapter, firstVisibleItem, groupPosition + 1));
}
}
/**
* 判斷是否需要先回收吸頂佈局,如果要回收,則回收吸頂佈局並返回null。
* 如果不回收,則返回吸頂佈局的ViewHolder。
* 這樣做可以避免頻繁的新增和移除吸頂佈局。
*
* @param viewType
* @return
*/
private BaseViewHolder recycleStickyView(int viewType) {
if (mStickyLayout.getChildCount() > 0) {
View view = mStickyLayout.getChildAt(0);
int type = (int) view.getTag(VIEW_TAG_TYPE);
if (type == viewType) {
return (BaseViewHolder) view.getTag(VIEW_TAG_HOLDER);
} else {
recycle();
}
}
return null;
}
/**
* 回收並移除吸頂佈局
*/
private void recycle() {
if (mStickyLayout.getChildCount() > 0) {
View view = mStickyLayout.getChildAt(0);
mStickyViews.put((int) (view.getTag(VIEW_TAG_TYPE)),
(BaseViewHolder) (view.getTag(VIEW_TAG_HOLDER)));
mStickyLayout.removeAllViews();
}
}
/**
* 從快取池中獲取吸頂佈局
*
* @param viewType 吸頂佈局的viewType
* @return
*/
private BaseViewHolder getStickyViewByType(int viewType) {
return mStickyViews.get(viewType);
}
/**
* 計算StickyLayout的偏移量。因為如果下一個組的組頭頂到了StickyLayout,
* 就要把StickyLayout頂上去,直到下一個組的組頭變成吸頂佈局。否則會發生兩個組頭重疊的情況。
*
* @param gAdapter
* @param firstVisibleItem 當前列表顯示的第一個項。
* @param groupPosition 下一個組的組下標。
* @return 返回偏移量。
*/
private float calculateOffset(GroupedRecyclerViewAdapter gAdapter, int firstVisibleItem, int groupPosition) {
int groupHeaderPosition = gAdapter.getPositionForGroupHeader(groupPosition);
if (groupHeaderPosition != -1) {
int index = groupHeaderPosition - firstVisibleItem;
if (mRecyclerView.getChildCount() > index) {
//獲取下一個組的組頭的itemView。
View view = mRecyclerView.getChildAt(index);
float off = view.getY() - mStickyLayout.getHeight();
if (off < 0) {
return off;
}
}
}
return 0;
}
/**
* 獲取當前第一個顯示的item .
*/
private int getFirstVisibleItem() {
int firstVisibleItem = -1;
RecyclerView.LayoutManager layout = mRecyclerView.getLayoutManager();
if (layout != null) {
if (layout instanceof LinearLayoutManager) {
firstVisibleItem = ((LinearLayoutManager) layout).findFirstVisibleItemPosition();
} else if (layout instanceof GridLayoutManager) {
firstVisibleItem = ((GridLayoutManager) layout).findFirstVisibleItemPosition();
} else if (layout instanceof StaggeredGridLayoutManager) {
int[] firstPositions = new int[((StaggeredGridLayoutManager) layout).getSpanCount()];
((StaggeredGridLayoutManager) layout).findFirstVisibleItemPositions(firstPositions);
firstVisibleItem = getMin(firstPositions);
}
}
return firstVisibleItem;
}
private int getMin(int[] arr) {
int min = arr[0];
for (int x = 1; x < arr.length; x++) {
if (arr[x] < min)
min = arr[x];
}
return min;
}
/**
* 是否吸頂
*
* @return
*/
public boolean isSticky() {
return isSticky;
}
/**
* 設定是否吸頂。
*
* @param sticky
*/
public void setSticky(boolean sticky) {
if (isSticky != sticky) {
isSticky = sticky;
if (mStickyLayout != null) {
if (isSticky) {
mStickyLayout.setVisibility(VISIBLE);
updateStickyView();
} else {
recycle();
mStickyLayout.setVisibility(GONE);
}
}
}
}
}複製程式碼
StickyHeaderLayout具有以下的優點:
1、非ItemDecoration。StickyHeaderLayout的懸浮View是一個真實的View,而不是一個簡單的影像,所以它可以懸浮任何的View。這有別於使用ItemDecoration實現懸浮效果的情況。
2、與GroupedRecyclerViewAdapter完美結合。懸浮佈局直接交由Adapter建立和更新,這使得懸浮佈局在顯示上和在處理上(事件監聽、業務邏輯等)都與列表中的item保持一致。你可以把懸浮佈局看做是列表中的一個項。而且GroupedRecyclerViewAdapter支援多種item型別,所以懸浮佈局也可以支援多種item型別。
3、StickyHeaderLayout對懸浮佈局進行快取複用,避免不必要的建立和更新、移除等操作。優化介面的繪製流暢。
4、使用簡單。你只需要使用StickyHeaderLayout包裹RecyclerView,並使用GroupedRecyclerViewAdapter實現兩級列表就可以了。
效果圖: