Android字母索引栏(Kotlin版本)

系列文章目录

接上一篇Android字母索引侧边栏(java版本),完成Kotlin版本的实现,并且解决java版本中的一些问题(具体使用可以参考对比当前这篇Kotlin的代码)。



前言

平常开发中遇到需要开发联系人的应用,这个字母侧边栏还是挺常用的,因为以前是java实现的代码,现在刚好改成Kotlin的版本,在使用中也发现一些以前的代码的问题,刚好也在kotlin代码中做一个修正。

还有未完成的问题:

1、除开字母列表之外,设置其他值列表,并且重新绘制整个侧边栏
2、选中的字母放大效果

这两个效果等有时间我可以考虑继续实现,后续补充代码

参考效果:动态的效果可以参考上一篇文章,效果类似
在这里插入图片描述


提示:以下是本篇文章正文内容,下面案例可供参考

一、思路

1、正常的自定义View的几个步骤,测量、绘制不能少(本次不是自定义ViewGroup,也没有用到onLayout)。
2、测量文本的宽高(都取当前文本列表单个文本最大的宽高),然后计算出具体的宽、高测量值,并且在onMeasure中使用setMeasuredDimension设置给父View。
3、最终就是在onDraw中绘制

二、实现代码

1. 各资源值

colors.xml和dimens.xml值代码如下(示例):

<!--LetterSidebar-->
<color name="side_text_normal_color">#000000</color>
<color name="side_text_select_color">#000000</color>
<color name="side_select_shape_color">#3ACF40</color>

<!--LetterSidebar-->
<dimen name="side_text_normal_size">12sp</dimen>
<dimen name="side_text_select_size">12sp</dimen>
<!--文本绘制的过程中,默认增加的偏移量,为了选中背景的绘制-->
<dimen name="side_default_offset_wh">2dp</dimen>

自定义属性

<!--字母搜索侧边栏-->
    <declare-styleable name="LetterSidebar">
        <attr name="side_text_normal_size" format="dimension"/>
        <attr name="side_text_select_size" format="dimension"/>
        <attr name="side_text_normal_color" format="color"/>
        <attr name="side_text_select_color" format="color"/>
        <attr name="side_text_gravity" format="enum">
            <enum name="start" value="1"/>
            <enum name="center" value="2"/>
        </attr>
        <attr name="side_select_shape" format="enum">
            <enum name="circle" value="1"/>
            <enum name="square" value="2"/>
        </attr>
        <attr name="side_select_shape_color" format="color"/>
    </declare-styleable>

2.代码实现

代码如下(示例):

private const val TAG = "LetterSidebar"
private const val TWO_TIMES = 2
/**
 * 字母侧边栏.
 */
class LetterSidebar: View {

    /**
     * 字母选中背景形状,圆、矩形。
     */
    annotation class SelectShape {
        companion object {
            // 圆
            const val CIRCLE = 1

            // 矩形
            const val SQUARE = 2
        }
    }

    /**
     * 绘制的字母位置,从左开始或者居中.
     */
    annotation class TextGravityY {
        companion object {
            // 从左开始
            const val GRAVITY_START = 1

            // 居中
            const val GRAVITY_CENTER = 2
        }
    }

