自定义view(五)----自定义简单字母索引效果

实现效果:


1645170676956.gif

分析需求:
根据上图效果分析需求
1、一列字母,总共27个;
2、字母触摸时,触摸字母变色;
3、将触摸的字母显示在屏幕中间;
4、手指抬起时屏幕中间显示的字母消失

准备工作

既然是自定义view,一样的,先创建一个类继承自View,并实现它的四个构造方法,切记我在前面说的,每个构造方法依次调用它的下一个构造方法,这样可以保证无论哪种构造方法都能实现我们的业务代码。初创建的自定义view类如下:

class LetterSideBar : View {
    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
    ) { }
}

实现该效果主要有以下几步,步骤参照自定义view(二)----自定义动画View

1、分析view属性

上图可知,自定义view主要是右边一排字母(中间显示可以直接用textview),字母主要两个属性大小(LetterSize)和颜色(LetterColor)

2、自定义属性

如何创建自定义属性文件我就不多说了,说了很多了,直接贴代码

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ProcessBarView">
        <attr name="innerColor" format="color"/>
        <attr name="outerColor" format="color"/>
        <attr name="borderWidth" format="dimension"/>
        <attr name="NumTextColor" format="color"/>
        <attr name="NumTextSize" format="dimension"/>
    </declare-styleable>
</resources>

3、在布局文件中使用自定义属性

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingEnd="15dp"
    tools:context=".MainActivity">

    <com.example.viewday_05.LetterSideBar
        app:LetterSize="20px"
        android:id="@+id/letter_side_bar"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:LetterColor="@color/design_default_color_primary_variant"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

4、在自定义view中获取自定义属性

一般在第三个构造方法中初始化自定义属性,除了初始化自定义属性之外,我们也将后面要用到的画笔在构造方法中进行初始化。这里为了防止字母大小不合适导致重叠,我自定义了一个sp转px的方法。

 constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LetterSideBar)
        mLetterColor = typedArray.getColor(R.styleable.LetterSideBar_LetterColor, Color.BLUE)
        mLetterSize = typedArray.getDimensionPixelSize(R.styleable.LetterSideBar_LetterSize, 20)

        mPaint.isAntiAlias = true
        mPaint.textSize = spToPx(mLetterSize)
        mPaint.color = mLetterColor
    }

spToPx

//sp转px
    private fun spToPx(sp: Int): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            sp.toFloat(),
            resources.displayMetrics
        )
    }

5、重写onMeasure()方法

我们绘画的区域计算,高度为match_parent,可以直接使用MeasureSpec.getSize获取,宽度是wrap_content,需要计算,在计算时为了以防万一需要考虑padding的情况,那么绘制区域的宽度=左边padding+右边padding+文字的宽度,文字宽度可以直接使用画笔测量所以字母中任意一个字母的宽度,在这里我测量了'W'的宽度。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //宽度=左右的pading+文字的宽度(取决于画笔)
        val textWidth = mPaint.measureText("W")
        val width = paddingLeft + paddingRight + textWidth
        //高度可以直接获取
        val height = MeasureSpec.getSize(heightMeasureSpec)
        setMeasuredDimension(width.toInt(), height)
    }

6、重写ondraw()方法

重写ondraw()方法特别的难点在于计算画笔水平开始画的位置和竖直位置的基线,对于不了解基线的可以去看一下我的自定义view(一)----自定义TextView
绘制开始的水平位置x表示画笔开始画的水平坐标,它应该是等于绘制区域的一半减去字母宽度的一半(自己理解吧),竖直位置27个字母等高,先算出每个字母的平均高度h,依次往下排列,第N的字母的中间值就等于h/2+N*h,这个也是要考大家自己理解,不行的可以画个图帮助理解一下哈,有了某个字母的中间值就可以根据前面说到的办法来计算基线了,计算方法见代码。

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //算出一个字母的高度
        var itemHeight = height / letterArray.size
        for (i in 0 until letterArray.size) {
            var x =
                width / 2 - mPaint.measureText(letterArray[i].toString()) / 2 //字母水平居中 x应该等于getwidth()/2-文字宽度/2
            var letterCenterY = itemHeight / 2 + itemHeight * i//算出字母的中间线
            //算基线
            val fontMetricsInt = mPaint.fontMetricsInt
            val dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom
            val baseLine = letterCenterY + dy
            //触摸的字母高亮
            canvas.drawText(letterArray[i].toString(), x, baseLine.toFloat(), mPaint)
        }
    }

7、其他实现效果

