RecyclerView ItemDecoration 实现分组吸顶效果

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xingxtao/article/details/80217873

本文实现的吸顶效果为:

简介

我们都知道 ListView 添加分割线可以通过在布局文件中添加 android:divider 属性即可,但是 RecyclerView 并没有提供那样的属性。如若需要使用分割线,则需要使用其他的方式实现:

  1. 给 RecyclerView item 设置 margin : 首先将布局文件中的 RecyclerView 背景设置成分割线的颜色(如黑色),itemView 的背景颜色设置成白色,然后在 onCreateViewHolder() 中为 itemView 设置 top / bottom margin;

    <!--布局文件-->
    <android.support.v7.widget.RecyclerView
           android:id="@+id/recycler_view"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:background="#000" />
    // RecyclerAdapter.java
    @Override
       public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
           View itemView = inflater.inflate(R.layout.item_recycler_view, parent, false);
           RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) itemView.getLayoutParams();
           layoutParams.bottomMargin = 50;  // 为方便观察,设置一个很大的间距
           itemView.setLayoutParams(layoutParams);
           return new ItemViewHolder(itemView);
       }

可以看到上面效果图:这种实现方式第一个 item 或者 最后一个 item(如上图 最后一个 item) 都会存在 分割线。同时也会增加不必要的背景设置,从而导致过度绘制。

. 2 自定义 ItemDecoration;

RecyclerView.ItemDecoration 的使用主要涉及到以下三个函数:

void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state)
void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state)

ItemDecoration 实现常规分割线

一般的,对于列表添加分割线,第一个 itemView 顶部和最后一个 itemView 底部是不需要分割线的,以下代码实现这种效果。

继承自 RecyclerView.ItemDecoration 类,在 onDraw() 中给 itemView 的顶部绘制一个矩形,第一个 itemView 没有绘制(我们通过 getChildAdapterPosition() 方法获取第 1 个 itemView 的索引,而不是直接使用 for 循环中的 i = 0,因为 i=0 是表示当前屏幕中第一个可见的 itemView 的索引)

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final String TAG = "DividerItemDecoration";

    private Context context;

    private int dividerHeight;

    private int dividerPaddingLeft;

    private int dividerPaddingRight;

    private Paint paint;

    public DividerItemDecoration(Context context) {
        this.context = context;
        dividerHeight = dp2Px(20);
        dividerPaddingLeft = dp2Px(10);
        dividerPaddingRight = dp2Px(10);
        initPaint();
    }

    private void initPaint() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(context.getResources().getColor(R.color.colorAccent));
        paint.setStyle(Paint.Style.FILL);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        // 不是第一个 item 才设置 top
        if (parent.getChildAdapterPosition(view) != 0) {
            outRect.top = dividerHeight;
        }
        Log.d(TAG, "getItemOffsets: left = " + outRect.left + ",top = " + outRect.top + ",right = " + outRect.right
                + ", bottom = " + outRect.bottom);


    }


    @Override
    public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(canvas, parent, state);
        // 获取当前屏幕可见 item 数量,而不是 RecyclerView 所有的 item 数量
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int childAdapterPosition = parent.getChildAdapterPosition(view);
            // 第一个 itemview 不需要绘制
            if (childAdapterPosition == 0) {
                continue;
            }
            // 由于分割线是绘制在每一个 itemview 的顶部,所以分割线矩形 rect.bottom = itemview.top,
            // rect.top = itemview.top - dividerHeight
            int bottom = view.getTop();
            int top = bottom - dividerHeight;
            int left = parent.getPaddingLeft();
            int right = parent.getPaddingRight();
            Log.d(TAG, "onDraw: top = " + top + ",bottom = " + bottom);
            // 考虑 divider 左右 padding
            canvas.drawRect(new Rect(left + dividerPaddingLeft, top,
                    view.getWidth() - right - dividerPaddingRight, bottom), paint);
        }
    }

    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);

    }


    private int dp2Px(int dpValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics());
    }
}

吸顶效果分割线

(1) 观察其他 App 中的列表吸顶效果,发现 第一个 itemView 的顶部也是有分割线的,因此,我们首先需要将 RecyclerView 的 第一个 itemView 改成含有 分割线的。

 @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        // 不是第一个 item 才设置 top
//        if (parent.getChildAdapterPosition(view) != 0) {
        outRect.top = dividerHeight;
