自定義view(五)----自定義簡單字母索引效果

實現(xiàn)效果:


1645170676956.gif

分析需求:
根據(jù)上圖效果分析需求
1、一列字母,總共27個;
2、字母觸摸時,觸摸字母變色;
3、將觸摸的字母顯示在屏幕中間;
4、手指抬起時屏幕中間顯示的字母消失

準備工作

既然是自定義view,一樣的,先創(chuàng)建一個類繼承自View,并實現(xiàn)它的四個構造方法,切記我在前面說的,每個構造方法依次調用它的下一個構造方法,這樣可以保證無論哪種構造方法都能實現(xiàn)我們的業(yè)務代碼。初創(chuàng)建的自定義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
    ) { }
}

實現(xiàn)該效果主要有以下幾步,步驟參照自定義view(二)----自定義動畫View

1、分析view屬性

上圖可知,自定義view主要是右邊一排字母(中間顯示可以直接用textview),字母主要兩個屬性大?。↙etterSize)和顏色(LetterColor)

2、自定義屬性

如何創(chuàng)建自定義屬性文件我就不多說了,說了很多了,直接貼代碼

<?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()方法

我們繪畫的區(qū)域計算,高度為match_parent,可以直接使用MeasureSpec.getSize獲取,寬度是wrap_content,需要計算,在計算時為了以防萬一需要考慮padding的情況,那么繪制區(qū)域的寬度=左邊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表示畫筆開始畫的水平坐標,它應該是等于繪制區(qū)域的一半減去字母寬度的一半(自己理解吧),豎直位置27個字母等高,先算出每個字母的平均高度h,依次往下排列,第N的字母的中間值就等于h/2+N*h,這個也是要考大家自己理解,不行的可以畫個圖幫助理解一下哈,有了某個字母的中間值就可以根據(jù)前面說到的辦法來計算基線了,計算方法見代碼。

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、其他實現(xiàn)效果

到第六步完整個自定義view的繪制就完成了,現(xiàn)在可以在右邊得到繪制的一列字母,接下來是一些其他的效果實現(xiàn),未實現(xiàn)的效果如下:

  • 觸摸字母變色
  • 觸摸后將字母顯示在屏幕中間
  • 松手時屏幕中間字母消失
    先來實現(xiàn)觸摸字母變色,無非就是重寫自定義view的onTouchEvent事件
    重寫這個方法有兩點需要注意
    1、怎么判斷我們觸摸的是哪個字母?
    這個的解決方法是通過高度來辨別觸摸字母,我們可以拿到觸摸的y坐標,每個字母豎直均勻排列,所以用觸摸的y坐標除以每個字母的平均高度取整,就是字母數(shù)組的下標,從而拿到觸摸的字母。
    2、觸摸字母上方或者下方空白區(qū)域程序崩潰,怎么解決?
    這個問題是我在測試時發(fā)現(xiàn),當我觸摸顯示字母區(qū)域上方或者下面時發(fā)生崩潰,原因是字母下標溢出,因為觸摸上方時當前拿到的currentPosition是一個負數(shù),觸摸下方時拿到的currentPosition的值大于letterArray的下標,兩種情況都會導致數(shù)組下標溢出,導致崩潰,所以需要加兩個判斷使觸摸上方時為觸摸第一個字母,觸摸下方時為觸摸最后一個字母。
 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//實現(xiàn)觸摸效果需要返回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)
            }

接下來就是將觸摸字母顯示在布局中間,我們先在布局文件中實現(xiàn)一個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上,回調接口中的參數(shù)isTouch是為了判斷是否觸摸,方便松開時使顯示效果消失。

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

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

在監(jiān)聽中實現(xiàn)接口方法
注意看代碼中注釋結合需求理解

 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//觸摸的是第幾個字母
                /* 下面這兩個判斷著重解釋一下,當我觸摸顯示字母區(qū)域的上方或者下方時,程序會崩潰,因為觸摸上方時當前拿到的currentPosition是一個負數(shù),
                 觸摸下方時拿到的currentPosition的值大于letterArray的下標,兩種情況都會導致數(shù)組下標溢出,導致崩潰,所以需要加兩個判斷*/
                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//實現(xiàn)觸摸效果需要返回true
    }

在activity中進行回調并實現(xiàn)相應的需求

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//觸摸的是第幾個字母
                /* 下面這兩個判斷著重解釋一下,當我觸摸顯示字母區(qū)域的上方或者下方時,程序會崩潰,因為觸摸上方時當前拿到的currentPosition是一個負數(shù),
                 觸摸下方時拿到的currentPosition的值大于letterArray的下標,兩種情況都會導致數(shù)組下標溢出,導致崩潰,所以需要加兩個判斷*/
                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//實現(xiàn)觸摸效果需要返回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>
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容