import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.purui.mobile.R
import com.purui.mobile.base.dip2sp
import com.purui.mobile.utils.ResUtils
abstract class StickyItemDecoration: RecyclerView.ItemDecoration() {
companion object {
// 边距
private val indexPaddingHorizontal = ResUtils.getDimenF(R.dimen.letter_padding_start)
// 索引文字颜色
private val indexFontColor = ResUtils.getColor(R.color.font_color_dark)
// 索引条背景颜色
private val indexBgColor = ResUtils.getColor(R.color.divider_color)
// 索引条高度
private val indexHeight = ResUtils.getDimen(R.dimen.letter_height)
// 文字大小
private val indexTextSize:Float = dip2sp(16f)
}
private val mIndexBgPaint by lazy { Paint() }
private val mTextPaint by lazy { Paint() }
init {
mTextPaint.let {
it.textSize = indexTextSize
it.isAntiAlias = true
it.color = indexFontColor
}
mIndexBgPaint.let {
it.isAntiAlias = true
it.color = indexBgColor
}
}
/**
* recyclerView 绘制 onDraw -> item.onDraw -> onDrawOver
*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (!showIndex()) {
return
}
for (index in 0 until parent.childCount) {
val childView = parent.getChildAt(index)
childView?.let {childViewNotNull->
// 绘制每个索引条
val indexRect = Rect()
val position = parent.getChildAdapterPosition(childViewNotNull)
if (isIndexItem(position) && position >= 0) {
// 控制索引条的绘制位置
indexRect.apply {
top = childViewNotNull.top - indexHeight
bottom = childViewNotNull.top
left = parent.paddingLeft
right = parent.width - parent.paddingRight
}
// 绘制索引条背景
c.drawRect(indexRect, mIndexBgPaint)
// 绘制索引条文字
c.drawText(getIndexTitle(position),
indexPaddingHorizontal,
getBaseLineY(paint = mTextPaint, centerY = indexRect.centerY()),
mTextPaint)
}
}
}
}
private fun getBaseLineY(paint:Paint,centerY:Int):Float {
return centerY - ( paint.fontMetricsInt.bottom + paint.fontMetricsInt.top ) / 2f
}
/**
* recyclerView 绘制 onDraw -> item.onDraw -> onDrawOver
* 绘制覆盖在 item 上面的索引条
*/
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (!showIndex()) {
return
}
val firstView = parent.getChildAt(0)
var nextView:View? = null;
if (1 < parent.childCount) {
nextView = parent.getChildAt(1) // 下一个 item
}
firstView?.let { firstViewNotNull->
val floatIndexRect = Rect()
val position = parent.getChildAdapterPosition(firstViewNotNull)
val firstLetterWillMeetSecondLetter = firstViewNotNull.bottom - indexHeight < parent.paddingTop
val secondItemViewIsTimeItem = if (nextView == null) false else isIndexItem(parent.getChildAdapterPosition(nextView))
val currentTitleNotEqualsToNextTitle = if (nextView == null) false else !isSameIndexTitle(parent.getChildAdapterPosition(nextView),position)
// 如果下一个 view 不为空,第一个 索引 快要接触到下一个 索引,下一个 item 需要显示 索引, 下一个 item 和 当前 item 索引文字不同
if (firstLetterWillMeetSecondLetter
&& secondItemViewIsTimeItem
&& currentTitleNotEqualsToNextTitle) {
// 跟随最后一个 item 向上推
floatIndexRect.top = firstViewNotNull.bottom - indexHeight // 第一个 item 的底部为悬浮提示的索引的底部 (显示的索引推出列表外)
floatIndexRect.bottom = firstViewNotNull.bottom
} else {
// 顶部固定悬浮
floatIndexRect.top = parent.paddingTop
floatIndexRect.bottom = floatIndexRect.top + indexHeight
}
floatIndexRect.left = parent.paddingLeft
floatIndexRect.right = parent.width - parent.paddingRight
// 绘制索引条背景
c.drawRect(floatIndexRect, mIndexBgPaint)
// 绘制索引条文字
c.drawText(getIndexTitle(position),
indexPaddingHorizontal,
getBaseLineY(paint = mTextPaint, centerY = floatIndexRect.centerY()),
mTextPaint)
}
}
/**
* 用于设置item周围的偏移量的,类似于设置padding喝margin效果,
* 空出的效果用于绘制分割线
*/
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (!showIndex()) {
return
}
val position:Int = parent.getChildAdapterPosition(view)
if (position >=0 && isIndexItem(position)) {
outRect.top = indexHeight
} else{
outRect.top = 0
}
}
/**
* 外部决定是否索引 item
*/
abstract fun isIndexItem(position: Int):Boolean
/**
* 索引标题
*/
abstract fun getIndexTitle(position: Int):String
/**
* 相同索引标题
*/
abstract fun isSameIndexTitle(position1: Int, position2: Int):Boolean
/**
* 显示索引 (当你有搜索模式不想显示的索引的时候使用)
*/
abstract fun showIndex():Boolean
}
使用时这么控制 recyclerview
recyclerview.addItemDecoration(object : StickyItemDecoration() {
override fun isIndexItem(position: Int) = if (list.size <= 0 || position < 0) false else list[position].isHeader
override fun getIndexTitle(position: Int) = if (list.size <= 0 || position < 0) "" else list[position].showHeader?:""
override fun isSameIndexTitle(position1: Int, position2: Int) = if (list.size <= 0 || position1 < 0 || position2 < 0) true else list[position1].showHeader == list[position2].showHeader
override fun showIndex() = true
})
所以对应的 model 需要加入这两个字段,控制需要显示索引的位置
data class XXXModel(
...
var showLetterHeader: String? = null,
var isLetterHeader: Boolean = false)
预览效果如下
想查看更多实用的demo源码,可以下载码农宝查看 码农宝下载链接