Android控件RecyclerView(三)——ItemDecoration的使用与自定义 - Go语言中文社区

Android控件RecyclerView(三)——ItemDecoration的使用与自定义


目录

前言

1. 添加分割线

1.1 添加一个默认分割线

1.2 添加自定义样式分割线

1.3 完整代码

2. ItemDecoration的自定义

2.1 自定义流程

2.2 getItemOffsets

2.3 onDraw

2.4 onDrawOver

2.5 完整代码


前言

文章属于学习总结 ,如有错漏之处,敬请指正。

同系列文章:

Android控件RecyclerView(一)——大家都知道的RecyclerView

Android控件RecyclerView(二)——LayoutManager及其自定义

1. 添加分割线

Android自带DividerItemDecoration类加上RecyclerView.addItemDecoration()方法可设置简单分割线。

1.1 添加一个默认分割线

val itemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
recyclerView.addItemDecoration(itemDecoration)

这个分割线使用的是系统自带分割线样式 android.R.attr.listDivider

1.2 添加自定义样式分割线

    val itemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply {
        setDrawable(resources.getDrawable(R.drawable.shape_divider_line_vertical_red_2dp))
    }
    recyclerView.addItemDecoration(itemDecoration)

drawable为高2dp的红色分割线 shape_divider_line_vertical_red_2dp

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@android:color/holo_red_dark" />
    <size android:height="2dp" />
</shape>

查看效果 

1.3 完整代码

在前文的基础上贴上显示RecyclerView列表显示的相关的代码

数据类

/**
 * 联系人 initial 拼音首字母 name 联系人名字
 */
data class Contact(val initial: String, val name: String)

Adapter

class ContactAdapter : RecyclerView.Adapter<ContactAdapter.ViewHolder>() {
    var items: List<Contact> = ArrayList()
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactAdapter.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ViewHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false))
    }

    override fun getItemCount(): Int = items.size

    override fun onBindViewHolder(viewHolder: ContactAdapter.ViewHolder, position: Int) {
        viewHolder.bindView(items[position])
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(android.R.id.text1)

        fun bindView(contact: Contact) {
            textView.text = contact.name
        }
    }
}

Activity

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.*
import cn.xhuww.recyclerview.R
import cn.xhuww.recyclerview.adapter.ContactAdapter
import kotlinx.android.synthetic.main.activity_recycle_view.*

class ItemDecorationActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycle_view)

        val linearLayoutManager = LinearLayoutManager(this)
        val contactAdapter = ContactAdapter().apply {
            // 生成首字母A - Z 开头的数据,5个为一组
            items = (325..454).map {
                val ascii = it / 5
                Contact(ascii.toChar().toString(), "${ascii.toChar()} $it")
            }
        }
        val itemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply {
            setDrawable(resources.getDrawable(R.drawable.shape_divider_line_vertical_red_2dp))
        }
        recyclerView.addItemDecoration(itemDecoration)

        recyclerView.apply {
            layoutManager = linearLayoutManager
            adapter = contactAdapter
            addItemDecoration(itemDecoration)
        }
    }
}

2. ItemDecoration的自定义

ItemDecoration 除了用于分割线,还可对其自定义来装饰Item,下面准备自定一个 悬浮吸顶效果 的ItemDecoration。

先上效果图

2.1 自定义流程

自定义ItemDecoration一般需要进行如下步骤

  • 新建类继承于 ItemDecoration
  • 重写 getItemOffsets 设置Item间的间隔
  • 重写 onDraw 在间隔间绘制 分割线
  • 重写 onDrawOver 在RecyclerView之上绘制内容

新建 SuspendItemDecoration,重写3个方法,然后定义需要外部给定的属性,如数据、背景、文字样式等等

class SuspendItemDecoration(val context: Context) : RecyclerView.ItemDecoration() {

    var dividerDrawable: Drawable? = null       //普通Item间的分割线
    var groupDividerDrawable: Drawable? = null  //每组之间的分割线
    var contacts: List<Contact>? = null         //联系人数据

    private val bounds = Rect()
    private val textPaint: TextPaint
    @ColorRes
    var textColor: Int = android.R.color.white
        set(value) {
            field = value
            textPaint.color = context.resources.getColor(value)
        }
    var textSize: Float = 20F
        set(value) {
            field = value
            textPaint.textSize = context.sp2px(value)
        }

    var textTypeface = Typeface.DEFAULT_BOLD!!
        set(value) {
            field = value
            textPaint.typeface = value
        }

    init {
        //系统默认的分割线
        val tapeArray = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
        dividerDrawable = tapeArray.getDrawable(0)
        tapeArray.recycle()

        textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)

        textPaint.color = context.resources.getColor(textColor)
        textPaint.textSize = context.sp2px(textSize)
        textPaint.typeface = textTypeface
    }

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
    }

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
    }
}

 扩展方法 sp转px

fun Context.sp2px(sp: Float): Float =
    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, resources.displayMetrics)

2.2 getItemOffsets

