栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Android 实现分组标题吸顶效果,支持上下左右padding

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Android 实现分组标题吸顶效果,支持上下左右padding

先上gif效果图:

技术方案:RecycleView + ItemDecoration

具体实现:

第一步:先实现相关业务代码,让数据加载出来

Activity:

class RecyclerViewActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recyclerview)
        val data: MutableList = getData()
        val adapter = RVAdapter(data)
        act_recyclerview_rv.adapter = adapter
        act_recyclerview_rv.layoutManager = LinearLayoutManager(this)
        val divider = DividerItemDecoration(this,DividerItemDecoration.VERTICAL)
        divider.setDrawable(getDrawable(R.drawable.shape_divider)!!)
//        act_recyclerview_rv.addItemDecoration(divider)
        //自定义itemDecoration 实现吸顶效果
        act_recyclerview_rv.addItemDecoration(MyItemDecoration())

    }

    private fun getData(): MutableList {
        val data: MutableList = mutableListOf()
        for (i in 0..2) {
            for (j in 0..9) {
                if (i == 0) {
                    data.add(DataBean("曹操$i$j","曹操分组"))
                } else if (i == 1) {
                    data.add(DataBean("刘备$i$j","刘备分组"))
                } else if (i == 2) {
                    data.add(DataBean("孙权$i$j","孙权分组"))
                }
            }
        }
        return data
    }
}
R.layout.activity_recyclerview



    

RVAdapter
class RVAdapter : RecyclerView.Adapter {
    var data: MutableList = mutableListOf()

    constructor(data: MutableList) {
        this.data = data
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RVViewHolder {
        val view =
            LayoutInflater.from(parent.context).inflate(R.layout.item_act_rec_rv, parent, false)
        view.setonClickListener {
            ToastUtil.showShortToast("you click ${view.findViewById(R.id.act_rec_rv_item_tv).text}")
        }
        return RVViewHolder(view)
    }

    override fun onBindViewHolder(holder: RVViewHolder, position: Int) {
        holder.name!!.text = data[position].name
    }

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

    
    fun isFirstGroupView(childLayoutPos: Int): Boolean {
        if (childLayoutPos == 0) {
            return true
        }
        if (data[childLayoutPos].groupName != data[childLayoutPos - 1].groupName) {
            return true
        }
        return false
    }

    fun getGroupName(childLayoutPosition: Int): String {
        return data[childLayoutPosition].groupName
    }

}
RVViewHolder
class RVViewHolder : RecyclerView.ViewHolder {
    var name: TextView? = null

    constructor(view: View) : super(view) {
        name = view.findViewById(R.id.act_rec_rv_item_tv)
    }
}
DataBean
data class DataBean(
    var name: String,
    var groupName: String
)
ZSConstants
object ZSConstants {
    val TITLE_TEXT_SIZE: Int = 18
    val DIVIDER_HEIGHT: Int = 10
    //此变量和布局文件中设置的高度保持一致
    val ITEM_HEIGHT: Int = 60
    val GROUP_HEIGHT: Int = 40
    val GROUP_NAME_MARGIN: Int = 10
}

第二步:利用自定义ItemDecoration来实现吸顶效果,并处理RecycleView的各种padding

相关说明都写在了注释里面,代码如下:

class MyItemDecoration : RecyclerView.ItemDecoration {

    private val headPaint = Paint()
    private val headPaint2 = Paint()
    private val textPaint = Paint()
    private val groupHeight: Float = DensityUtil.dp2px(ZSConstants.GROUP_HEIGHT).toFloat()
    private val dividerHeight: Float = DensityUtil.dp2px(ZSConstants.DIVIDER_HEIGHT).toFloat()
    private val groupNameMargin: Float = DensityUtil.dp2px(ZSConstants.GROUP_NAME_MARGIN).toFloat()