    // 字母和特殊符号列表
    private val mLetterList: MutableList<String> = mutableListOf()
    // 未选中文字大小
    private var mTextNormalSize = 0f
    // 选中文字大小
    private var mTextSelectSize = 0f
    // 文本绘制的过程中,默认增加的偏移量(乘以2使用,因为同时给上下左右增加),为了选中背景的绘制
    private var mDefaultOffsetWh = 0f
    // 未选中文字颜色
    private var mTextNormalColor = 0
    // 选中文字颜色
    private var mTextSelectColor = 0
    // 文字显示位置
    private var mTextGravity = TextGravityY.GRAVITY_CENTER
    // 选中之后的背景图形
    private var mSelectShape = SelectShape.CIRCLE
    // 选中之后背景图形颜色
    private var mSelectShapeColor = 0
    // 选中背景图形半径
    private var mSelectShapeRadius = 0f
    // 控件的默认宽高
    private var mDefaultWidth = 0
    private var mDefaultHeight = 0
    // 文字的画笔
    private var mTextPaint: Paint? = null
    // 选中背景的画笔
    private var mShapePaint: Paint? = null
    // 控件的宽高
    private var mWidth = 0
    private var mHeight = 0
    // 触摸选中的位置, 默认未触摸选中
    private var mPosition = -1
    // 记录上一次触摸的位置,避免重复调用
    private var mPrePosition = -1
    // 计算单个字符所占用的高度
    private var mSingleTxtHeight = 0f
    // 判断当前手指是否触摸在View上
    private var mIsTouch = false
    // 回调监听
    private var mOnLetterChangedListener: OnLetterChangedListener? = null


