最近使用app時發現一個RecycleView滑動過程中組佈局吸頂的效果, 記得以前學習ListView的時候也見過類似的效果,由於工作中沒有真正使用過雖然那會看懂了,但是現在一點印象沒有了。週末在家搜索了一下發現實現方案有幾種,找了一種實現效果容易理解的把代碼消化了一遍,順便記錄一下分析過程。
效果圖
抽象類 ItemDecoration 的幾個方法
public abstract static class ItemDecoration {
/** 該方法會在RecyclerView的ItemView的內容繪製之前繪製,會顯示在ItemView的底層 */
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
onDraw(c, parent);
}
/**
* 該方法會在RecyclerView的ItemView內容繪製之後繪製,會顯示在ItemView的上層
*/
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
@NonNull State state) {
onDrawOver(c, parent);
}
/**
指定當前View的(left, top, right, bottom) 一般是修改top屬性,給groupView佈局的繪製留出空間
*/
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
View組件顯示的內容通過cache機制保存爲bitmap,使用到的API有
void setDrawingCacheEnabled(boolean flag)
Bitmap getDrawingCache(boolean autoScale)
void buildDrawingCache(boolean autoScale)
void destroyDrawingCache()
我們要獲取View的cache先要通過setDrawingCacheEnable方法把cache開啓,然後再調用getDrawingCache方法就可以獲得view的cache圖片了。buildDrawingCache方法可以不用調用,因爲調用getDrawingCache方法時,若果 cache沒有建立,系統會自動調用buildDrawingCache方法生成cache。若果要更新cache, 必須要調用destoryDrawingCache方法把舊的cache銷燬,才能建立新的。
當調用setDrawingCacheEnabled方法設置爲false,系統也會自動把原來的cache銷燬。
源碼部分直接貼出代碼, 帶有詳細分析
public class SectionDecoration extends RecyclerView.ItemDecoration {
private PinGroupListener mPinGroupListener;
/** 懸浮欄高度 */
private int mGroupHeight = 80;
public SectionDecoration(PinGroupListener listener) {
this.mPinGroupListener = listener;
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
// 得到添加的分割線View的位置
int pos = parent.getChildAdapterPosition(view);
String groupId = getGroupName(pos);
if (TextUtils.isEmpty(groupId)) {
return;
}
// 只有是同一組的第一個才顯示懸浮欄, 即 0 - itemView.top 之間的區域顯示懸浮欄
if (isFirstInGroup(pos)) {
outRect.top = mGroupHeight;
}
}
/**
* 獲取組名
*/
private String getGroupName(int position) {
return mPinGroupListener == null ? null : mPinGroupListener.getGroupName(position);
}
/**
* 判斷是不是同一組第一個ItemView
*
* @return
*/
private boolean isFirstInGroup(int pos) {
return pos == 0 || !TextUtils.equals(getGroupName(pos - 1), getGroupName(pos));
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
// 獲取數據源的數目
int itemCount = state.getItemCount();
// 當前屏幕顯示的Item數目
int childCount = parent.getChildCount();
// 得出距離左邊和右邊的padding
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
//
String preGroupName;
String currentGroupName = null;
// 開始繪製當前屏幕的ItemView的GroupView
for (int i = 0; i < childCount; i++) {
View view = parent.getChildAt(i);
/** 該position是給定子視圖對應的適配器位置 */
int position = parent.getChildAdapterPosition(view);
preGroupName = currentGroupName;
currentGroupName = getGroupName(position);
// 是否是同一組
// 1、️在某次上滑的過程中, 當第一個可見的itemView是該組最後一個itemView時,和groupView將會有重合部分, preGroupName = null
if (currentGroupName == null || TextUtils.equals(currentGroupName, preGroupName)) {
continue;
}
int viewBottom = view.getBottom();
// top決定當前頂部第一個懸浮Group的位置
int top = Math.max(mGroupHeight, view.getTop());
int nextPosition = position + 1;
if (nextPosition < itemCount) {
String nextGroupName = getGroupName(nextPosition);
// 下一組的第一個View接近頭部
// 2、當頂部的groupView和該組最後一個itemView重合時, viewBottom = top(即 mGroupHeight), 繼續上滑時,viewBottom < top (即 mGroupHeight)
if (!TextUtils.equals(currentGroupName, nextGroupName) && viewBottom < top) {
top = viewBottom; // top - mGroupHeight < 0 -> 部分滑出屏幕
}
}
// 根據position獲取懸浮View
View groupView = getGroupView(position);
if (groupView == null) {
return;
}
drawGroupView(c, left, right, view, top, groupView);
}
}
private void drawGroupView(@NonNull Canvas c, int left, int right, View view, int top, View groupView) {
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mGroupHeight);
groupView.setLayoutParams(layoutParams);
groupView.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
// 指定groupView的高度、寬度
groupView.layout(0, 0, right, mGroupHeight);
// View組件顯示的內容通過cache機制保存爲bitmap
groupView.setDrawingCacheEnabled(true);
Bitmap bitmap = groupView.getDrawingCache();
// 系統會自動把原來的cache銷燬
view.setDrawingCacheEnabled(false);
c.drawBitmap(bitmap, left, top - mGroupHeight, null);
}
private View getGroupView(int position) {
return mPinGroupListener == null ? null : mPinGroupListener.getGroupView(position);
}
public static class Builder {
SectionDecoration mSectionDecoration;
private Builder(PinGroupListener listener) {
mSectionDecoration = new SectionDecoration(listener);
}
public static Builder init(PinGroupListener listener) {
return new Builder(listener);
}
/**
* 設置group高度
*/
public Builder groupHeight(int groupHeight) {
mSectionDecoration.mGroupHeight = groupHeight;
return this;
}
public SectionDecoration build() {
return mSectionDecoration;
}
}
public interface PinGroupListener {
/** 獲取每一組組名 */
String getGroupName(int position);
/** 獲取組View */
View getGroupView(int position);
}
}