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)
    }
}

 

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RecyclerViewAndroid 中常用的列表展示控件,它的适配器(Adapter)负责将数据绑定到 RecyclerView 上。下面是使用 Android Studio 创建 RecyclerView 适配器的一般步骤: 1. 首先,在布局文件中定义 RecyclerView,例如在 activity_main.xml 文件中添加以下代码: ```xml <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 2. 然后,在适配器类中创建一个继承自 RecyclerView.Adapter 的自定义适配器类,例如 MyAdapter: ```java public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> { // 定义数据源 private List<String> mData; // 构造方法,传入数据源 public MyAdapter(List<String> data) { mData = data; } // 创建 ViewHolder @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false); return new MyViewHolder(view); } // 绑定 ViewHolder @Override public void onBindViewHolder(MyViewHolder holder, int position) { String item = mData.get(position); holder.textView.setText(item); } // 返回数据源大小 @Override public int getItemCount() { return mData.size(); } // 自定义 ViewHolder public static class MyViewHolder extends RecyclerView.ViewHolder { TextView textView; public MyViewHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.text_view); } } } ``` 3. 接下来,在布局文件中定义 RecyclerView 的子项布局,例如在 item_layout.xml 文件中添加一个 TextView: ```xml <TextView android:id="@+id/text_view" android:layout_width="match_parent" android:layout_height="wrap_content" /> ``` 4. 最后,在 Activity 或 Fragment 中设置 RecyclerView 的适配器,例如在 MainActivity.java 文件中添加以下代码: ```java public class MainActivity extends AppCompatActivity { private RecyclerView mRecyclerView; private MyAdapter mAdapter; private List<String> mData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化数据源 mData = new ArrayList<>(); mData.add("Item 1"); mData.add("Item 2"); mData.add("Item 3"); // 初始化 RecyclerView mRecyclerView = findViewById(R.id.recyclerView); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); // 创建适配器并设置给 RecyclerView mAdapter = new MyAdapter(mData); mRecyclerView.setAdapter(mAdapter); } } ``` 这样,一个简单的 RecyclerView 适配器就完成了。你可以根据需要自定义适配器的布局和数据源,并在 onBindViewHolder 方法中绑定数据到 ViewHolder 中的视图上。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值