Android 开发加入索引吸顶效果

84 阅读2分钟

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)

预览效果如下

titleup 00_00_00-00_00_30.gif

想查看更多实用的demo源码,可以下载码农宝查看 码农宝下载链接