//        }
        Log.d(TAG, "getItemOffsets: left = " + outRect.left + ",top = " + outRect.top + ",right = " + outRect.right
                + ", bottom = " + outRect.bottom);


    }


    @Override
    public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(canvas, parent, state);
        // 获取当前屏幕可见 item 数量,而不是 RecyclerView 所有的 item 数量
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int childAdapterPosition = parent.getChildAdapterPosition(view);
//            // 第一个 itemview 不需要绘制
//            if (childAdapterPosition == 0) {
//                continue;
//            }
            // 由于分割线是绘制在每一个 itemview 的顶部,所以分割线矩形 rect.bottom = itemview.top,
            // rect.top = itemview.top - dividerHeight
            int bottom = view.getTop();
            int top = bottom - dividerHeight;
            int left = parent.getPaddingLeft();
            int right = parent.getPaddingRight();
            Log.d(TAG, "onDraw: top = " + top + ",bottom = " + bottom);
            canvas.drawRect(left + dividerPaddingLeft, top,
                    view.getWidth() - right - dividerPaddingRight, bottom, paint);
        }
    }

(2) 在 RecyclerView 顶部添加一个固定的分割线,向上滚动时,当前屏幕第一个可见 itemView 的 bottom 开始小于分割线高度时,第二个 itemView 的分割线将第一个 itemView 分割线挤出屏幕,第二个 itemView 的分割线充当顶部固定的分割线。

@Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);
        View firstVisibleView = parent.getChildAt(0);
        int left = parent.getPaddingLeft();
        int right = firstVisibleView.getWidth() - parent.getPaddingRight();
        // 第一个itemview(firstVisibleView) 的 bottom 值小于分割线高度,分割线随着 recyclerview 滚动,
        // 分割线top固定不变,bottom=firstVisibleView.bottom
        if (firstVisibleView.getBottom() <= dividerHeight) {
            canvas.drawRect(left, 0, right, firstVisibleView.getBottom(), topDividerPaint);
        } else {
            canvas.drawRect(left, 0, right, dividerHeight, topDividerPaint);
        }
    }

(3) 在分组分割线上绘制 分组文字。

