【安卓笔记】RecyclerView之ItemDecoration实现吸顶效果

发布于:2025-07-21 ⋅ 阅读:(19) ⋅ 点赞:(0)

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部分按照项目修改即可


网站公告

今日签到

点亮在社区的每一天
去签到