    constructor(context: Context?) : this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init()
        initAttrs(attrs)
    }

    private fun init() {
        // 给字母列表添加字母和特殊符号
        for (i in 'A'.code..'Z'.code + 1) {
            val ch = if (i > 'Z'.code) {
                '#'
            } else {
                i.toChar()
            }
            mLetterList.add(ch.toString())
        }
        mTextNormalSize = context.resources.getDimension(R.dimen.side_text_normal_size)
        mTextSelectSize = context.resources.getDimension(R.dimen.side_text_select_size)
        mDefaultOffsetWh = context.resources.getDimension(R.dimen.side_default_offset_wh)
        mTextNormalColor = context.getColor(R.color.side_text_normal_color)
        mTextSelectColor = context.getColor(R.color.side_text_select_color)
        mSelectShapeColor = context.getColor(R.color.side_select_shape_color)

        mTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)
        mShapePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    }

    private fun initAttrs(attrs: AttributeSet?) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LetterSidebar)
        mTextNormalSize = typedArray.getDimension(R.styleable.LetterSidebar_side_text_normal_size, mTextNormalSize)
        mTextSelectSize = typedArray.getDimension(R.styleable.LetterSidebar_side_text_select_size, mTextSelectSize)
        mTextNormalColor = typedArray.getColor(R.styleable.LetterSidebar_side_text_normal_color, mTextNormalColor)
        mTextSelectColor = typedArray.getColor(R.styleable.LetterSidebar_side_text_select_color, mTextSelectColor)
        mTextGravity = typedArray.getInt(R.styleable.LetterSidebar_side_text_gravity, mTextGravity)
        mSelectShape = typedArray.getInt(R.styleable.LetterSidebar_side_select_shape, mSelectShape)
        mSelectShapeColor = typedArray.getColor(R.styleable.LetterSidebar_side_select_shape_color, mSelectShapeColor)
        typedArray.recycle()

        mTextPaint?.color = mTextNormalColor
        mTextPaint?.textSize = mTextNormalSize
        mShapePaint?.color = mSelectShapeColor

        mDefaultWidth = if (mTextNormalSize > mTextSelectSize) {
            (mTextNormalSize + mDefaultOffsetWh.times(TWO_TIMES)).toInt()
        } else {
            (mTextSelectSize + mDefaultOffsetWh.times(TWO_TIMES)).toInt()
        }
        mDefaultHeight = getDefaultHeight()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val width = getViewSize(mDefaultWidth + paddingStart + paddingEnd, widthMeasureSpec)
        val height = getViewSize(mDefaultHeight + paddingTop + paddingBottom, heightMeasureSpec)
        // Logger.d(TAG, "getViewSize mDefaultHeight:: $mDefaultHeight, height:: $height")
        // Logger.d(TAG, "getViewSize paddingStart::$paddingStart, paddingEnd::$paddingEnd, paddingTop::$paddingTop, paddingBottom::$paddingBottom")
        setMeasuredDimension(width, height)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mWidth = w
        mHeight = h

        if (mLetterList.isEmpty()) {
            return
        }
        mSingleTxtHeight = (mHeight - paddingTop - paddingBottom).toFloat().div(mLetterList.size)
        // 选中背景圆形的半径
        mSelectShapeRadius = mSingleTxtHeight.div(TWO_TIMES)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        for (i in 0 until mLetterList.size) {
            if (i == mPosition) {
                drawSelect(canvas, mLetterList[i], i)
            } else {
                drawNormal(canvas, mLetterList[i], i)
            }
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        mIsTouch = false
        when (event?.action) {
            MotionEvent.ACTION_DOWN,
            MotionEvent.ACTION_MOVE -> {
                mIsTouch = true
                // 获取触摸位置的Y坐标
                val y = event.y
                mPosition = getPosition(y)
                if (mPosition != mPrePosition && mPosition >= 0) {
                    mOnLetterChangedListener?.onChanged(mLetterList[mPosition], mPosition)
                    mPrePosition = mPosition
                }
            }
            MotionEvent.ACTION_UP,
            MotionEvent.ACTION_CANCEL -> {
                mPosition = -1
                mIsTouch = false
            }
            else -> {
                mIsTouch = false
            }
        }
        invalidate()
        mOnLetterChangedListener?.onTouch(mIsTouch)
        return mIsTouch
    }

    private fun getViewSize(size: Int, measureSpec: Int): Int {
        var result = size
        val specMode = MeasureSpec.getMode(measureSpec)
        val specSize = MeasureSpec.getSize(measureSpec)
        when (specMode) {
            MeasureSpec.EXACTLY -> {
                result = specSize
            }
            MeasureSpec.AT_MOST -> {
                result = min(size, specSize)
            }
            MeasureSpec.UNSPECIFIED -> {
                result = size
            }
        }
        return result
    }

    private fun getDefaultHeight(): Int {
        if (mLetterList.isEmpty()) {
            return 0
        }
        mTextPaint?.let { paint ->
            paint.textSize = if (mTextNormalSize > mTextSelectSize) {
                mTextNormalSize
            } else {
                mTextSelectSize
            }
            //var tempHeight = 0
            var maxLetterHeight = 0
            for (letter in mLetterList) {
                //tempHeight += (getTextHeight(letter, paint) + mDefaultOffsetWh.times(TWO_TIMES).toInt())
                val textHeight = getTextHeight(letter, paint)
                if (textHeight > maxLetterHeight) {
                    maxLetterHeight = textHeight
                }
            }

            return (maxLetterHeight + mDefaultOffsetWh.times(TWO_TIMES)).times(mLetterList.size).toInt()
        }
        return 0
    }

    /**
     * 获取文字的高度.
     *
     * @return 文本高度
     */
    private fun getTextHeight(text: String, paint: Paint): Int {
        val rect = Rect()
        paint.getTextBounds(text, 0, text.length, rect)
        return rect.bottom - rect.top
    }

    /**
     * 计算当前触摸的字母的position。
     *
     * @param y 当前触摸的屏幕的位置
     * @return 返回字母的position
     */
    private fun getPosition(y: Float): Int {
        return if (y < paddingTop || y > mHeight - paddingBottom || mHeight <= 0 || mLetterList.isEmpty()) {
            -1
        } else {
            (y - paddingTop).div(mHeight - paddingTop - paddingBottom).times(mLetterList.size).toInt()
        }
    }

    /**
     * 绘制选中的样式.
     *
     * @param canvas 画布
     * @param letter 需要绘制的字母
     * @param index 绘制的下标
     */
    private fun drawSelect(canvas: Canvas?, letter: String, index: Int) {
        if (canvas == null) {
            return
        }
        mTextPaint?.let { paint ->
            paint.color = mTextSelectColor
            paint.textSize = mTextSelectSize
            val letterWidth = paint.measureText(letter)
            val letterHeight = getTextHeight(letter, paint)
            // 计算文本绘制的(x,y),默认是该字母的居中绘制坐标
            val xPos = if (mTextGravity == TextGravityY.GRAVITY_CENTER) {
                // 绘制在中间
                paddingStart + (mWidth - paddingStart - paddingEnd - letterWidth).div(TWO_TIMES)
            } else {
                // 从左侧开始绘制
                paddingStart.toFloat()
            }
            var textOffset = 0f
            if (mSingleTxtHeight > letterHeight) {
                textOffset = (mSingleTxtHeight - letterHeight).div(TWO_TIMES)
            }
            val yPos = paddingTop + mSingleTxtHeight.times(index + 1) - textOffset

            // 绘制背景
            if (mSelectShape == SelectShape.CIRCLE) {
                val cy = paddingTop + mSingleTxtHeight.times(index + 1) - mSelectShapeRadius
                val cx = paddingStart + (mWidth - paddingStart - paddingEnd).div(TWO_TIMES)
                mShapePaint?.let {
                    canvas.drawCircle(cx.toFloat(), cy, mSelectShapeRadius, it)
                }
            } else {
                val left = paddingStart.toFloat()
                val top = paddingTop + mSingleTxtHeight.times(index)
                val right = (mWidth - paddingEnd).toFloat()
                val bottom = paddingTop + mSingleTxtHeight.times(index + 1)
                mShapePaint?.let {
                    canvas.drawRect(left, top, right, bottom, it)
                }
            }

            // 绘制文本
            canvas.drawText(letter, xPos, yPos, paint)
        }
    }

    /**
     * 绘制默认的样式.
     *
     * @param canvas 画布
     * @param letter 需要绘制的字母
     * @param index 绘制的下标
     */
    private fun drawNormal(canvas: Canvas?, letter: String, index: Int) {
        if (canvas == null) {
            return
        }
        mTextPaint?.let { paint ->
            paint.color = mTextNormalColor
            paint.textSize = mTextNormalSize
            val letterWidth = paint.measureText(letter)
            val letterHeight = getTextHeight(letter, paint)
            // 计算文本绘制的(x,y),默认是该字母的居中绘制坐标
            val xPos: Float = if (mTextGravity == TextGravityY.GRAVITY_CENTER) {
                // 绘制在中间
                paddingStart + (mWidth - paddingStart - paddingEnd - letterWidth).div(TWO_TIMES)
            } else {
                // 从左侧开始绘制
                paddingStart.toFloat()
            }
            var textOffset = 0f
            if (mSingleTxtHeight > letterHeight) {
                textOffset = (mSingleTxtHeight - letterHeight).div(TWO_TIMES)
            }
            val yPos = paddingTop + mSingleTxtHeight.times(index + 1) - textOffset
            canvas.drawText(letter, xPos, yPos, paint)
        }
    }

    /**
     * 设置字母侧边栏回调监听.
     *
     * @param listener 回调监听
     */
    fun setOnLetterChangedListener(listener: OnLetterChangedListener) {
        this.mOnLetterChangedListener = listener
    }

    /**
     * 设置字母侧边栏回调监听.
     *
     * @param onChanged 选中字母的监听.
     * @param onTouch 是否被触摸.
     */
    fun setOnLetterChangedListener(
        onChanged: (letter: String, position: Int) -> Unit,
        onTouch: (isTouch: Boolean) -> Unit
    ) {
        mOnLetterChangedListener = object : OnLetterChangedListener {
            override fun onChanged(letter: String, position: Int) {
                onChanged(letter, position)
            }

            override fun onTouch(isTouch: Boolean) {
                onTouch(isTouch)
            }
        }
    }

    interface OnLetterChangedListener {
        /**
         * 选中字母的监听.
         *
         * @param letter 选中的字母
         * @param position 选中字母的下标
         */
        fun onChanged(letter: String, position: Int)

        /**
         * 是否被触摸.
         *
         * @param isTouch {@true} 触摸
         */
        fun onTouch(isTouch: Boolean)
    }
}

最后就是具体使用,在xml中使用,这就不给代码了,大家应该都会


总结

以上代码简单实现了字母侧边搜索栏,代码仅供参考,大家可以根据自己需求修改。
正确后续完成文章开头未完成的问题,后续更新代码。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃骨头不吐股骨头皮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值