14_自定义ItemDecoration实现qq好友列表分组效果
一.先上效果
二.RecyclerView实现简单分组
RecyclerView比较常规的用法,显示多item布局,直接上代码:
public class SectionListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<SectionItem> datas;
private Context context;
public static final int ITEM_TYPE_SECTION = 1000;
public static final int ITEM_TYPE_NORMAL = 1001;
public SectionListAdapter(Context context, List<SectionItem> datas) {
this.context = context;
this.datas = datas;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
RecyclerView.ViewHolder holder = null;
if (viewType == ITEM_TYPE_SECTION) {
View itemView = LayoutInflater.from(context).inflate(R.layout.section_item, parent, false);
holder = new SectionItemHolder(itemView);
} else {
View itemView = LayoutInflater.from(context).inflate(R.layout.group_item, parent, false);
holder = new NormalItemHolder(itemView);
}
return holder;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
SectionItem sectionItem = datas.get(position);
if(getItemViewType(position) == ITEM_TYPE_SECTION) {
SectionItemHolder sectionItemHolder = (SectionItemHolder) holder;
sectionItemHolder.tvSectionName.setText(sectionItem.name);
sectionItemHolder.tvButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, sectionItem.name, Toast.LENGTH_SHORT).show();
}
});
} else {
NormalItemHolder normalItemHolder = (NormalItemHolder) holder;
normalItemHolder.tvGroupName.setText(sectionItem.name);
}
}
@Override
public int getItemCount() {
return datas != null ? datas.size():0;
}
@Override
public int getItemViewType(int position) {
if(datas != null && datas.size() > position) {
if(datas.get(position).isSection) {
return ITEM_TYPE_SECTION;
}
}
return ITEM_TYPE_NORMAL;
}
protected static class SectionItemHolder extends RecyclerView.ViewHolder {
private Button tvButton;
private TextView tvSectionName;
public SectionItemHolder(@NonNull View itemView) {
super(itemView);
tvSectionName = itemView.findViewById(R.id.tv_section_item_name);
tvButton = itemView.findViewById(R.id.tv_button);
}
}
protected static class NormalItemHolder extends RecyclerView.ViewHolder {
TextView tvGroupName;
public NormalItemHolder(@NonNull View itemView) {
super(itemView);
tvGroupName = itemView.findViewById(R.id.tv_group_item_name);
}
}
}
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding dataBinding;
private List<SectionItem> datas = new ArrayList<>();
private SectionListAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
dataBinding.groupList.setLayoutManager(new LinearLayoutManager(this));
datas.add(new SectionItem("group1", "group1", true));
for(int i=0; i < 10; i++) {
datas.add(new SectionItem("item" + i, "group1", false));
}
datas.add(new SectionItem("group2", "group2", true));
for(int i=0; i < 5; i++) {
datas.add(new SectionItem("item" + i, "group2", false));
}
datas.add(new SectionItem("group3", "group3", true));
for(int i=0; i < 20; i++) {
datas.add(new SectionItem("item" + i, "group3", false));
}
mAdapter = new SectionListAdapter(this, datas);
dataBinding.groupList.setAdapter(mAdapter);
}
}
三.自定义ItemDecoration实现吸顶
在ItemDecoration的实现方法中,有两个方法是用于绘制的,一个是onDraw,一个是onDrawOver,onDraw方法中绘制的内容如果和RecyclerView的item有重叠,重叠区域会被item覆盖,常用于在RecyclerView的item周围绘制分割,而在onDrawOver方法中绘制的内容会覆盖在RecyclerView的item之上,因此我们需要在onDrawOver中进行绘制,才能实现吸顶。具体实现步骤如下:
- 重写ItemDecoration的getItemOffsets方法,在getItemOffsets方法中直接调用adapter的createViewHolder创建一个新的分组的ViewHolder,后续会提供给onDrawOver进行绘制,并缓存RecyclerView中所有已经创建的分组ViewHolder
public class SectionDecoration extends RecyclerView.ItemDecoration {
Map<Integer, RecyclerView.ViewHolder> sections = new LinkedHashMap<>();
private RecyclerView.ViewHolder overViewHolder = null;
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
RecyclerView.Adapter adapter = parent.getAdapter();
int position = parent.getChildLayoutPosition(view);
if(adapter != null) {
if(overViewHolder == null) {
overViewHolder = adapter.createViewHolder(null, SectionListAdapter.ITEM_TYPE_SECTION);
}
if(adapter.getItemViewType(position) == SectionListAdapter.ITEM_TYPE_SECTION) {
if(sections.get(position) == null) {
sections.put(position, parent.getChildViewHolder(view));
}
}
}
}
}
- 在绘制吸顶之前,我们需要先找到需要吸顶的目标ViewHolder,目标ViewHolder有多大,overViewHolder就有多大,而overViewHolder的位置始终是在RecyclerView的最顶部,因此问题的关键在于怎么寻找目标ViewHolder的position,我们通过一张图来说明怎么寻找目标ViewHolder的position,找到目标ViewHolder的position之后,从缓存中取出Viewholder即为目标ViewHolder。
public class SectionDecoration extends RecyclerView.ItemDecoration {
Map<Integer, RecyclerView.ViewHolder> sections = new LinkedHashMap<>();
private RecyclerView.ViewHolder overViewHolder = null;
...
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
RecyclerView.Adapter adapter = parent.getAdapter();
if(adapter != null) {
int count = parent.getChildCount();
int firstVisibleItemPosition = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
int position = firstVisibleItemPosition;
if (position > 0) {
for (int i = 0; i < count; i++) {
View child = parent.getChildAt(i);
position = parent.getChildLayoutPosition(child);
if (child.getBottom() >= (parent.getPaddingTop()) && adapter.getItemViewType(position + 1) == SectionListAdapter.ITEM_TYPE_SECTION) {
break;
}
}
}
int sectionPosition = -1;
for (int i = 0; i <= position; i++) {
if(adapter.getItemViewType(i) == SectionListAdapter.ITEM_TYPE_SECTION) {
sectionPosition = i;
}
}
RecyclerView.ViewHolder targetViewHolder = null;
if(sectionPosition != -1) {
targetViewHolder = sections.get(sectionPosition);
}
}
}
}
- 绘制overViewHolder
@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(canvas, parent, state);
RecyclerView.Adapter adapter = parent.getAdapter();
if(adapter != null) {
int count = parent.getChildCount();
int firstVisibleItemPosition = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
int position = firstVisibleItemPosition;
if (position > 0) {
for (int i = 0; i < count; i++) {
View child = parent.getChildAt(i);
position = parent.getChildLayoutPosition(child);
if (adapter.getItemViewType(position + 1) == SectionListAdapter.ITEM_TYPE_SECTION) {
break;
}
}
}
int sectionPosition = -1;
for (int i = 0; i <= position; i++) {
if(adapter.getItemViewType(i) == SectionListAdapter.ITEM_TYPE_SECTION) {
sectionPosition = i;
}
}
RecyclerView.ViewHolder targetViewHolder = null;
if(sectionPosition != -1) {
targetViewHolder = sections.get(sectionPosition);
}
if(targetViewHolder != null) {
int width = targetViewHolder.itemView.getMeasuredWidth();
int height = targetViewHolder.itemView.getMeasuredHeight();
overViewHolder.itemView.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
int top = parent.getPaddingTop();
int left = parent.getPaddingLeft();
overViewHolder.itemView.layout(left + parent.getLeft(), top + parent.getTop(), left + parent.getLeft() + width, top + parent.getTop() + height);
drawOverViewHolder(canvas, overViewHolder.itemView, parent, left, top);
}
}
}
private void drawOverViewHolder(Canvas canvas, View overSectionView, RecyclerView parent, int left, int top) {
canvas.save();
canvas.clipRect(left, parent.getPaddingTop(), left + overSectionView.getMeasuredWidth(), top + overSectionView.getMeasuredHeight());
canvas.translate(left, top);
overSectionView.draw(canvas);
canvas.restore();
}
- 我们先把它添加到RecyclerView中看下效果:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
dataBinding.groupList.setLayoutManager(new LinearLayoutManager(this));
dataBinding.groupList.addItemDecoration(new SectionDecoration());
...
}
可以看到吸顶的ViewHolder已经被绘制出来了,但是吸顶item的数据没有动态变化,并且下一个需要吸顶的ViewHolder滑动到当前吸顶的ViewHolder位置时,不会往上推,并且吸顶ViewHolder上的按钮是不可点击状态。
- 动态改变吸顶ViewHolder的数据
adapter.onBindViewHolder(overViewHolder, sectionPosition);
int width = targetViewHolder.itemView.getMeasuredWidth();
int height = targetViewHolder.itemView.getMeasuredHeight();
overViewHolder.itemView.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
- 什么时候触发上推
找到下一个需要吸顶的ViewHolder的前一个ViewHolder的itemView,如果没找到,那么说明不需要上推,如果找到了:
if(itemView.getBottom() - recyclerView.getPaddingTop() <= 当前吸顶item.getMeasuredHeight()) {
开始上推
}
int top = parent.getPaddingTop();
int left = parent.getPaddingLeft();
RecyclerView.ViewHolder sectionTailHolder = null;
if(position > 0) {
sectionTailHolder = parent.findViewHolderForLayoutPosition(position);
} else {
if(adapter.getItemViewType(1) == SectionListAdapter.ITEM_TYPE_SECTION) {
sectionTailHolder = parent.findViewHolderForLayoutPosition(position);
}
}
if(sectionTailHolder != null && (sectionTailHolder.itemView.getBottom() - top) <= height) {
//上推
}
overViewHolder.itemView.layout(left + parent.getLeft(), top + parent.getTop(), left + parent.getLeft() + width, top + parent.getTop() + height);
drawOverViewHolder(canvas, overViewHolder.itemView, parent, left, top);
- 实现吸顶ViewHolder上推
int top = parent.getPaddingTop();
int left = parent.getPaddingLeft();
RecyclerView.ViewHolder sectionTailHolder = null;
if(position > 0) {
sectionTailHolder = parent.findViewHolderForLayoutPosition(position);
} else {
if(adapter.getItemViewType(1) == SectionListAdapter.ITEM_TYPE_SECTION) {
sectionTailHolder = parent.findViewHolderForLayoutPosition(position);
}
}
if(sectionTailHolder != null && (sectionTailHolder.itemView.getBottom() - top) <= height) {
//上推
top = sectionTailHolder.itemView.getBottom() - overViewHolder.itemView.getMeasuredHeight();
}
overViewHolder.itemView.layout(left + parent.getLeft(), top + parent.getTop(), left + parent.getLeft() + width, top + parent.getTop() + height);
drawOverViewHolder(canvas, overViewHolder.itemView, parent, left, top);
- 处理点击事件
吸顶的itemView中的button不能点击的原因在于,吸顶的itemView只是被摆放和绘制到了屏幕上,并没有attachToWindow,而View的点击事件是从window开始分发的,所以我们可以考虑把吸顶的itemView添加到一个已经attachToWindow的ViewGroup中,我们把它添加到RecyclerView的父View中即可。
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
RecyclerView.Adapter adapter = parent.getAdapter();
int position = parent.getChildLayoutPosition(view);
if(adapter != null) {
if(overViewHolder == null) {
ViewGroup recyclerViewParent = (ViewGroup) parent.getParent();
overViewHolder = adapter.createViewHolder(recyclerViewParent, SectionListAdapter.ITEM_TYPE_SECTION);
overViewHolder.itemView.setClipBounds(new Rect(0, 0, 0, 0));
recyclerViewParent.addView(overViewHolder.itemView);
}
if(adapter.getItemViewType(position) == SectionListAdapter.ITEM_TYPE_SECTION) {
if(sections.get(position) == null) {
sections.put(position, parent.getChildViewHolder(view));
}
}
}
}
四.完整代码
public class SectionDecoration extends RecyclerView.ItemDecoration {
Map<Integer, RecyclerView.ViewHolder> sections = new LinkedHashMap<>();
private RecyclerView.ViewHolder overViewHolder = null;
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
RecyclerView.Adapter adapter = parent.getAdapter();
int position = parent.getChildLayoutPosition(view);
if(adapter != null) {
if(overViewHolder == null) {
ViewGroup recyclerViewParent = (ViewGroup) parent.getParent();
overViewHolder = adapter.createViewHolder(recyclerViewParent, SectionListAdapter.ITEM_TYPE_SECTION);
overViewHolder.itemView.setClipBounds(new Rect(0, 0, 0, 0));
recyclerViewParent.addView(overViewHolder.itemView);
}
if(adapter.getItemViewType(position) == SectionListAdapter.ITEM_TYPE_SECTION) {
if(sections.get(position) == null) {
sections.put(position, parent.getChildViewHolder(view));
}
}
}
}
@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(canvas, parent, state);
RecyclerView.Adapter adapter = parent.getAdapter();
if(adapter != null) {
int count = parent.getChildCount();
int firstVisibleItemPosition = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
int position = firstVisibleItemPosition;
if (position > 0) {
for (int i = 0; i < count; i++) {
View child = parent.getChildAt(i);
position = parent.getChildLayoutPosition(child);
if (child.getBottom() >= (parent.getPaddingTop()) && adapter.getItemViewType(position + 1) == SectionListAdapter.ITEM_TYPE_SECTION) {
break;
}
}
}
int sectionPosition = -1;
for (int i = 0; i <= position; i++) {
if(adapter.getItemViewType(i) == SectionListAdapter.ITEM_TYPE_SECTION) {
sectionPosition = i;
}
}
RecyclerView.ViewHolder targetViewHolder = null;
if(sectionPosition != -1) {
targetViewHolder = sections.get(sectionPosition);
}
if(targetViewHolder != null) {
adapter.onBindViewHolder(overViewHolder, sectionPosition);
int width = targetViewHolder.itemView.getMeasuredWidth();
int height = targetViewHolder.itemView.getMeasuredHeight();
overViewHolder.itemView.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
int top = parent.getPaddingTop();
int left = parent.getPaddingLeft();
RecyclerView.ViewHolder sectionTailHolder = null;
if(position > 0) {
sectionTailHolder = parent.findViewHolderForLayoutPosition(position);
} else {
if(adapter.getItemViewType(1) == SectionListAdapter.ITEM_TYPE_SECTION) {
sectionTailHolder = parent.findViewHolderForLayoutPosition(position);
}
}
if(sectionTailHolder != null && (sectionTailHolder.itemView.getBottom() - top) <= height) {
//上推
top = sectionTailHolder.itemView.getBottom() - overViewHolder.itemView.getMeasuredHeight();
}
overViewHolder.itemView.layout(left + parent.getLeft(), top + parent.getTop(), left + parent.getLeft() + width, top + parent.getTop() + height);
drawOverViewHolder(canvas, overViewHolder.itemView, parent, left, top);
}
}
}
private void drawOverViewHolder(Canvas canvas, View overSectionView, RecyclerView parent, int left, int top) {
canvas.save();
canvas.clipRect(left, parent.getPaddingTop(), left + overSectionView.getMeasuredWidth(), top + overSectionView.getMeasuredHeight());
canvas.translate(left, top);
overSectionView.draw(canvas);
canvas.restore();
}
}
五.总结
RecyclerView实现item吸顶的突破口在于ItemDecoration的onDrawOver方法,至于怎么去绘制吸顶item以及处理点击事件,就是仁者见仁、智者见智了。