Android-Kotlin 实现Recycleview粘性头部分组效果

先上效果图:

可以从图中看到,增加一个粘性头部,也相当于一个直观的分组效果,有比较多的实现方式,例如:

  • 通过RecyclerView的ItemDecoration来实现
  • 通过BaseSectionQuickAdapter适配器来实现

本文是通过重写RecyclerView的ItemDecoration来实现,如果想使用BaseSectionQuickAdapter可以参考使用BRVAH

https://github.com/CymChad/BaseRecyclerViewAdapterHelper

在贴代码之前先了解一下ItemDecoration

介绍:

官方介绍

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

All ItemDecorations are drawn in the order they were added, before the item views (in onDraw() and after the items (in onDrawOver(Canvas, RecyclerView, RecyclerView.State).

官方的说明定义还是很清晰精准,大概的意思是

ItemDecoration 允许应用给具体的View添加具体的图画或者layout的偏移,对于绘制View之间的分割线,视觉分组边界等等是非常有用的。所有的ItemDecorations按照被添加的顺序在itemview之前(如果通过重写`onDraw()`)或者itemview之后(如果通过重写 `onDrawOver(Canvas, RecyclerView, RecyclerView.State)`)绘制。

所以整体的实现思路就是重写ItemDecoration,通过接口回调从数据data数组中获取对应item的日期,通过日期来区分分组设置一个item的偏移,再通过onDrawOver绘制一个分组显示。

ItemDecoration 除去被标记为过时的外,只剩如下三个方法:

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

其中三个方法的用途

  1. getItemOffests可以通过outRect.set(l,t,r,b)设置指定itemview的paddingLeft,paddingTop, paddingRight, paddingBottom
  2. onDraw可以通过一系列c.drawXXX()方法在绘制itemView之前绘制我们需要的内容。
  3. onDrawOver与onDraw类似,只不过是在绘制itemView之后绘制,具体表现形式,就是绘制的内容在itemview上层。

通过调用RecyclerView的addItemDecoration()便可以添加ItemDecoration,ItemDecoration会被add到集合中,然后RecyclerView会根据add的顺序依次调用(getItemOffsets->onDraw->onDrawOver)的方法。

贴上实现的代码:

适配器,也可按需继承使用其他adapter:

class UndoneTodoListAdapter (val context: Context, datas: MutableList<TodoListResponse.Data.Datas>) :
        BaseQuickAdapter<TodoListResponse.Data.Datas, BaseViewHolder>(R.layout.undone_todo_item, datas) {

    private var d : MutableList<TodoListResponse.Data.Datas> = datas

    override fun getItemCount(): Int {
        return d.size
    }

    override fun convert(helper: BaseViewHolder?, item: TodoListResponse.Data.Datas?) {
        item ?: return
        var p : String = "一般"
        when {
            item.priority == 1 -> p = "重要"
            item.priority == 2 -> p = "一般"
        }
        if (helper != null) {
            @Suppress("DEPRECATION")
            helper.setText(R.id.item_title, item.title)
                    .setText(R.id.item_content, item.content)
                    .setText(R.id.todo_p, "优先级:$p")
                    .addOnClickListener(R.id.delete_todo)
                    .addOnClickListener(R.id.update_status_todo)
            when {
                item.type == 1 -> helper.setImageResource(R.id.item_type, R.drawable.work)
                item.type == 2 -> helper.setImageResource(R.id.item_type, R.drawable.sport)
                item.type == 3 -> helper.setImageResource(R.id.item_type, R.drawable.play)
            }

        }
    }

}

item布局代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="140dp"
    android:background="@color/white"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp">

        <ImageView
            android:id="@+id/item_type"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:src="@drawable/work"
            android:layout_centerVertical="true"
            android:layout_marginStart="@dimen/mt10"/>

        <TextView
            android:id="@+id/item_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toEndOf="@+id/item_type"
            android:layout_centerVertical="true"
            android:layout_marginStart="@dimen/mt10"
            android:layout_marginEnd="@dimen/mt10"
            android:text="测试"
            android:textSize="16sp"
            android:textColor="@color/colorPrimary"/>

        <ImageButton
            android:id="@+id/delete_todo"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:layout_marginEnd="@dimen/mt10"
            android:background="@drawable/delete_black"/>

    </RelativeLayout>
    
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="30dp">

        <TextView
            android:id="@+id/todo_p"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginStart="@dimen/mt10"
            android:text="优先级:一般"
            android:textSize="14sp"
            android:textColor="@color/colorPrimary"/>

    </RelativeLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/item_content"
            android:layout_toStartOf="@+id/update_status_todo"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingStart="10dp"
            android:paddingEnd="10dp"
            android:paddingBottom="10dp"
            android:maxLines="3"
            android:text="测试内容"
            android:textSize="14sp"/>

        <ImageButton
            android:id="@+id/update_status_todo"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:layout_marginEnd="@dimen/mt10"
            android:background="@drawable/finsh"/>

    </RelativeLayout>

</LinearLayout>
自定义列表间隔 StickyDecoration
class StickyDecoration(context: Context, decorationCallback: DecorationCallback) : RecyclerView.ItemDecoration() {

    private var callback: DecorationCallback? = decorationCallback
    private var textPaint: TextPaint? = null
    private var paint: Paint? = null
    private var topHead: Int = 0
    private var topHeadD: Int = 0

    init {
        paint = Paint()
        paint!!.color = ContextCompat.getColor(context, R.color.bg_header)
        textPaint = TextPaint()
        textPaint!!.typeface = Typeface.DEFAULT
        textPaint!!.isFakeBoldText = false
        textPaint!!.isAntiAlias = true
        textPaint!!.textSize = 35f
        textPaint!!.color = ContextCompat.getColor(context, R.color.colorPrimary)
        textPaint!!.textAlign = Paint.Align.LEFT
        topHead = context.resources.getDimensionPixelSize(R.dimen.head_top)
        topHeadD = context.resources.getDimensionPixelSize(R.dimen.head_top_d)
    }

    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.getItemOffsets(outRect, view, parent, state)
        val position = parent!!.getChildAdapterPosition(view)
        val data : String = callback!!.getData(position)
        if (TextUtils.isEmpty(data) || TextUtils.equals(data,"")) {
            return
        }
        //同组的第一个添加日期padding
        if (position == 0 || isHeader(position)) {
            outRect!!.top = topHead
        } else {
            outRect!!.top = topHeadD
        }
    }

    override fun onDrawOver(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        super.onDrawOver(c, parent, state)
        //获取当前可见的item的数量
        val childCount = parent!!.childCount
        //获取所有的的item个数
//        val itemCount = state!!.itemCount
        val itemCount = parent.adapter.itemCount
        val left : Int = parent.left + parent.paddingLeft
        val right : Int = parent.right + parent.paddingRight
        var preData : String?
        var currentDate : String? = null
        for (i in 0 until childCount) {
            val view = parent.getChildAt(i)
            val position = parent.getChildAdapterPosition(view)
            val textLine = callback!!.getData(position)
            preData = currentDate
            currentDate = callback!!.getData(position)
            if (TextUtils.isEmpty(currentDate) || TextUtils.equals(currentDate, preData)) {
                continue
            }
            if (TextUtils.isEmpty(textLine)) {
                continue
            }
            val viewBottom = view.bottom
            var textY = max(topHead, view.top).toFloat()
            //下一个和当前不一样移动当前
            if (position + 1 < itemCount) {
                val nextData = callback!!.getData(position + 1)
                if (currentDate != nextData && viewBottom < textY) {
                    //组内最后一个view进入了header
                    textY = viewBottom.toFloat()
                }
            }
            val rect = Rect(left, textY.toInt() - topHead, right, textY.toInt())
            c!!.drawRect(rect, paint!!)
            //绘制文字基线,文字的的绘制是从绘制的矩形底部开始的
            val fontMetrics = textPaint!!.fontMetrics
            val baseline = ((rect.bottom + rect.top).toFloat() - fontMetrics.bottom - fontMetrics.top) / 2
            textPaint!!.textAlign = Paint.Align.CENTER//文字居中
            //绘制文本
            c.drawText(textLine, 100F, baseline, textPaint!!)
        }
    }

    //判断是否为头部
    private fun isHeader(pos: Int): Boolean {
        return if (pos == 0) {
            true
        } else {
            val preData = callback!!.getData(pos - 1)
            val data = callback!!.getData(pos)
            preData != data
        }
    }

    interface DecorationCallback {
        fun getData(position: Int): String
    }
}

使用添加

todo_rv.run {
            layoutManager = LinearLayoutManager(context)
            adapter = todoListAdapter
            addItemDecoration(StickyDecoration(context, object : StickyDecoration.DecorationCallback {
                override fun getData(position: Int): String {
                    return datas[position].dateStr
                }
            }))
        }

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值