Android letter index column (Kotlin version)

Series Article Directory

Continuing from the previous article Android Alphabet Index Sidebar (java version) , complete the implementation of the Kotlin version, and solve some problems in the java version (for specific usage, please refer to the current Kotlin code comparison).



foreword

In normal development, when encountering an application that needs to develop contacts, this letter sidebar is still very common, because the code implemented in java used to be changed to the Kotlin version, and some problems with the previous code were found during use. Just happened to make a correction in the kotlin code too.

There are still unfinished issues:

1. In addition to the letter list, set other value lists, and redraw the entire sidebar
2. The selected letter enlarges the effect

I can consider continuing to implement these two effects when I have time, and I will add code later

Reference effect: For the dynamic effect, please refer to the previous article, the effect is similar
insert image description here


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

1. Ideas

1. The steps of normal custom View, measurement and drawing are indispensable (this time it is not a custom ViewGroup, and onLayout is not used).
2. Measure the width and height of the text (both take the maximum width and height of a single text in the current text list), then calculate the specific width and height measurement values, and use setMeasuredDimension to set them to the parent View in onMeasure.
3. Finally, draw in onDraw

Two, implement the code

1. Each resource value

The codes for colors.xml and dimens.xml values ​​are as follows (example):

<!--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. Code implementation

The code is as follows (example):

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

The last is the specific use, use it in xml, this will not give the code, everyone should know


Summarize

The above code simply implements the search bar on the side of the letter. The code is for reference only, and you can modify it according to your needs.
Correctly complete the unfinished questions at the beginning of the article, and update the code later.

Guess you like

Origin blog.csdn.net/u013855006/article/details/125739300