RecyclerView粘性頭部
1.實現原理:在外部添加一個與RecyclerView對齊的headerView,動態添加需要展示的header
2.支持線性和網格佈局
3.支持item添加和刪除(無需再次排序)
博主文筆太菜,不想多說,直接上項目鏈接
github: https://github.com/PPQingZhao/StickyHeaderDemo
需要的夥伴自行看源碼,主要實現在 StickyHeaderScrollerListener 滑動監聽類中
下面貼出關鍵代碼:
package com.pp.stickyheaderdemo.adapter;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import com.pp.stickyheaderdemo.mutilitem.StickyHeader;
/**
* @author qing
* 滑動監聽
* 用於實現粘性頭部
*/
public class StickyHeaderScrollerListener extends RecyclerView.OnScrollListener {
private static final String TAG = "StickyHeaderScroller";
private final StickHeaderContainer mHeaderContainer;
private final SparseArray<RecyclerView.ViewHolder> mHolderMap;
public StickyHeaderScrollerListener(StickHeaderContainer container) {
if (null == container) {
throw new RuntimeException("StickHeaderContainer must not be null.");
}
this.mHeaderContainer = container;
mHolderMap = new SparseArray<>();
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (null == mHeaderAdapter) {
throw new RuntimeException("StickyHeaderAdapter must not be null.");
}
if (dy > 0) {
int headerHeight = mHeaderContainer.getMeasuredHeight();
// header top邊下的view
View viewTop = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, 1);
// 獲取view 在列表中的位置
int positionTop = recyclerView.getChildAdapterPosition(viewTop);
StickyHeader topHeaderItem = mHeaderAdapter.getHeaderItem(positionTop);
boolean isHeaderTop = null != topHeaderItem && topHeaderItem.isHeader();
// header交換時機: viewTop是header 並且 viewTop.getTop() <= 0
if (isHeaderTop && viewTop.getTop() <= 0) {
if (positionTop != mHeaderContainer.getHeaderPosition()) {
// 更新old header記錄
mHeaderContainer.setOldHeaderPosition(mHeaderContainer.getHeaderPosition());
// 更新當前header記錄
mHeaderContainer.setHeaderPosition(positionTop);
// 更新map , key:當前header value: 當前header的前一個header
mHeaderContainer.putPreviousValue(mHeaderContainer.getOldHeaderPosition());
RecyclerView.ViewHolder holder = getHolder(mHeaderContainer.getHeaderPosition());
if (null != holder && null == holder.itemView.getParent()) {
// 更新headerview
addHeaderView(holder);
}
}
}
// header bottom邊下的view
View viewBottom = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, headerHeight);
// 獲取view 在列表中的位置
int positionBottom = recyclerView.getChildAdapterPosition(viewBottom);
StickyHeader bottomHeaderItem = mHeaderAdapter.getHeaderItem(positionBottom);
boolean isHeaderBottom = null != bottomHeaderItem && bottomHeaderItem.isHeader();
int viewBottomTop = null == viewBottom ? 0 : viewBottom.getTop();
if (isHeaderBottom && 0 <= viewBottomTop && viewBottomTop <= headerHeight) {
mHeaderContainer.setHeaderTranslationY(viewBottomTop - headerHeight);
} else {
mHeaderContainer.setHeaderTranslationY(0);
}
} else if (dy <= 0) {
int headerHeight = mHeaderContainer.getMeasuredHeight();
View viewUnder = null;
// dy 等於0時,取mHeaderContainer top邊下的item,通常是 刪除/添加 itme時觸發
if (0 == dy) {
viewUnder = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, 1);
} else {
viewUnder = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, headerHeight);
}
// 獲取view 在列表中的位置
int position = recyclerView.getChildAdapterPosition(viewUnder);
StickyHeader headerItem = mHeaderAdapter.getHeaderItem(position);
boolean isHeader = null != headerItem && headerItem.isHeader();
// header交換時機: viewUnder 是header 並且 viewUnder.getTop() > 0
if (isHeader && viewUnder.getTop() > 0) {
// 獲取當前position header的 前一個header
Integer previousHeaderPosition = mHeaderContainer.getPreviousHeaderPosition(position);
if (null != previousHeaderPosition
&& previousHeaderPosition != mHeaderContainer.getHeaderPosition()) {
// 更新old header 記錄
mHeaderContainer.setOldHeaderPosition(mHeaderContainer.getHeaderPosition());
// 更新當前header 記錄
mHeaderContainer.setHeaderPosition(previousHeaderPosition);
RecyclerView.ViewHolder holder = getHolder(mHeaderContainer.getHeaderPosition());
if (null != holder && null == holder.itemView.getParent()) {
// 更新headerview
addHeaderView(holder);
}
}
}
// header bottom邊下的view
View viewBottom = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, headerHeight);
// 獲取view 在列表中的位置
int positionBottom = recyclerView.getChildAdapterPosition(viewBottom);
StickyHeader bottomHeaderItem = mHeaderAdapter.getHeaderItem(positionBottom);
boolean isHeaderBottom = null != bottomHeaderItem && bottomHeaderItem.isHeader();
int viewBottomTop = null == viewBottom ? 0 : viewBottom.getTop();
if (isHeaderBottom && 0 <= viewBottomTop && viewBottomTop <= headerHeight) {
mHeaderContainer.setHeaderTranslationY(viewBottomTop - headerHeight);
} else {
mHeaderContainer.setHeaderTranslationY(0);
}
}
// 主要解決:mHeaderContainer 未完全初始化就已經添加headerView,此時headerView是沒有寬高信息的,需要重繪
if (View.VISIBLE == mHeaderContainer.getVisibility()
&& (0 == mHeaderContainer.getHeight()
|| 0 == mHeaderContainer.getWidth())) {
mHeaderContainer.requestLayout();
}
}
private RecyclerView.ViewHolder getHolder(int headerPosition) {
StickyHeader headerItem = mHeaderAdapter.getHeaderItem(headerPosition);
if (null == headerItem || !headerItem.isHeader()) {
return null;
}
// 獲取 header type
int headerType = mHeaderAdapter.getHeaderType(headerPosition);
// 從緩存中獲取 header 對應holder
RecyclerView.ViewHolder holder = mHolderMap.get(headerType);
if (null == holder) {
// 創建holder
holder = mHeaderAdapter.createHeaderViewHolder(mHeaderContainer, headerType);
// 緩存 holder
mHolderMap.put(headerType, holder);
}
// bind holder
mHeaderAdapter.bindHeaderViewHolder(holder, mHeaderContainer.getHeaderPosition());
return holder;
}
private void addHeaderView(RecyclerView.ViewHolder holder) {
if (null == holder || null != holder.itemView.getParent()) {
return;
}
mHeaderContainer.removeHeader();
mHeaderContainer.addHeader(holder.itemView);
Log.i(TAG, "add header View");
}
private BaseHeaderAdapter mHeaderAdapter;
public <Adapter extends BaseHeaderAdapter> void setHeaderAdapter(Adapter adapter) {
this.mHeaderAdapter = adapter;
}
/**
* 刪除條目,刷新header
*
* @param index
*/
public void notifyPreviousRemove(int index) {
// 是否是當前header
boolean curHeader = mHeaderContainer.getHeaderPosition() == index;
// 刷新 當前headerposition
mHeaderContainer.notifyHeaderPoisitionRemove(index);
// 刷新記錄的所有header
mHeaderContainer.notifyPreviousRemove(index);
// 刪除的條目是當前header,需要更新header 條目
if (curHeader) {
RecyclerView.ViewHolder holder = getHolder(mHeaderContainer.getHeaderPosition());
if (null != holder && null == holder.itemView.getParent()) {
addHeaderView(holder);
}
}
}
/**
* 添加條目,刷新header
*
* @param index
*/
public void notifyPreviousInsert(int index) {
StickyHeader headerItem = mHeaderAdapter.getHeaderItem(index);
// 是否插入header
boolean isInsertHeader = null != headerItem && headerItem.isHeader();
// 刷新 當前headerposition
mHeaderContainer.notifyHeaderPoisitionInsert(index, isInsertHeader);
// 刷新記錄的所有header
mHeaderContainer.notifyPreviousInsert(index, isInsertHeader);
}
public interface BaseHeaderAdapter<VH extends RecyclerView.ViewHolder> {
/**
* 獲取header type
*
* @param position
* @return
*/
int getHeaderType(int position);
/**
* 創建header holder
*
* @param parent
* @param headerType
* @return
*/
VH createHeaderViewHolder(ViewGroup parent, int headerType);
/**
* 綁定header view
*
* @param holder
* @param position
*/
void bindHeaderViewHolder(VH holder, int position);
/**
* 獲取header條目
*
* @param position
* @param <T>
* @return
*/
<T extends StickyHeader> T getHeaderItem(int position);
}
}
package com.pp.stickyheaderdemo.adapter;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
/**
* @author qing
* 粘性頭部
*/
public class StickHeaderContainer extends ViewGroup {
private final HeaderView mHeaderView;
public StickHeaderContainer(@NonNull Context context) {
this(context, null);
}
public StickHeaderContainer(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public StickHeaderContainer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mHeaderView = new HeaderView(getContext());
mHeaderView.setLayoutParams(new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
/**
* 測量設置寬高信息
* <p>
* 這裏我們希望StickHeaderContainer只需要包裹住HeaderView
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
if (childCount > 1) {
throw new IllegalArgumentException("StickHeaderContainer only allows one child.");
}
if (childCount == 0) {
setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
return;
}
// 獲取child
View child = getChildAt(0);
// 測量單個child(考慮外邊距)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 獲取child 的佈局參數(此時包含外邊距)
MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
// 期望的寬高 (取child最大寬高)
int desireWidth = child.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
int desireHeight = child.getHeight() + layoutParams.topMargin + layoutParams.bottomMargin;
// 考慮本身內邊距
desireWidth += getPaddingLeft() + getPaddingRight();
desireHeight += getPaddingTop() + getPaddingBottom();
// 比較建議最小值和期望值,並取最大值
desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());
// 設置最終測量值 (view.getMeasureWidth()和view.getMeasureHeight()的值就是從這裏確定的)
setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() == 0) {
return;
}
View child = getChildAt(0);
MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
int cLeft = getPaddingLeft() + layoutParams.leftMargin;
int cTop = getPaddingTop() + layoutParams.topMargin;
int cRight = cLeft + child.getMeasuredWidth();
int cBottom = cTop + child.getMeasuredHeight();
/* 設置child在parent中的位置
分別保存在child的 mLeft,mTop,mBottom,mRight中,view.getWidth()的值等於 mRight - mLeft,
所以在執行完view.layout()後,view.getWidth()和view.getHeight();纔有值
*/
child.layout(cLeft, cTop, cRight, cBottom);
}
/**
* 重寫構造默認佈局參數方法
* 因爲在onMeasure()中使用了 measureChildWithMargins(),這要求view的佈局參數是MarginLayoutParams
*
* @return
*/
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
public void putPreviousValue(int previousPosition) {
// key:當前header value: 當前header的前一個header
mHeaderView.putPreviousValue(previousPosition);
}
public void notifyPreviousRemove(int index) {
mHeaderView.notifyPreviousRemove(index);
}
public void notifyHeaderPoisitionRemove(int index) {
mHeaderView.notifyHeaderPoisitionRemove(index);
}
public void notifyPreviousInsert(int index, boolean isInsertHeader) {
mHeaderView.notifyPreviousInsert(index, isInsertHeader);
}
public void notifyHeaderPoisitionInsert(int index, boolean isInsertHeader) {
mHeaderView.notifyHeaderPoisitionInsert(index, isInsertHeader);
}
public void addHeader(View header) {
if (null == mHeaderView.getParent()) {
addView(mHeaderView);
}
mHeaderView.addView(header);
}
public void removeHeader() {
mHeaderView.removeAllViews();
}
public int getHeaderPosition() {
return mHeaderView.getHeaderPosition();
}
public void setHeaderPosition(int headerPosition) {
mHeaderView.setHeaderPosition(headerPosition);
}
public int getOldHeaderPosition() {
return mHeaderView.getOldHeaderPosition();
}
public void setOldHeaderPosition(int oldHeaderPosition) {
mHeaderView.setOldHeaderPosition(oldHeaderPosition);
}
public Integer getPreviousHeaderPosition(int headerPosition) {
return mHeaderView.getPreviousHeaderPosition(headerPosition);
}
public void setHeaderTranslationY(float translationY) {
mHeaderView.setTranslationY(translationY);
}
public void setHeaderTranslationX(float translationX) {
mHeaderView.setTranslationX(translationX);
}
static class HeaderView extends FrameLayout {
// key:當前header value: 當前header的前一個header
private final SparseArray<Integer> headerPositionMap;
private HeaderView(Context context) {
super(context);
headerPositionMap = new SparseArray<>();
}
public void putPreviousValue(int previousPosition) {
// key:當前header value: 當前header的前一個header
headerPositionMap.put(getHeaderPosition(), previousPosition);
}
public Integer getPreviousHeaderPosition(int headerPosition) {
return headerPositionMap.get(headerPosition);
}
/**
* 記錄當前headerView對應數據源中的位置
*/
private int headerPosition = -1;
public int getHeaderPosition() {
return headerPosition;
}
public void setHeaderPosition(int headerPosition) {
this.headerPosition = headerPosition;
}
/**
* 記錄 old headerPosition
*/
private int oldHeaderPosition = headerPosition;
public int getOldHeaderPosition() {
return oldHeaderPosition;
}
public void setOldHeaderPosition(int oldHeaderPosition) {
this.oldHeaderPosition = oldHeaderPosition;
}
public void notifyHeaderPoisitionRemove(int index) {
if (getHeaderPosition() < index) {
// do nothing
} else if (getHeaderPosition() == index) {
setHeaderPosition(headerPositionMap.get(getHeaderPosition()));
} else if (getHeaderPosition() > index) {
setHeaderPosition(getHeaderPosition() - 1);
}
}
public void notifyPreviousRemove(int index) {
SparseArray<Integer> cloneMap = headerPositionMap.clone();
headerPositionMap.clear();
for (int i = 0; i < cloneMap.size(); i++) {
int key = cloneMap.keyAt(i);
Integer previousKey = cloneMap.valueAt(i);
// index 小於 key和previousKey
if (index < previousKey && index < key) {
if (previousKey == 0) {
headerPositionMap.put(key - 1, 0);
} else {
headerPositionMap.put(key - 1, previousKey - 1);
}
// index < previousKey
} else if (index == previousKey) {
Integer deletedValue = cloneMap.get(previousKey);
headerPositionMap.put(key - 1, deletedValue);
// previousKey < index < key
} else if (previousKey < index && index < key) {
headerPositionMap.put(key - 1, previousKey);
} else if (index == key) {
int valueIndex = cloneMap.indexOfValue(key);
if (-1 != valueIndex) {
int keyValue = cloneMap.keyAt(valueIndex);
headerPositionMap.put(keyValue, previousKey);
cloneMap.put(keyValue, previousKey);
}
} else if (index > key) {
// do nothing
}
}
}
public void notifyPreviousInsert(int index, boolean isInsertHeader) {
SparseArray<Integer> cloneMap = headerPositionMap.clone();
headerPositionMap.clear();
for (int i = 0; i < cloneMap.size(); i++) {
int key = cloneMap.keyAt(i);
Integer previousKey = cloneMap.valueAt(i);
if (index > key && index > previousKey) {
headerPositionMap.put(key, previousKey);
if (isInsertHeader) {
headerPositionMap.put(index, key);
}
} else if (previousKey < index && index < key) {
if (isInsertHeader) {
headerPositionMap.put(index, previousKey);
headerPositionMap.put(key + 1, index);
} else {
headerPositionMap.put(key + 1, previousKey);
}
} else if (index < previousKey && index < key) {
headerPositionMap.put(key + 1, previousKey + 1);
if (isInsertHeader) {
// 獲取 previousKey的前一個header
Integer value = cloneMap.get(previousKey);
headerPositionMap.put(index, value);
headerPositionMap.put(previousKey + 1, index);
}
}
}
}
/**
* 插入item刷新headerposition
*
* @param index
* @param isInsertHeader
*/
public void notifyHeaderPoisitionInsert(int index, boolean isInsertHeader) {
if (index > getHeaderPosition()) {
// do nothing
} else if (index <= getHeaderPosition()) {
setHeaderPosition(getHeaderPosition() + 1);
}
}
}
}
/**
* @author qing
*/
public interface StickyHeader {
/**
* 頭部類型
*
* @return
*/
int getHeaderType();
/**
* 是否是header
*
* @return
*/
boolean isHeader();
}