最近使用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);
}
}