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中使用,这就不给代码了,大家应该都会


总结

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

猜你喜欢

转载自blog.csdn.net/u013855006/article/details/125739300