0. 环境:
电脑:Windows10
Android Studio: 2024.3.2
编程语言: Java
Gradle version:8.11.1
Compile Sdk Version:35
Java 版本:Java11
1. ItemDecoration简单介绍
itemDecoration允许给具体的view添加具体的图画或者layout的偏移。大部分用于给每个item之间画分割线。
调用方法:recyclerView.addItemDecoration()
我们将使用ItemDecoration,来实现吸顶效果
2. 吸顶效果展示
应用场景:城市--省份,姓氏--首字母,列表--首字母 等
3. 实现步骤:
关键代码:
ProvinceDecoration.java
package com.liosen.androidnote;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class ProvinceDecoration extends RecyclerView.ItemDecoration {
private Context context;
private int provinceHeight; // 省份部分高度
private Paint headPaint; // 头部画笔,即 吸顶那部分用到的画笔
private Paint textPaint; // 文字画笔
private Rect textRect; // 文字Rect
public ProvinceDecoration(Context context) {
this.context = context;
this.provinceHeight = dp2px(context, 100); // 设置100,可调整
headPaint = new Paint(); // 实例化头部画笔
headPaint.setColor(Color.GREEN); // 设置头部画笔的颜色
textPaint = new Paint(); // 实例化文字画笔
textPaint.setTextSize(50); // 设置文字画笔的大小
textPaint.setColor(Color.WHITE); // 设置文字画笔的颜色
textRect = new Rect(); // 设置文字画笔必须要Rect
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
if (parent.getAdapter() instanceof ProvinceAdapter) {
ProvinceAdapter adapter = (ProvinceAdapter) parent.getAdapter();
int count = parent.getChildCount(); // 获取当前屏幕item的个数
int left = parent.getPaddingLeft(); // 获取paddingLeft
int right = parent.getWidth() - parent.getPaddingRight(); // 计算right
for (int i = 0; i < count; i++) {
// 获取View
View view = parent.getChildAt(i);
// 获取view的布局位置
int pos = parent.getChildLayoutPosition(view);
// 是否为省份
boolean isProvince = adapter.isProvince(pos);
if (isProvince) {
// 如果为省份,则用画笔画出头部部分:headPaint、文字部分textPain
c.drawRect(left, view.getTop() - provinceHeight, right, view.getTop(), headPaint);
String provinceName = adapter.getProvinceName(pos);
textPaint.getTextBounds(provinceName, 0, provinceName.length(), textRect);
c.drawText(provinceName, left + 10, view.getTop() - provinceHeight / 2 + textRect.height() / 2, textPaint);
} else {
// 如果是城市,则画出分割线
c.drawRect(left, view.getTop() - 1, right, view.getTop(), headPaint);
}
}
}
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (parent.getAdapter() instanceof ProvinceAdapter) {
ProvinceAdapter adapter = (ProvinceAdapter) parent.getAdapter();
// 返回可见区域内,第一个item的position
// 注意!!!如果你的recyclerView被ScrollView或者NestedScrollView 包裹,此处只会返回0;
// 解决方法: 1. 要么移除ScrollView/NestedScrollView
// 2. 布局文件中设置recyclerView: android:nestedScrollingEnabled="false",并且android:layout_height="wrap_content"
// 3. 自己手动计算可见位置
int firstPos = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
// 获取对应position的itemView
View itemView = parent.findViewHolderForAdapterPosition(firstPos).itemView;
int left = parent.getPaddingLeft();
int top = parent.getPaddingTop();
int right = parent.getWidth() - parent.getPaddingRight();
// 当第二个是省份的时候
boolean isProvince = adapter.isProvince(firstPos + 1);
String provinceName = adapter.getProvinceName(firstPos);
if (isProvince) {
// 如果是省份,则需要向上推走 上一个省份
int bottom = Math.min(provinceHeight, itemView.getBottom());
c.drawRect(left, top, right, top + bottom, headPaint);
textPaint.getTextBounds(provinceName, 0, provinceName.length(), textRect);
c.drawText(provinceName, left + 10, top + bottom - provinceHeight / 2 + textRect.height() / 2, textPaint);
} else {
// 如果不是省份,则继续吸顶
c.drawRect(left, top, right, top + provinceHeight, headPaint);
textPaint.getTextBounds(provinceName, 0, provinceName.length(), textRect);
c.drawText(provinceName, left + 10, top + provinceHeight / 2 + textRect.height() / 2, textPaint);
}
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (parent.getAdapter() instanceof ProvinceAdapter) {
ProvinceAdapter adapter = (ProvinceAdapter) parent.getAdapter();
int pos = parent.getChildLayoutPosition(view);
boolean isProvince = adapter.isProvince(pos);
if (isProvince) {
// 如果是省份,则 顶部腾出省份高度,也就是初始化的时候传入的100
outRect.set(0, provinceHeight, 0, 0);
} else {
// 如果不是省份,则画出分割线,这边设置分割线高度为1
outRect.set(0, 1, 0, 0);
}
}
}
/**
* @return dp转px
*/
private int dp2px(Context context, float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale * 0.5f);
}
}
ProvinceAdapter.java(适配器adapter部分,没啥好讲的。基本操作)
package com.liosen.androidnote;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class ProvinceAdapter extends RecyclerView.Adapter<ProvinceAdapter.CityVH> {
private List<CityBean> cityList; // recyclerView数据
private Context context;
public ProvinceAdapter(Context context, List<CityBean> cityList) {
this.context = context;
this.cityList = cityList;
}
/**
* @return 是否为省份
*/
public boolean isProvince(int pos) {
if (pos == 0) {
return true;
} else {
String provinceName = getProvinceName(pos);
String preProvinceName = getProvinceName(pos - 1);
// 当前item和上一个item对比,如果相同,则不是省份;如果不相同,则为新的省份
if (preProvinceName.equals(provinceName)) {
return false;
} else {
return true;
}
}
}
public String getProvinceName(int pos) {
return cityList.get(pos).getProvinceName();
}
@NonNull
@Override
public CityVH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.rv_item_city, null);
return new CityVH(view);
}
@Override
public void onBindViewHolder(@NonNull CityVH holder, int position) {
holder.textView.setText(cityList.get(position).getCityName());
}
@Override
public int getItemCount() {
return cityList == null ? 0 : cityList.size();
}
public class CityVH extends RecyclerView.ViewHolder {
TextView textView;
public CityVH(@NonNull View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.tv);
}
}
}
只要按照我贴的ProvinceDecoration部分代码,就可以实现吸顶功能。至于其他部分,根据自己项目的业务,稍微修改即可。
4. 优化部分
adapter部分,我推荐BaseRecyclerViewAdapterHelper,github:https://github.com/CymChad/BaseRecyclerViewAdapterHelper
5. 源码上传
源码已上传:https://download.csdn.net/download/liosen/91415153
给伸手党
6. 写在最后
至此,我们就简单完成了吸顶效果。UI部分按照项目修改即可