实现效果:
分析需求:
根据上图效果分析需求
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>