根据数据规则,以及传入的Drawable来设置Item间的高

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.set(0, 0, 0, 0)
        val list = contacts ?: return

        val position = (view.layoutParams as RecyclerView.LayoutParams).viewLayoutPosition

        val currentInitial = list[position].initial
        val lastInitial = if (position >= 1) {
            list[position - 1].initial
        } else {
            null
        }

        //当前首字母与上一个首字母相同 则属于同一组,使用 dividerDrawable
        val drawable = if (currentInitial == lastInitial) {
            dividerDrawable
        } else {
            groupDividerDrawable
        }

        val height = drawable?.intrinsicHeight ?: 0
        //设置Item的上下左右偏移
        outRect.set(0, height, 0, 0)
    }

然后设置分割线Drawable,分别是红色的2dp 与45 dp

class ItemDecorationActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_recycle_view)

        val list = (325..454).map {
            val ascii = it / 5
            Contact(ascii.toChar().toString(), "${ascii.toChar()} $it")
        }

        val itemDecoration = SuspendItemDecoration(this).apply {
            dividerDrawable = resources.getDrawable(R.drawable.shape_divider_line_vertical_red_2dp)
            groupDividerDrawable = resources.getDrawable(R.drawable.shape_divider_line_vertical_red_45dp)
            contacts = list
        }

        recyclerView.addItemDecoration(itemDecoration)

        val linearLayoutManager = LinearLayoutManager(this)
        val contactAdapter = ContactAdapter().apply { items = list }

        recyclerView.apply {
            layoutManager = linearLayoutManager
            adapter = contactAdapter
            addItemDecoration(itemDecoration)
        }
    }
}

此时查看效果

间隔有了但并没有预想的红色分割线,因为 getItemOffsets 方法中只是设置了Item之间的间距,红色Drawable还需要在onDraw中绘制。

2.3 onDraw

与 getItemOffsets逻辑一致,两个Item之间相同首字母则为一组,item之间就不绘制首字母文本,不相同则绘制文本

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        val list = contacts ?: return

        c.save()

        val left = parent.paddingLeft
        val right = parent.width - parent.paddingRight

        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)
            val params = child.layoutParams as RecyclerView.LayoutParams
            val position = params.viewLayoutPosition

            val currentInitial = list[position].initial
            val lastInitial = if (position >= 1) {
                list[position - 1].initial
            } else {
                null
            }

            var isDrawText = false
            parent.getDecoratedBoundsWithMargins(child, bounds)
            val drawable = if (currentInitial == lastInitial) {
                dividerDrawable
            } else {
                isDrawText = true
                groupDividerDrawable
            }

            //绘制分割线
            val top = bounds.top
            val bottom = top + (drawable?.intrinsicHeight ?: 0)
            drawable?.setBounds(left, top, right, bottom)
            drawable?.draw(c)
            //绘制文本内容
            if (isDrawText) {
                if (drawable != null) {
                    textPaint.getTextBounds(currentInitial, 0, currentInitial.length, bounds)
                    val textX = child.paddingLeft.toFloat()
                    val textY = (child.top - (drawable.intrinsicHeight - bounds.height()) / 2).toFloat()
                    c.drawText(currentInitial, textX, textY, textPaint)
                }
            }
        }
        c.restore()
    }

此时查看效果

分割线背景颜色有了,联系人首字母文本也有了,就差顶部悬浮的功能了

2.4 onDrawOver

看名字就可以猜想,此方法是在xxx之上绘制内容,使用此方法就可以在RecyclerView表面上方绘制我们想要展示的内容

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)

        val list = contacts ?: return
        val drawable = groupDividerDrawable ?: return

        //只判断layoutManager为LinearLayoutManager的情况,其他情况的不做处理
        val layoutManager = parent.layoutManager as? LinearLayoutManager ?: return

        val position = layoutManager.findFirstVisibleItemPosition()
        if (position < 0 || position > list.size - 1) {
            return
        }

        val child = parent.findViewHolderForLayoutPosition(position)!!.itemView

        val currentInitial = list[position].initial
        val nextInitial = if (position + 1 < list.size) {
            list[position + 1].initial
        } else {
            null
        }

        parent.getDecoratedBoundsWithMargins(child, bounds)

        c.save()
        if (currentInitial != nextInitial) {
            //顶部移出效果
            if (child.top + child.height < drawable.intrinsicHeight) {
                c.translate(0f, (child.height + child.top - drawable.intrinsicHeight).toFloat())
            }
        }

        val left = parent.paddingLeft
        val top = parent.paddingTop
        val right = parent.right - parent.paddingRight
        val bottom = parent.paddingTop + drawable.intrinsicHeight

        drawable.setBounds(left, top, right, bottom)
        drawable.draw(c)

        textPaint.getTextBounds(currentInitial, 0, currentInitial.length, bounds)
        val textX = child.paddingLeft.toFloat()
        val textY =
            (parent.paddingTop + drawable.intrinsicHeight - (drawable.intrinsicHeight - bounds.height()) / 2).toFloat()
        c.drawText(currentInitial, textX, textY, textPaint)
        c.restore()
    }

