Android 文字索引列 (Kotlin 版)

シリーズ記事ディレクトリ

前回の記事Android Alphabet Index Sidebar (java版)に続き、Kotlin版の実装を完了し、Java版でいくつかの問題を解決しました(具体的な使い方については、現在のKotlinコード比較を参照してください)。



序文

通常の開発では、連絡先を開発する必要があるアプリケーションに遭遇した場合、Java で実装されたコードが Kotlin バージョンに変更され、使用中に以前のコードにいくつかの問題が見つかったため、このレターサイドバーは依然として非常に一般的です。たまたまkotlinコードも修正しました。

まだ未完成の問題があります:

1. 文字リストに加えて、他の値リストを設定し、サイドバー全体を再描画します
2. 選択した文字が効果を拡大します

時間があれば、これら 2 つの効果を引き続き実装することを検討できます。後でコードを追加します。

参照効果:動的効果については、以前の記事を参照してください。効果は同様です
ここに画像の説明を挿入


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

1. アイデア

1.通常のカスタムView、計測、描画の手順は必須です(今回はカスタムViewGroupではなく、onLayoutは使用しません)。
2. テキストの幅と高さを測定し (現在のテキスト リスト内の単一テキストの最大幅と高さを取得)、特定の幅と高さの測定値を計算し、setMeasuredDimension を使用して onMeasure の親ビューに設定します。 .
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