最近比较忙,研讨杂乱的东西需求大量集中的时刻,可是又抽不出来,就写点简单的东西吧。车载应用开发中有一个几乎避不开的自界说View,便是收音机的刻度条。本篇文章咱们来研讨怎么制作一个收音机的刻度尺。
本系列文章的目的是在解说自界说View是怎么完结的,阅览时,注意一些通用作用的完结方式,而不要仅仅局限于怎么完结本文中提到的刻度尺。
本文触及的的知识点如下:
-
自界说View时的一些常识,例如:怎么处理
layout_height
、layout_width
; -
刻度的制作;
-
OverScroller介绍,以及怎么完结惯性滑动;
-
滑动方位批改,完结刻度吸附作用。
完结思路
写一个 Android 收音机 UI 上的刻度尺 View 的根本思路如下:
- 第一步,创立一个自界说 View 类,继承自 View 并依据需求重写构造办法和onMeasure、onLayout、onDraw 办法。
- 第二步,在 onDraw 办法中运用 Canvas 和 Paint 方针来制作刻度尺的各个部分,包括刻度线,刻度值,指示器等。
- 第三步,运用 scrollBy 或许 scrollTo 办法来完结刻度尺的滑动作用,并运用 Scroller 或许 OverScroller 方针来完结惯性滑动作用。
- 第四步,在自界说 View 类中界说一些接口或许回调办法,用于与外部进行通讯和交互。
下面咱们来一一完结。
完结过程
界说View的宽、高
界说View的宽、高便是重写onMeasure办法。还记得View丈量形式的意义吗?没关系,咱们简单回想一下即可。
- MeasureSpec.EXACTLY(精确形式)
当咱们在xml中将为layout_height
或layout_width
设定为match_parent或许详细的值时,在onMeasure
时对应的丈量形式就会是EXACTLY
,表明当时View的高度或宽度值是现已确定好的。
所以,在EXACTLY
时咱们一般不会批改体系丈量出的值,直接将其用作当时View的高度或宽度。
- MeasureSpec.AT_MOST(至多形式)
当咱们在xml中将为layout_height
或layout_width
设定为wrap_content时,在onMeasure
时对应的丈量形式就会是AT_MOST
,表明体系并不知道当时View的高度和宽度,可是有一个确定规模值,只要不超过体系给出值,都能够。
所以,在EXACTLY
时咱们需求核算出当时View需求的高度和宽度(也便是View默认宽高)。大于、等于体系的丈量值时,运用体系的丈量值;小于体系的丈量值时,运用咱们自己的。
- MeasureSpec.UNSPECIFIED(不约束形式)
当自界说View的父布局时ScrollView一类,能够跟从子View巨细改动自身巨细的View时,在onMeasure
时对应的丈量形式就会是UNSPECIFIED
。处理方式不定,一般也能够直接采用体系的丈量值。
在本例中,咱们只核算View的高度,宽度运用体系丈量好的即可。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
measureHeight(heightMeasureSpec)
)
}
private fun measureHeight(heightMeasureSpec: Int): Int {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var height = 0
when (heightMode) {
// MeasureSpec.EXACTLY 为match_parent或许详细的值
MeasureSpec.EXACTLY -> {
height = heightSize
}
// MeasureSpec.AT_MOST 为wrap_content
MeasureSpec.AT_MOST -> {
// 高度 = 刻度尺长刻度的高度 + 上边距 + 下边距
height = longScaleHeight.coerceAtLeast(pointHeight) + paddingTop + paddingBottom
// 假如高度大于父容器给的高度,则取父容器给的高度
height = height.coerceAtMost(heightSize)
}
// MeasureSpec.UNSPECIFIED 父容器关于子容器没有任何约束,子容器想要多大就多大,多呈现于ScrollView
MeasureSpec.UNSPECIFIED -> {
height = heightSize
}
}
return height
}
为了支撑xml中的padding特色,在计量高度时还需求加上paddingTop、paddingBottom。然后,在onSizeChanged
办法中咱们会得到View的最终宽、高,并据此核算出刻度条长指针、短指针的高度。
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
initParams()
}
private fun initParams() {
// 长刻度的高度 = 控件高度 - 上边距 - 下边距
longScaleHeight = height - paddingTop - paddingBottom
// 短刻度的高度 = 长刻度的高度 - 15dp
shortScaleHeight = longScaleHeight - 15.dp
}
到此,咱们界说的View现已能够正确处理xml中的layout_height
和layout_width
特色了。接下来咱们开端制作刻度条。
制作刻度
制作刻度原理并不杂乱,思路如下:
- 运用刻度尺的最大值 – 最小值,得到总的刻度数
- 循环总刻度数,运用drawLine制作出线条
- 制作出收音机刻度尺中心的游标
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
for (i in 0..scaleCount) {
// 点从下往上制作
// 刻度开端的x坐标 = 刻度距离 * i + 左边距
val x1 = scaleSpace * i
// 刻度开端的y坐标 = 上边距
val y1 = height - paddingBottom
// 刻度结尾的x坐标 = 刻度开端的x坐标
val x2 = x1
// 刻度结尾的y坐标 = 控件高度 - 下边距 - 刻度高度
val y2 = height - paddingBottom - (if (i % 10 == 0) longScaleHeight else shortScaleHeight)
// 制作表尺刻度
canvas?.drawLine(
x1.toFloat(), y1.toFloat(),
x2.toFloat(), y2.toFloat(),
linePaint
)
drawCenterLine(canvas)
}
}
private fun drawCenterLine(canvas: Canvas?) {
// 表尺中心点的x坐标 +翻滚的距离是为了让中心点一直在屏幕中心
val centerPointX = viewWidth / 2 + scrollX
// 中心刻度的开端y坐标 = 上边距 - 5dp(加长5dp)
val centerStartPointY = paddingTop - 5.dp
// 中心刻度的结尾y坐标 = 控件高度 - 下边距 + 5dp(加长5dp)
val centerEndPointY = viewHeight - paddingBottom + 5.dp
canvas?.drawLine(
centerPointX.toFloat(), centerStartPointY.toFloat(),
centerPointX.toFloat(), centerEndPointY.toFloat(),
pointPaint
)
}
完结上述步骤咱们就能够看到下面的作用。
这一步,咱们制作出了刻度尺的刻度和游标,现已能够看到刻度尺根本的雏形了。接下来,咱们持续完结刻度尺的滑动。
接触滑动
惯例完结接触滑动的方式之一是重写OnTouchEvent
办法,依据接触移动时的坐标判别是否处于滑动状况。本例中,咱们运用手势辨认类 – GestureDetector
。
GestureDetector
是一个用于检测用户在屏幕上的手势操作的类,它能够辨认一些根本的手势,如按下、抬起、滑动、长按、轻击、快速滑动等。
它的运用办法是创立一个GestureDetector
实例,并传入一个GestureDetector.OnGestureListener
接口,该接口界说了一些办法,用于处理不同的手势事情。在需求检测手势的View或Activity中,将接触事情传递给GestureDetector
的onTouchEvent
办法,从而让GestureDetector
响应接触事情并调用相应的监听器办法。
private val gestureDetector by lazy { GestureDetector(context, touchGestureListener) }
private val touchGestureListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
return true
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
// 当监听到滑动事情时,翻滚到指定方位
scrollBy(distanceX.toInt(), 0)
return true
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
//
return gestureDetector.onTouchEvent(event!!)
}
上面的代码中,当GestureDetector
监听到接触手势为滑动onScroll
时,调用View.scrollBy()办法,将当时View的内容移动对应的滑动距离。onScroll
的回调值 distanceX 便是手指在x轴(水平)上滑动的距离。
这儿注意scrollBy
和scrollTo
的差异和特色:
-
scrollBy
是在当时的方位基础上,相对滑动必定的距离,而scrollTo
是直接滑动到指定的绝对方位。 -
scrollBy
实践上是调用了scrollTo
办法,它的参数是滑动的增量,而scrollTo
的参数是滑动的方针方位。 -
scrollBy
和scrollTo
都是移动View的内容,而不是View自身。关于一个ViewGroup,它的内容是它的一切子View;关于一个TextView,它的内容是它的文本。
这一步中,咱们完结了接触滑动。可是当中止滑动时,刻度尺会立即停下,用户体会比较差,所以还需进一步完结惯性滑动。
惯性滑动
惯性滑动是一种在用户在屏幕上滑动页面后,页面不会马上停下,而是持续保持必定时刻的翻滚作用的手势操作。它能够进步用户的交互体会,让页面的翻滚更加滑润和自然。
Android中的OverScroller
是一个用于完结View滑润翻滚的辅佐类,它能够依据用户的手势操作或许指定的参数来核算出每一时刻View的方位和速度,并提供了一些办法来操控翻滚的开端、完毕、中止等状况。
OverScroller.fling()
办法能够完结,从指定方位滑动一段方位然后停下。滑动作用只与离手速度以及滑动鸿沟有关,不能设置惯性滑动距离、时刻和插值器。
private val touchGestureListener = object : GestureDetector.SimpleOnGestureListener() {
// ...
override fun onFling(
e1: MotionEvent,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
// 发动翻滚器,设置翻滚的开端方位,速度,规模和回弹距离
scroller.fling(
scrollX, 0,
-velocityX.toInt() / 2, 0,
-viewWidth / 2, (scaleCount - 1) * scaleSpace - viewWidth / 2,
0, 0,
viewWidth / 4, 0
)
invalidate()
return true
}
}
fling办法参数的意义如下:
- startX, startY:表明滑动的开端方位的x和y坐标。
- velocityX, velocityY:表明滑动的初始速度的x和y分量,单位是像素/秒。
- minX, maxX, minY, maxY:表明滑动的鸿沟规模,假如滑动超过这个规模,就会触发OverScroll作用。
- overX, overY:表明OverScroll的最大距离,即滑动超过鸿沟后,还能持续滑动的距离
computeScroll()
是一个用于操控View的滑动作用的办法,它会在View的draw()办法中被调用,用于核算View在每一时刻的方位和状况。
override fun computeScroll() {
super.computeScroll()
// 假如翻滚器正在翻滚,更新翻滚的方位,并依据需求批改方位
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
invalidate()
}
}
OverScroller
不只能够完结惯性滑动,还能够完结以下几种翻滚作用:
-
startScroll
:从指定方位翻滚一段指定的距离然后停下,翻滚作用与设置的翻滚距离、翻滚时刻、插值器有关,跟离手速度没有关系。一般用于操控View翻滚到指定的方位。 -
springBack
:从指定方位回弹到指定方位,一般用于完结拖拽后的回弹作用,不能指定回弹时刻和插值器。
好了,惯性滑动完结了,可是当时的刻度尺还有很多瑕疵,例如:滑动时会越界,滑动中止后可能停在两个刻度之间,接下来咱们还需求进一步批改这些瑕疵。
约束滑动规模
首要,咱们需求约束滑动的规模,确保滑动时不能越界。
重写View的scrollTo
办法,在这儿咱们能够监听View每次的滑动坐标,当滑动坐标越界时,及时批改滑动坐标,就能够避免越界。
// 翻滚办法
override fun scrollTo(x: Int, y: Int) {
Log.e("TAG", "scrollTo: ")
// 约束翻滚的规模,避免越界
var x = x
// 当x坐标小于可视区域的一半时,设置x坐标为可视区域的一半
if (x < -viewWidth / 2) {
x = -viewWidth / 2
}
// 当x坐标大于最大翻滚距离时,设置x坐标为最大翻滚距离
if (x > (scaleCount) * scaleSpace - viewWidth / 2) {
x = (scaleCount) * scaleSpace - viewWidth / 2
}
// 调用父类的翻滚办法
super.scrollTo(x, y)
// 保存当时选中的值 和x坐标
currentValue = (x + viewWidth / 2) / scaleSpace + scaleMinValue
currentX = x
// 触发重绘,更新视图
invalidate()
}
在scrollTo
中咱们核算出了当时的实践刻度值,这儿保存好备用。
方位批改
在computeScroll()
中判别翻滚事情是否现已完毕,假如完毕,就开端批改滑动坐标。
override fun computeScroll() {
// 假如翻滚器正在翻滚,更新翻滚的方位,并依据需求批改方位
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
if (scroller.isFinished) {
correctPosition()
}
invalidate()
}
}
批改坐标根本思路是,首要核算出当时刻度数值对应的x坐标,因为刻度值在核算时现已取整了,所以必定是不等于当时的实践x坐标,两者的差值便是偏移量。当偏移量大于刻度距离的一半时,向前滑动,否则向后滑动。
// 批改方位,核算当时选中值距离最近的刻度的偏移量,并依据偏移量进行滑润翻滚到正确的方位
private fun correctPosition() {
// 刻度值对应的x坐标
val scaleX: Int = (currentValue - scaleMinValue) * scaleSpace - viewWidth / 2
// 偏移值 = 刻度值对应的x坐标-当时x坐标 的绝对值
val offset = (scaleX - currentX).absoluteValue
if (offset == 0) {
return
}
// 大于距离
if (offset > scaleSpace / 2) {
smoothScrollBy(scaleSpace - offset)
} else {
smoothScrollBy(-offset)
}
}
// 滑润翻滚办法
private fun smoothScrollBy(dx: Int) {
// 发动翻滚器,设置翻滚的开端方位,距离,时刻和插值器
scroller.startScroll(scrollX, 0, dx, 0, 200)
invalidate()
}
因为computeScroll()
只有在滑动时才会有回调,所以还需求在手指抬起时批改一次方位,避免遗失拖动事情。
override fun onTouchEvent(event: MotionEvent?): Boolean {
// 当手指抬起时,校准方位
if (event?.action == MotionEvent.ACTION_UP || event?.action == MotionEvent.ACTION_CANCEL) {
correctPosition()
// 将刻度值通过回调传出去.
}
return gestureDetector.onTouchEvent(event!!)
}
通过,以上这几步咱们就完结了一个收音机刻度,最后咱们进行收尾。
收尾
刻度尺需求支撑外部传入数据,并移动相应的方位,所以暴露一个setCurrentValue
办法,然后重写onLayout办法,在其中核算出刻度值的滑动坐标,运用scrollTo
滑动到对应的x坐标即可。
// 注意,因为会主动调用requestLayout(),所以不能复写kotlin的set办法。
fun setCurrentValue(value: Int) {
// 约束值的规模,避免越界
var value = value
if (value < scaleMinValue) {
value = scaleMinValue
}
if (value > scaleMaxValue) {
value = scaleMaxValue
}
currentValue = value
// 更新当时选中值,并重新布局
requestLayout()
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
// 依据当时选中值核算翻滚的距离 = (当时选中值 - 最小值) * 刻度距离 - 控件宽度的一半
val scrollX: Int = (currentValue - scaleMinValue) * scaleSpace - viewWidth / 2
scrollTo(scrollX, 0)
}
运用一段测试代码,就能够完结收音机搜台作用了。
val handler = Handler(Looper.getMainLooper())
for (i in 0..155) {
handler.postDelayed({
findViewById<ScaleView>(R.id.scaleView).setCurrentValue(i)
}, 150 * i.toLong())
}
总结
本文介绍了怎么编写一个刻度尺View,不过本文的比如不要直接运用在你的项目中,以我个人经验而言,收音机刻度尺改变较多,互联网上很少有View能不做批改直接运用在项目中的,所以更应该重视完结的原理,方便咱们在需求时进行批改和界说。
源码地址:github.com/linxu-link/…
好,以上便是本文的一切内容,感谢你的阅览,希望对你有所协助。