此时在运行查看效果就可以发现已经达到了前面发出的效果。

2.5 完整代码

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.support.annotation.ColorRes
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.text.TextPaint
import android.view.View
import cn.xhuww.recyclerview.itemdecoration.Contact
import cn.xhuww.recyclerview.utils.sp2px

class SuspendItemDecoration(val context: Context) : RecyclerView.ItemDecoration() {

    var dividerDrawable: Drawable? = null       //普通Item间的分割线
    var groupDividerDrawable: Drawable? = null  //每组之间的分割线
    var contacts: List<Contact>? = null         //联系人数据

    private val bounds = Rect()
    private val textPaint: TextPaint
    @ColorRes
    var textColor: Int = android.R.color.white
        set(value) {
            field = value
            textPaint.color = context.resources.getColor(value)
        }
    var textSize: Float = 20F
        set(value) {
            field = value
            textPaint.textSize = context.sp2px(value)
        }

    var textTypeface = Typeface.DEFAULT_BOLD!!
        set(value) {
            field = value
            textPaint.typeface = value
        }

    init {
        //系统默认的分割线
        val tapeArray = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
        dividerDrawable = tapeArray.getDrawable(0)
        tapeArray.recycle()

        textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)

        textPaint.color = context.resources.getColor(textColor)
        textPaint.textSize = context.sp2px(textSize)
        textPaint.typeface = textTypeface
    }

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        val list = contacts ?: return

        c.save()

        val left = parent.paddingLeft
        val right = parent.width - parent.paddingRight

        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)
            val params = child.layoutParams as RecyclerView.LayoutParams
            val position = params.viewLayoutPosition

            val currentInitial = list[position].initial
            val lastInitial = if (position >= 1) {
                list[position - 1].initial
            } else {
                null
            }

            var isDrawText = false
            parent.getDecoratedBoundsWithMargins(child, bounds)
            val drawable = if (currentInitial == lastInitial) {
                dividerDrawable
            } else {
                isDrawText = true
                groupDividerDrawable
            }

            //绘制分割线
            val top = bounds.top
            val bottom = top + (drawable?.intrinsicHeight ?: 0)
            drawable?.setBounds(left, top, right, bottom)
            drawable?.draw(c)
            //绘制文本内容
            if (isDrawText) {
                drawable ?: return
                textPaint.getTextBounds(currentInitial, 0, currentInitial.length, bounds)
                val textX = child.paddingLeft.toFloat()
                val textY = (child.top - (drawable.intrinsicHeight - bounds.height()) / 2).toFloat()
                c.drawText(currentInitial, textX, textY, textPaint)
            }
        }
        c.restore()
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)

        val list = contacts ?: return
        val drawable = groupDividerDrawable ?: return

        //只判断layoutManager为LinearLayoutManager的情况,其他情况的不做处理
        val layoutManager = parent.layoutManager as? LinearLayoutManager ?: return

        val position = layoutManager.findFirstVisibleItemPosition()
        if (position < 0 || position > list.size - 1) {
            return
        }

        val child = parent.findViewHolderForLayoutPosition(position)!!.itemView

        val currentInitial = list[position].initial
        val nextInitial = if (position + 1 < list.size) {
            list[position + 1].initial
        } else {
            null
        }

        parent.getDecoratedBoundsWithMargins(child, bounds)

        c.save()
        if (currentInitial != nextInitial) {
            //顶部移出效果
            if (child.top + child.height < drawable.intrinsicHeight) {
                c.translate(0f, (child.height + child.top - drawable.intrinsicHeight).toFloat())
            }
        }

        val left = parent.paddingLeft
        val top = parent.paddingTop
        val right = parent.right - parent.paddingRight
        val bottom = parent.paddingTop + drawable.intrinsicHeight

        drawable.setBounds(left, top, right, bottom)
        drawable.draw(c)

        textPaint.getTextBounds(currentInitial, 0, currentInitial.length, bounds)
        val textX = child.paddingLeft.toFloat()
        val textY =
            (parent.paddingTop + drawable.intrinsicHeight - (drawable.intrinsicHeight - bounds.height()) / 2).toFloat()
        c.drawText(currentInitial, textX, textY, textPaint)
        c.restore()
    }

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        outRect.set(0, 0, 0, 0)
        val list = contacts ?: return

        val position = (view.layoutParams as RecyclerView.LayoutParams).viewLayoutPosition

        val currentInitial = list[position].initial
        val lastInitial = if (position >= 1) {
            list[position - 1].initial
        } else {
            null
        }

        //当前首字母与上一个首字母相同 则属于同一组,使用 dividerDrawable
        val drawable = if (currentInitial == lastInitial) {
            dividerDrawable
        } else {
            groupDividerDrawable
        }

        val height = drawable?.intrinsicHeight ?: 0
        //设置Item的上下左右偏移
        outRect.set(0, height, 0, 0)
    }
}

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/ww897532167/article/details/86187058
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 17:42:44
  • 阅读 ( 1505 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