    constructor() {
        headPaint.color = Color.parseColor("#ff0000")
        headPaint.style = Paint.Style.FILL
        headPaint2.color = Color.parseColor("#00ff00")
        headPaint2.style = Paint.Style.FILL
        textPaint.color = Color.BLACK
        textPaint.isDither = true
        textPaint.isAntiAlias = true
        textPaint.textSize = DensityUtil.dp2px(ZSConstants.TITLE_TEXT_SIZE).toFloat()
    }

    
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        //在预留出的空间中 绘制分组标题
        val adapter = parent.adapter
        val left: Float = parent.paddingLeft.toFloat()
        val right: Float = parent.width.toFloat() - parent.paddingRight
        if (adapter is RVAdapter) {
            //获取可见view的个数
            val childCount = parent.childCount
            //循环遍历去绘制
            for (i in 0 until childCount) {
                c.save()
                //得到屏幕上显示的view
                val view = parent.getChildAt(i)
                //得到该view在整个列表布局中的位置
                val childLayoutPosition = parent.getChildLayoutPosition(view)
                //判断该位置是否是每组view的第一个
                val isFirstGroupView = adapter.isFirstGroupView(childLayoutPosition)
                if (isFirstGroupView &&
                    //头部屏蔽没有必要的绘制
                    view.top - groupHeight - parent.paddingTop >= 0 &&
                    //底部屏蔽没有必要的绘制
                    view.top <= parent.measuredHeight - parent.paddingBottom + groupHeight
                ) {
                    // 最底部的分割线需要c.clip一下
                    if (view.top.toFloat() > parent.measuredHeight - parent.paddingBottom) {
                        val rect = Rect(
                            left.toInt(),
                            view.top - groupHeight.toInt(),
                            right.toInt(),
                            parent.measuredHeight - parent.paddingBottom
                        )
                        c.clipRect(rect)
                    }
                    //绘制分组矩形背景
                    c.drawRect(
                        left,
                        view.top - groupHeight,
                        right,
                        view.top.toFloat(),
                        headPaint
                    )
                    //绘制标题文本
                    val text: String = adapter.getGroupName(childLayoutPosition)
                    c.drawText(
                        text,
                        left + groupNameMargin,
                        view.top - groupHeight / 2 + abs(textPaint.fontMetrics.ascent) / 2 - textPaint.fontMetrics.descent / 2,
                        textPaint
                    )
                } else if (
                //头部屏蔽没有必要的绘制
                    view.top - groupHeight - parent.paddingTop >= 0 &&
                    //底部屏蔽没有必要的绘制
                    view.top <= parent.measuredHeight - parent.paddingBottom + dividerHeight
                ) {
                    //绘制分割线
                    if (i == childCount - 1) {
                        log("parent height - parent.paddingBottom = ${parent.measuredHeight - parent.paddingBottom} view.top=${view.top}")
                    }
                    //最底部的分割线需要c.clip一下
                    if (view.top.toFloat() > parent.measuredHeight - parent.paddingBottom) {
                        val rect = Rect(
                            left.toInt(),
                            view.top - dividerHeight.toInt(),
                            right.toInt(),
                            parent.measuredHeight - parent.paddingBottom
                        )
                        c.clipRect(rect)
                    }
                    c.drawRect(
                        left,
                        view.top.toFloat() - dividerHeight.toInt(),
                        right,
                        view.top.toFloat(),
                        headPaint
                    )
                }
                c.restore()
            }
        }
    }

    
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        val adapter = parent.adapter
        val left: Float = parent.paddingLeft.toFloat()
        val top: Float = parent.paddingTop.toFloat()
        val right: Float = parent.width.toFloat() - parent.paddingRight
        if (adapter is RVAdapter) {
            //拿到第一个可见的view
            val firstVisiblePos =
                (parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
            val viewHolder = parent.findViewHolderForLayoutPosition(firstVisiblePos)
            val itemView = viewHolder!!.itemView
            //日志打印
//            val textView = itemView.findViewById(R.id.act_rec_rv_item_tv)
//            log("${textView.text} firstVisiblePos = $firstVisiblePos")
            //判断当前位置的下一个是否是分组的第一个view
            //为甚是下一个,因为当前的那个被onDrawOver位置的常驻标题挡住了
            //所以如果下一个是分组第一个的话,刚好开始执行推动的效果
            val isFirstGroupView = adapter.isFirstGroupView(firstVisiblePos + 1)
            if (isFirstGroupView) {
                //慢慢往上推动
//                log("${itemView.top} itemView.bottom = ${itemView.bottom}")
//                log("top-$top itemView.top=${itemView.top} itemView.bottom = ${itemView.bottom}")
                val bottom = min(groupHeight, itemView.bottom.toFloat() - top) + top
                c.drawRect(left, top, right, bottom, headPaint2)
                val y =
                    bottom - groupHeight / 2 + abs(textPaint.fontMetrics.ascent) / 2 - textPaint.fontMetrics.descent / 2
                val rect = Rect(0, top.toInt(), right.toInt(), bottom.toInt())
                c.clipRect(rect)
                val text: String = adapter.getGroupName(firstVisiblePos)
                c.drawText(
                    text,
                    left + groupNameMargin,
                    y,
                    textPaint
                )
            } else {
                //标题常驻在顶部
                c.drawRect(left, top, right, top + groupHeight, headPaint2)
                val y =
                    top + groupHeight - groupHeight / 2 + abs(textPaint.fontMetrics.ascent) / 2 - textPaint.fontMetrics.descent / 2
                val text: String = adapter.getGroupName(firstVisiblePos)
                c.drawText(
                    text,
                    left + groupNameMargin,
                    y,
                    textPaint
                )
            }
        }

    }

    
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        //拿到对应的adapter
        val adapter = parent.adapter
        if (adapter is RVAdapter) {
            //拿到当前view所在的位置
            val childLayoutPos = parent.getChildLayoutPosition(view)
            //判断此view是否是每一组的第一个view
            if (adapter.isFirstGroupView(childLayoutPos)) {
                outRect.set(0, groupHeight.toInt(), 0, 0)
            } else {
                outRect.set(0, dividerHeight.toInt(), 0, 0)
            }

            //日志打印
            val textView = view.findViewById(R.id.act_rec_rv_item_tv)
//            log("${textView.text} childLayoutPos = $childLayoutPos")
        }

    }
}

注意:涉及到具体的尺寸计算,特别是bottom、top之类的要十分细心小心,可以自己画画图来理解,也可以把工程跑起来,根据效果一点一点去理解。

难点就在于两个标题靠在一起时上面的标题慢慢被顶上去,这里的实现思路是在onDrawOver方法里面不断绘制上面的标题空间,让bottom不断减小(减小就是往上走),标题文字的绘制也要跟着往上走,然后还要通过canvas的clipRect方法去裁剪绘制区域,要不然会绘制到RecycleView paddingTop区域。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/750420.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号