Android 自定义UI 实战 03 RecyclerView 吸顶 列表实现
Android 自定义UI 实战 03 RecyclerView 吸顶 列表实现
文章目录
- Android 自定义UI 实战 03 RecyclerView 吸顶 列表实现
- 前言
- 一、吸顶效果准备工作
-
- 1、创建item,和item 实现
- 2、创建一个 Star
- 3、创建一个 StarAdapter
- 4、Activity 完整代码
- 二、吸顶功能实现
-
- 1、自定义 ItemDecoration
- 2、StarDecoration 初始化
- 3、重写 getItemOffsets() 方法
- 4、绘制头部预留空间
- 5、重写 onDraw() 绘制头部
- 6、绘制吸顶效果
- 7、StarDecoration 完整代码
- 总结
前言
使用纯代码 加 注释的方式,可以更快的理解源码
如果你喜欢,请点个赞,后期会不断的深入讲解
一、吸顶效果准备工作
在绘制吸顶效果之前,需要先实现一个RecyclerView功能列表
1、创建item,和item 实现
新建一个item 的Layout 文件
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/user_name" android:text="姓名" android:gravity="center" android:layout_width="match_parent" android:layout_height="44dp" android:textSize="20sp" tools:ignore="MissingConstraints" /></RelativeLayout>
在 RecyclerView 中引用item
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/main_rv" android:layout_width="match_parent" android:layout_height="match_parent" tools:listitem="@layout/item" /></LinearLayout>
2、创建一个 Star
public class Star { String userName; String groupName; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getGroupName() { return groupName; } public void setGroupName(String groupName) { this.groupName = groupName; } public Star(String userName, String groupName) { this.userName = userName; this.groupName = groupName; }}
3、创建一个 StarAdapter
在StarAdapter 中实现 RecyclerView 功能
public class StarAdapter extends RecyclerView.Adapter<StarAdapter.StarViewHolder> { private Context mContext; private List<Star> starList; private LayoutInflater inflater; public StarAdapter(Context context, List<Star> stars){ this.mContext = context; this.starList = stars; this.inflater = LayoutInflater.from(mContext); } @NonNull @Override public StarViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = inflater.inflate(R.layout.item, null); return new StarViewHolder(view); } @Override public void onBindViewHolder(@NonNull StarViewHolder holder, int position) { holder.textView.setText(starList.get(position).getUserName());if (position %2 == 0){ holder.textView.setBackgroundColor(Color.YELLOW);}else { holder.textView.setBackgroundColor(Color.RED);} } @Override public int getItemCount() { return starList.size(); } public class StarViewHolder extends RecyclerView.ViewHolder {private TextView textView; public StarViewHolder(@NonNull View itemView) { super(itemView); textView = itemView.findViewById(R.id.user_name); } }}
4、Activity 完整代码
public class MainActivity extends AppCompatActivity { private RecyclerView recyclerView; private StarAdapter starAdapter; private List<Star> starList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); recyclerView = findViewById(R.id.main_rv); starAdapter = new StarAdapter(this, starList); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(starAdapter); } private void init(){ starList = new ArrayList<>(); for (int i = 0; i < 4; i++) { for (int j = 0; j < 20; j++) { if (i % 2 ==0){ starList.add(new Star("小明" + j, "快乐家族" + i)); }else { starList.add(new Star("王美丽" + j, "嗨嗨家族" + i)); } } } }}
上面源码中,我写了一个嵌套的for 循环,用来实现数据的赋值,显示为4组,每组组20条数据。代码运行效果如下:
二、吸顶功能实现
1、自定义 ItemDecoration
在 StarDecoration 中,我写了一个分辩率的转换方法
public class StarDecoration extends RecyclerView.ItemDecoration { / * 分辩率 * @param context * @param dpValue * @return */ private int dp2px(Context context, float dpValue){ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics()); } }
2、StarDecoration 初始化
// 分组栏的高度 private int headerHeight; private int valueHeight = 50; private Paint headPaint; private Paint drawTextPaint; private Rect textRect; public StarDecoration(Context context){ // 顶部吸顶栏的高度 headerHeight = dp2px(context, valueHeight);// 每一组的头部的Paint headPaint = new Paint(); headPaint.setColor(Color.RED); drawTextPaint = new Paint(); drawTextPaint.setColor(Color.YELLOW); drawTextPaint.setTextSize(50); textRect = new Rect(); }
3、重写 getItemOffsets() 方法
ItemDecoration 想要绘制的话,就必须得重写 getItemOffsets()
方法。代码如下
@Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state);// 绑定自己的 Adapter if (parent.getAdapter() instanceof StarAdapter){ StarAdapter starAdapter = (StarAdapter) parent.getAdapter();// 当前item 的位置 int position = parent.getChildLayoutPosition(view);// 分割线预留的空间 outRect.set(10, 10, 0, 0); } }
上面的代码中,我们给ItemDecoration 预留了一个划线的空间
运行如下:
4、绘制头部预留空间
- 在Adapter 中判断当前组的第一个item 是不是头部
// 是否是组的第一个Item public boolean isFirstItemOfGroup(int position) { if (position == 0) { return true; } else {// 拿到当前位置的和前一个位置的 组名 String currentItemGroupName = getGroupName(position); String preItemGroupName = getGroupName(position - 1);// 如果相等,则表示position 的 item 不是第一个,否则是 if (currentItemGroupName.equals(preItemGroupName)) { return false; } else { return true; } } }
- 在 获取到头部 item 之后,预留一个绘制空间
// 判断 Item 是头部 boolean isGroupHeader = starAdapter.isFirstItemOfGroup(position); if (isGroupHeader){// headerHeight 是头部 Item 的高度 outRect.set(0, headerHeight, 0, 0); }else {// decorationTop 分割线的高度 outRect.set(0, decorationTop, 0, 0); }
在 getItemOffsets()
中添加上面代码后,预留的 item 的头部空间就出来了,运行如下
5、重写 onDraw() 绘制头部
// 绘制自己 @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDraw(c, parent, state); if (parent.getAdapter() instanceof StarAdapter){ StarAdapter starAdapter = (StarAdapter) parent.getAdapter();// 当前屏幕上展示的 int count = parent.getChildCount();// 实现 itemView 的宽度和分割线的宽度一样 int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingLeft(); for (int i = 0; i < count; i++) { View view = parent.getChildAt(i);// 当前item 的位置 int position = parent.getChildLayoutPosition(view);// 判断是否是头部 boolean isGroupHeader = starAdapter.isFirstItemOfGroup(position); if (isGroupHeader){ c.drawRect(left, view.getTop() - headerHeight, right, view.getTop(), headPaint);// 获取当前组名 String groupName = starAdapter.getGroupName(position); c.drawText(groupName,left + groupNameLeftPadding,view.getTop() - headerHeight / 2 + textRect.height() / 2,drawTextPaint); }else { c.drawRect(left, view.getTop() - valueHeight, right, view.getTop(), headPaint); } } } }
代码运行结果如下:
6、绘制吸顶效果
// 绘制吸顶效果 @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDrawOver(c, parent, state); if (parent.getAdapter() instanceof StarAdapter){ StarAdapter starAdapter = (StarAdapter) parent.getAdapter(); int left = parent.getPaddingLeft(); int right = parent.getRight(); int top = parent.getTop();// 当前显示在界面的第一个 item int position = ((LinearLayoutManager)parent.getLayoutManager()).findFirstVisibleItemPosition(); View itemView = parent.findViewHolderForAdapterPosition(position).itemView;// 获取当前的上一个 头部 item boolean isFirstItemOfGroup = starAdapter.isFirstItemOfGroup(position +1 ); if(isFirstItemOfGroup){ int bottom = Math.min(top + headerHeight, itemView.getBottom()); c.drawRect(left, top, right, bottom, headPaint); String groupName = starAdapter.getGroupName(position); drawTextPaint.getTextBounds(groupName, 0, groupName.length(), textRect); c.clipRect(left, top, right, bottom); c.drawText(groupName, left + 20, bottom - headerHeight / 2 + textRect.height() / 2, drawTextPaint); }else {// 固定的头部信息 c.drawRect(left, top, right, top + headerHeight, headPaint); String groupName = starAdapter.getGroupName(position); drawTextPaint.getTextBounds(groupName, 0, groupName.length(), textRect);// 绘制文字 c.drawText(groupName, left + 20, top + headerHeight / 2 + textRect.height() /2, drawTextPaint); } } }
运行结果如下:
7、StarDecoration 完整代码
public class StarDecoration extends RecyclerView.ItemDecoration { // 分组栏的高度 private int headerHeight; private int valueHeight = 50; private Paint headPaint; private Paint drawTextPaint; private Rect textRect; private int decorationTop = 4;// 组名的 padding private int groupNameLeftPadding = 20; public StarDecoration(Context context){ // 顶部吸顶栏的高度 headerHeight = dp2px(context, valueHeight);// 每一组的头部的Paint headPaint = new Paint(); headPaint.setColor(Color.RED); drawTextPaint = new Paint(); drawTextPaint.setColor(Color.YELLOW); drawTextPaint.setTextSize(50); textRect = new Rect(); }// 绘制自己 @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDraw(c, parent, state); if (parent.getAdapter() instanceof StarAdapter){ StarAdapter starAdapter = (StarAdapter) parent.getAdapter();// 当前屏幕上展示的 int count = parent.getChildCount();// 实现 itemView 的宽度和分割线的宽度一样 int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingLeft(); for (int i = 0; i < count; i++) { View view = parent.getChildAt(i);// 当前item 的位置 int position = parent.getChildLayoutPosition(view);// 判断是否是头部 boolean isGroupHeader = starAdapter.isFirstItemOfGroup(position); if (isGroupHeader){ c.drawRect(left, view.getTop() - headerHeight, right, view.getTop(), headPaint);// 获取当前组名 String groupName = starAdapter.getGroupName(position); c.drawText(groupName,left + groupNameLeftPadding,view.getTop() - headerHeight / 2 + textRect.height() / 2,drawTextPaint); }else { c.drawRect(left, view.getTop() - decorationTop, 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 StarAdapter){ StarAdapter starAdapter = (StarAdapter) parent.getAdapter(); int left = parent.getPaddingLeft(); int right = parent.getRight(); int top = parent.getTop();// 当前显示在界面的第一个 item int position = ((LinearLayoutManager)parent.getLayoutManager()).findFirstVisibleItemPosition(); View itemView = parent.findViewHolderForAdapterPosition(position).itemView;// 获取当前的上一个 头部 item boolean isFirstItemOfGroup = starAdapter.isFirstItemOfGroup(position +1 ); if(isFirstItemOfGroup){ int bottom = Math.min(top + headerHeight, itemView.getBottom()); c.drawRect(left, top, right, bottom, headPaint); String groupName = starAdapter.getGroupName(position); drawTextPaint.getTextBounds(groupName, 0, groupName.length(), textRect); c.clipRect(left, top, right, bottom); c.drawText(groupName, left + 20, bottom - headerHeight / 2 + textRect.height() / 2, drawTextPaint); }else {// 固定的头部信息 c.drawRect(left, top, right, top + headerHeight, headPaint); String groupName = starAdapter.getGroupName(position); drawTextPaint.getTextBounds(groupName, 0, groupName.length(), textRect);// 绘制文字 c.drawText(groupName, left + 20, top + headerHeight / 2 + textRect.height() /2, drawTextPaint); } } } @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state);// 绑定自己的 Adapter if (parent.getAdapter() instanceof StarAdapter){ StarAdapter starAdapter = (StarAdapter) parent.getAdapter();// 当前item 的位置 int position = parent.getChildLayoutPosition(view);// 判断 Item 是头部 boolean isGroupHeader = starAdapter.isFirstItemOfGroup(position); if (isGroupHeader){// headerHeight 是头部 Item 的高度 outRect.set(0, headerHeight, 0, 0); }else {// decorationTop 分割线的高度 outRect.set(0, decorationTop, 0, 0); } } } / * 分辩率 * @param context * @param dpValue * @return */ private int dp2px(Context context, float dpValue){ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics()); }}
总结
计算的我有点头晕