到第六步完整个自定义view的绘制就完成了,现在可以在右边得到绘制的一列字母,接下来是一些其他的效果实现,未实现的效果如下:

  • 触摸字母变色
  • 触摸后将字母显示在屏幕中间
  • 松手时屏幕中间字母消失
    先来实现触摸字母变色,无非就是重写自定义view的onTouchEvent事件
    重写这个方法有两点需要注意
    1、怎么判断我们触摸的是哪个字母?
    这个的解决方法是通过高度来辨别触摸字母,我们可以拿到触摸的y坐标,每个字母竖直均匀排列,所以用触摸的y坐标除以每个字母的平均高度取整,就是字母数组的下标,从而拿到触摸的字母。
    2、触摸字母上方或者下方空白区域程序崩溃,怎么解决?
    这个问题是我在测试时发现,当我触摸显示字母区域上方或者下面时发生崩溃,原因是字母下标溢出,因为触摸上方时当前拿到的currentPosition是一个负数,触摸下方时拿到的currentPosition的值大于letterArray的下标,两种情况都会导致数组下标溢出,导致崩溃,所以需要加两个判断使触摸上方时为触摸第一个字母,触摸下方时为触摸最后一个字母。
 override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                //计算当前触摸字母
                val currentMoveY = event.y //拿到触摸的y坐标
                var itemHeight = height / letterArray.size//每个字母的高度
                var currentPosition = currentMoveY / itemHeight//触摸的是第几个字母
               
                if (currentPosition < 0) {
                    currentPosition = 0F
                }
                if (currentPosition > letterArray.size - 1) {
                    currentPosition = (letterArray.size - 1).toFloat()
                }
                mCurrentTouchLetter = letterArray[currentPosition.toInt()]
                //重新绘制
                invalidate()
            }
        }
        return true//实现触摸效果需要返回true
    }

拿到触摸字母后可以在onDraw()方法中重新绘制,将触摸字母变色,逻辑代码如下:

           //触摸的字母高亮
            if (letterArray[i] == mCurrentTouchLetter) {
                mPaint.color = Color.RED
                canvas.drawText(letterArray[i].toString(), x, baseLine.toFloat(), mPaint)
            } else {
                mPaint.color = Color.BLUE
                canvas.drawText(letterArray[i].toString(), x, baseLine.toFloat(), mPaint)
            }

接下来就是将触摸字母显示在布局中间,我们先在布局文件中实现一个TextView用于显示触摸字母,加入属性使它居中,并使用 android:visibility="gone"让他默认不显示,布局文件修改如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingEnd="15dp"
    tools:context=".MainActivity">
    <TextView
        android:visibility="gone"
        android:textColor="#FF0000"
        android:textSize="26sp"
        android:id="@+id/letter_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="A"/>

    <com.example.viewday_05.LetterSideBar
        app:LetterSize="20px"
        android:id="@+id/letter_side_bar"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:LetterColor="@color/design_default_color_primary_variant"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

定义触摸的回调事件
定义回调事件,在触摸时将触摸字母显示在TextView上,回调接口中的参数isTouch是为了判断是否触摸,方便松开时使显示效果消失。

 private lateinit var mLetterTouchListener: LetterTouchListener
    public fun setOnLetterTouchListener(listener: LetterTouchListener) {
        this.mLetterTouchListener = listener
    }

    //触摸回调接口
    public interface LetterTouchListener {
        public fun touch(letter: Char, isTouch: Boolean)
    }

在监听中实现接口方法
注意看代码中注释结合需求理解

 override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                //计算当前触摸字母
                val currentMoveY = event.y //拿到触摸的y坐标
                var itemHeight = height / letterArray.size//每个字母的高度
                var currentPosition = currentMoveY / itemHeight//触摸的是第几个字母
                /* 下面这两个判断着重解释一下,当我触摸显示字母区域的上方或者下方时,程序会崩溃,因为触摸上方时当前拿到的currentPosition是一个负数,
                 触摸下方时拿到的currentPosition的值大于letterArray的下标,两种情况都会导致数组下标溢出,导致崩溃,所以需要加两个判断*/
                if (currentPosition < 0) {
                    currentPosition = 0F
                }
                if (currentPosition > letterArray.size - 1) {
                    currentPosition = (letterArray.size - 1).toFloat()
                }
                mCurrentTouchLetter = letterArray[currentPosition.toInt()]
                if (mLetterTouchListener != null) {
                    mLetterTouchListener.touch(mCurrentTouchLetter, true)
                }
                //重新绘制
                invalidate()
            }
            //手指松开时消失
            MotionEvent.ACTION_UP -> {
                if (mLetterTouchListener != null) {
                    mLetterTouchListener.touch(mCurrentTouchLetter, false)
                }
            }

        }
        return true//实现触摸效果需要返回true
    }

在activity中进行回调并实现相应的需求

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        letter_side_bar.setOnLetterTouchListener(object : LetterSideBar.LetterTouchListener {
            override fun touch(letter: Char, switch: Boolean) {
                if (switch){
                    letter_tv.text=letter.toString()
                    letter_tv.visibility=View.VISIBLE
                }else{
                    //松开手指不显示
                    letter_tv.visibility=View.GONE
                }
            }

        })
    }
}

到这里整个需求也就完成了,本篇文章使用kotlin语言完成,不对的地方希望大佬指正,下面我把每个部分的完整代码贴出来,方便用到的cv。
LetterSideBar.kt

