實現(xiàn)效果:

分析需求:
根據(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>