完整代码 DividerItemDecoration.java:

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final String TAG = "DividerItemDecoration";

    private Context context;

    private int groupDividerHeight;     // 分组分割线高度

    private int itemDividerHeight;     // 分组内item分割线高度

    private int dividerPaddingLeft;    // 分割线左间距

    private int dividerPaddingRight;   // 分割线右间距

    private Paint dividerPaint;     // 绘制分割线画笔

    private Paint textPaint;        // 绘制文字画笔

    private Paint topDividerPaint;


    public DividerItemDecoration(Context context, OnGroupListener listener) {
        this.context = context;
        this.listener = listener;
        groupDividerHeight = dp2Px(24);
        itemDividerHeight = dp2Px(1);
        initPaint();
    }

    private void initPaint() {
        dividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        dividerPaint.setColor(context.getResources().getColor(R.color.colorAccent));
        dividerPaint.setStyle(Paint.Style.FILL);


        topDividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        topDividerPaint.setColor(Color.parseColor("#9924f715"));
        topDividerPaint.setStyle(Paint.Style.FILL);


        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(sp2Px(14));

    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
//        // 不是第一个 item 才设置 top
////        if (parent.getChildAdapterPosition(view) != 0) {
//        outRect.top = groupDividerHeight;
////        }
//        Log.d(TAG, "getItemOffsets: left = " + outRect.left + ",top = " + outRect.top + ",right = " + outRect.right
//                + ", bottom = " + outRect.bottom);

        int position = parent.getChildAdapterPosition(view);
        // 获取组名
        String groupName = getGroupName(position);
        if (groupName == null) {
            return;
        }
        if (position == 0 || isGroupFirst(position)) {
            outRect.top = groupDividerHeight;
        } else {
            outRect.top = dp2Px(1);
        }
    }


    @Override
    public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(canvas, parent, state);
        // getChildCount() 获取的是当前屏幕可见 item 数量,而不是 RecyclerView 所有的 item 数量
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = parent.getChildAt(i);
            // 获取当前itemview在adapter中的索引
            int childAdapterPosition = parent.getChildAdapterPosition(childView);
            /**
             * 由于分割线是绘制在每一个 itemview 的顶部,所以分割线矩形 rect.bottom = itemview.top,
             * rect.top = itemview.top - groupDividerHeight
             */
            int bottom = childView.getTop();
            int left = parent.getPaddingLeft();
            int right = parent.getPaddingRight();
            if (isGroupFirst(childAdapterPosition)) {   // 是分组第一个,则绘制分组分割线
                int top = bottom - groupDividerHeight;
                Log.d(TAG, "onDraw: top = " + top + ",bottom = " + bottom);
                // 绘制分组分割线矩形
                canvas.drawRect(left + dividerPaddingLeft, top,
                        childView.getWidth() - right - dividerPaddingRight, bottom, dividerPaint);
                // 绘制分组分割线中的文字
                float baseLine = (top + bottom) / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
                canvas.drawText(getGroupName(childAdapterPosition), left + dp2Px(10),
                        baseLine, textPaint);
            } else {    // 不是分组中第一个,则绘制常规分割线
                int top = bottom - dp2Px(1);
                canvas.drawRect(left + dividerPaddingLeft, top,
                        childView.getWidth() - right - dividerPaddingRight, bottom, dividerPaint);
            }
        }
    }

    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);
        View firstVisibleView = parent.getChildAt(0);
        int firstVisiblePosition = parent.getChildAdapterPosition(firstVisibleView);
        String groupName = getGroupName(firstVisiblePosition);
        int left = parent.getPaddingLeft();
        int right = firstVisibleView.getWidth() - parent.getPaddingRight();
        // 第一个itemview(firstVisibleView) 的 bottom 值小于分割线高度,分割线随着 recyclerview 滚动,
        // 分割线top固定不变,bottom=firstVisibleView.bottom
        if (firstVisibleView.getBottom() <= groupDividerHeight && isGroupFirst(firstVisiblePosition + 1)) {
            canvas.drawRect(left, 0, right, firstVisibleView.getBottom(), dividerPaint);
            float baseLine = firstVisibleView.getBottom() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
            canvas.drawText(groupName, left + dp2Px(10),
                    baseLine, textPaint);
        } else {
            canvas.drawRect(left, 0, right, groupDividerHeight, dividerPaint);
            float baseLine = groupDividerHeight / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
            canvas.drawText(groupName, left + dp2Px(10), baseLine, textPaint);
        }


    }


    private OnGroupListener listener;

    static interface OnGroupListener {

        // 获取分组中第一个文字
        String getGroupName(int position);
    }


    public String getGroupName(int position) {
        if (listener != null) {
            return listener.getGroupName(position);
        }
        return null;
    }


    /**
     * 是否是某组中第一个item
     *
     * @param position
     * @return
     */
    private boolean isGroupFirst(int position) {
        // 第一个 itemView 肯定是新的一个分组
        if (position == 0) {
            return true;
        } else {
            String preGroupName = getGroupName(position - 1);
            String groupName = getGroupName(position);
            return !TextUtils.equals(preGroupName, groupName);
        }
    }


    private int dp2Px(int dpValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics());
    }

    private int sp2Px(int spValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, context.getResources().getDisplayMetrics());
    }
}

Activity 中调用:

public class RecyclerViewActivity extends AppCompatActivity {

    private List<String> dataList = new ArrayList();

    private RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_view);
        recyclerView = findViewById(R.id.recycler_view);
        initData();
        LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(layoutManager);

        RecyclerAdapter adapter = new RecyclerAdapter(this, dataList);
        recyclerView.setAdapter(adapter);

        recyclerView.addItemDecoration(new DividerItemDecoration(this, new DividerItemDecoration.OnGroupListener() {
            @Override
            public String getGroupName(int position) {
                return dataList.get(position).substring(0, 1);
            }
        }));

    }

    private void initData() {
        dataList.add("java");
        dataList.add("jdk");
        dataList.add("php");
        dataList.add("c++");
        dataList.add("linux");
        dataList.add("windows");
        dataList.add("macos");
        dataList.add("red hat");
        dataList.add("python");
        dataList.add("jvm");
        dataList.add("wechat");
        dataList.add("cellphone");
        dataList.add("iphone");
        dataList.add("mouse");
        dataList.add("huawei");
        dataList.add("xiaomi");
        dataList.add("meizu");
        dataList.add("mocrosoft");
        dataList.add("google");
        dataList.add("whatsapp");
        dataList.add("iMac");
        dataList.add("c#");
        dataList.add("iOS");
        dataList.add("water");
        dataList.add("xiaohongshu");
        dataList.add("jake");
        dataList.add("zuk");


        Collections.sort(dataList);

    }


    public static void start(Context context) {
        Intent intent = new Intent(context, RecyclerViewActivity.class);
        context.startActivity(intent);
    }
}

猜你喜欢

转载自blog.csdn.net/xingxtao/article/details/80217873