class LetterSideBar : View {
    //定义26个字母
    private var letterArray: ArrayList<Char> = arrayListOf(
        'A',
        'B',
        'C',
        'D',
        'E',
        'F',
        'G',
        'H',
        'I',
        'J',
        'K',
        'L',
        'M',
        'N',
        'O',
        'P',
        'Q',
        'R',
        'S',
        'T',
        'U',
        'V',
        'W',
        'X',
        'Y',
        'Z',
        '#'
    )
    private var mCurrentTouchLetter: Char = '\u0000'
    private var mPaint: Paint = Paint()
    private var mLetterColor: Int = 0
    private var mLetterSize: Int = 0


    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
    ) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LetterSideBar)
        mLetterColor = typedArray.getColor(R.styleable.LetterSideBar_LetterColor, Color.BLUE)
        mLetterSize = typedArray.getDimensionPixelSize(R.styleable.LetterSideBar_LetterSize, 20)

        mPaint.isAntiAlias = true
        mPaint.textSize = sp2px(mLetterSize)
        mPaint.color = mLetterColor
    }

    //sp转px
    private fun sp2px(sp: Int): Float {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            sp.toFloat(),
            resources.displayMetrics
        )
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //宽度=左右的pading+文字的宽度(取决于画笔)
        val textWidth = mPaint.measureText("W")
        val width = paddingLeft + paddingRight + textWidth
        //高度可以直接获取
        val height = MeasureSpec.getSize(heightMeasureSpec)
        setMeasuredDimension(width.toInt(), height)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //算出一个字母的高度
        var itemHeight = height / letterArray.size
        for (i in 0 until letterArray.size) {
            var x =
                width / 2 - mPaint.measureText(letterArray[i].toString()) / 2 //字母水平居中 x应该等于getwidth()/2-文字宽度/2
            var letterCenterY = itemHeight / 2 + itemHeight * i//算出字母的中间线
            //算基线
            val fontMetricsInt = mPaint.fontMetricsInt
            val dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom
            val baseLine = letterCenterY + dy
            //触摸的字母高亮
            if (letterArray[i] == mCurrentTouchLetter) {
                mPaint.color = Color.RED
                canvas.drawText(letterArray[i].toString(), x, baseLine.toFloat(), mPaint)
            } else {
                mPaint.color = Color.BLUE
                canvas.drawText(letterArray[i].toString(), x, baseLine.toFloat(), mPaint)
            }
        }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                //计算当前触摸字母
                val currentMoveY = event.y //拿到触摸的y坐标
                var itemHeight = height / letterArray.size//每个字母的高度
                var currentPosition = currentMoveY / itemHeight//触摸的是第几个字母
                /* 下面这两个判断着重解释一下,当我触摸显示字母区域的上方或者下方时,程序会崩溃,因为触摸上方时当前拿到的currentPosition是一个负数,
                 触摸下方时拿到的currentPosition的值大于letterArray的下标,两种情况都会导致数组下标溢出,导致崩溃,所以需要加两个判断*/
                if (currentPosition < 0) {
                    currentPosition = 0F
                }
                if (currentPosition > letterArray.size - 1) {
                    currentPosition = (letterArray.size - 1).toFloat()
                }
                mCurrentTouchLetter = letterArray[currentPosition.toInt()]
                if (mLetterTouchListener != null) {
                    mLetterTouchListener.touch(mCurrentTouchLetter, true)
                }
                //重新绘制
                invalidate()
            }
            //手指松开时消失
            MotionEvent.ACTION_UP -> {
                if (mLetterTouchListener != null) {
                    mLetterTouchListener.touch(mCurrentTouchLetter, false)
                }
            }

        }
        return true//实现触摸效果需要返回true
    }

    private lateinit var mLetterTouchListener: LetterTouchListener
    public fun setOnLetterTouchListener(listener: LetterTouchListener) {
        this.mLetterTouchListener = listener
    }

    //触摸回调接口
    public interface LetterTouchListener {
        public fun touch(letter: Char, isTouch: Boolean)
    }
}

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LetterSideBar">
        <attr name="LetterSize" format="dimension"/>
        <attr name="LetterColor" format="color"/>
    </declare-styleable>
</resources>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        letter_side_bar.setOnLetterTouchListener(object : LetterSideBar.LetterTouchListener {
            override fun touch(letter: Char, switch: Boolean) {
                if (switch){
                    letter_tv.text=letter.toString()
                    letter_tv.visibility=View.VISIBLE
                }else{
                    //松开手指不显示
                    letter_tv.visibility=View.GONE
                }
            }

        })
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingEnd="15dp"
    tools:context=".MainActivity">
    <TextView
        android:visibility="gone"
        android:textColor="#FF0000"
        android:textSize="26sp"
        android:id="@+id/letter_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="A"/>

    <com.example.viewday_05.LetterSideBar
        app:LetterSize="20px"
        android:id="@+id/letter_side_bar"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:LetterColor="@color/design_default_color_primary_variant"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,561评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,218评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,162评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,470评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,550评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,806评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,951评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,712评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,166评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,510评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,643评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,306评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,930评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,745评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,983评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,351评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,509评论 2 348

推荐阅读更多精彩内容