前言
上篇文章给大家分析了Android事件传递机制,基本是纯理论内容。光说不练假把式,今来自定义一把可以滑动的刻度尺,通过这个小案例你能从中学到Canvas、Paint、触摸反馈、Scroller、VelocityTracker的基本使用。
先上一波图
1. 绘制刻度
绘制刻度很简单,就是最基本的Canvas、Paint的使用,直接上代码:
private fun drawScale(canvas: Canvas) {
//maxScale是最大刻度(总刻度。好像名字起的有点问题...)
for (index in 0..maxScale) {
//每十个刻度有一个粗长刻度
if (index % 10 == 0) {
paint.strokeWidth = (scaleWidth * 2).toFloat()
//当前线段x轴起点
val drawX = (index * scaleInterval).toFloat()
//高度是普通刻度两倍
canvas.drawLine(
drawX, 0f,
drawX, scaleHeight.toFloat() * 2, paint
)
//绘制文字
canvas.drawText("$index", drawX-scaleTextSize/2, scaleHeight.toFloat() * 3, paint)
} else {
paint.strokeWidth = scaleWidth.toFloat()
//当前线段x轴起点
val drawX = (index * scaleInterval).toFloat()
canvas.drawLine(
drawX, 0f,
drawX, scaleHeight.toFloat(), paint
)
}
}
}
每十个刻度有一个粗长的刻度,并且下方会有刻度信息。执行完这一步我们会得到下面的效果
文字好像有点歪,刚发现,后面再调整吧~~~
2. 拖动刻度尺
想要拖动刻度尺就需要用到我们上一节所说的触摸事件反馈,所以需要我们重写onTouchEvent方法,代码如下:
//1 记录上一次move时的坐标,用于计算每次move的差量
private var lastX = 0
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastX = event.x.toInt()
//按下时通知父View不要对事件进行拦截
parent.requestDisallowInterceptTouchEvent(true)
}
//2
MotionEvent.ACTION_MOVE -> {
currentScrollX -= (event.x - lastX)
//超出最大滑动范围
if (currentScrollX > maxScrollX) {
currentScrollX = maxScrollX.toFloat()
}
//超出最小滑动范围
if (currentScrollX < minScrollX) {
currentScrollX = minScrollX.toFloat()
}
scrollTo(currentScrollX.roundToInt(), 0)
//记录当前坐标
lastX = event.x.toInt()
postInvalidate()
}
}
return true
}
注意点:在该案例中我们是通过修改
scrollX实现拖动效果,在非ViewGroup的View中修改scrollX其实是调整的是画布的位置,而到ViewGroup中修改scrollX是会调整所有子View的位置。
第一步
定义一个变量lastX记录上一次move到的水平方向坐标,用于计算每次move后的偏移量,并且调用父View的requestDisallowInterceptTouchEvent通知其不要对事件进行拦截
第二步
通过lastX配合event.x计算本次事件的偏移量,以累加的方式记录到currentScrollX中,随后通过scrollTo做绝对偏移移动,同时我们要做一个滑动范围的限制。过程中一定要调用postInvalidate或者Invalidate刷新视图。
到这一步我们实现的效果是这样的:
问题很明显,没有惯性滑动。
3. 增加惯性滑动
什么是Scroller?
一般来讲View的惯性滑动都是通过
Scroller配合来实现,Scroller本身和View无关,它只是提供了一套过度算法,比如从0..100,Scroller会通过计算会在规定的时间内给你返回一系列0-100之间的值,用于做平滑过度动画。如果还不明白也可以直接参考属性动画,它们实现的功能其实都差不多。
实行惯性滑动(fling效果)的方式有很多,我们通过Scroller配合VelocityTracker来实现,在手指抬起时通过velocityTracker采集到滑动速度,然后通过scroller的fing来实现惯性滑动,代码如下:
此段代码包含下一小节要说的刻度矫正,由于不想贴重复的代码所以在此一并贴出
private var lastX = 0
override fun onTouchEvent(event: MotionEvent): Boolean {
//开始速度检测,每个事件序列创建一个
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain()
}
velocityTracker?.addMovement(event)
when (event.actionMasked) {
...
...
MotionEvent.ACTION_UP -> {
velocityTracker?.computeCurrentVelocity(1000, maxVelocity.toFloat())
//计算水平方向速度
val velocityX = velocityTracker!!.xVelocity.toInt()
//大于可滑动速度
if (abs(velocityX) > minVelocity) {
fling(-velocityX)
}
//没有触发惯性滑动,矫正刻度
else {
correctScale()
}
//VelocityTracker回收
velocityTracker?.recycle()
velocityTracker = null
}
}
return true
}
/**
* 惯性滑动
* @param vX 单位时间内x轴位移
*/
private fun fling(vX: Int) {
scroller.fling(
currentScrollX.toInt(), 0,
vX, 0,
minScrollX, maxScrollX,
0, 0
)
invalidate()
}
/**
* draw内部会调用,专门用户处理滑动。
*/
override fun computeScroll() {
//滚动未完成完成,已完成就停止刷新界面
if (scroller.computeScrollOffset()) {
currentScrollX = scroller.currX.toFloat()
//滚动view
scrollTo(currentScrollX.toInt(), 0)
//刷新界面
postInvalidate()
//最后一次惯性滑动,进行矫正刻度
if (!scroller.computeScrollOffset()) {
correctScale()
}
//改变当前刻度
changeScale()
}
super.computeScroll()
}
提示
在View的
draw方法中会提供一个钩子方法computeScroll,每次触发draw的时候都会调用一次这个方法。本案例中会通过postInvalidate触发重绘间接回调computeScroll。
在移动过程中不断的将事件添加到velocityTracker中,它内部自己会计算当前的滑动速度。在手指抬起的一瞬间,判断此时的速度是否达到触发惯性滑动的最小速度(速度过慢是不会触发惯性滑动的),如果达到调用scroller的fling,这个过程中配合postInvalidate间接调用computeScroll方法,在内部通过scroller获取到当前应该到达的位置随后通过scrollTo做位置设置。如果scroller结束需要停止调用postInvalidate否则会无限递归。
4. 计算指示器位置和刻度矫正
首先计算指示器的位置
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
//中心坐标,最多可见刻度 / 2 * 刻度间隔,得到指示器位置,即scrollX的最小值
indicatorPointX = (width / scaleInterval) / 2 * scaleInterval
//最小滚动距离,-指示器x轴偏移量
minScrollX = -indicatorPointX
//最大滚动距离,刻度数 * 间隔 - width + width - indicatorPointX。换算后结果如下
maxScrollX = maxScale * scaleInterval - indicatorPointX
//给一个初始值
changeScale()
}
刻度尺初始位置是从0开始,我们的指示器会在中间固定显示。首先计算当前宽度最多显示刻度的个数,随后除2再乘刻度间隔 就可以得到中间刻度的位置。由于中间刻度跟宽度有关,所以计算过程我是在onSizeChanged中进行。
关于指示器的绘制
由于指示器需要在中间固定显示,而调整ScrollX会移动画布,即便做矫正处理在快滑时指示器也会左右漂浮不定,所以我在刻度View外面套了一层ViewGroup,指示器在这个ViewGroup中绘制。关于这部分代码非常简单,就不贴了,文章底部会贴出源码地址,有兴趣的可以下载下来阅读。
4.1 刻度矫正
还剩下最后一个问题,当滑动停止后如何将最近的刻度矫正至与指示器对齐?如果你懂了上面的惯性滑动相信你很快就有灵感了,没错也是通过Scroller来实现,上代码:
关于矫正时机的代码在上一小结统一贴出,请配合阅读
private fun correctScale() {
//x轴偏移量与间隔区域
val remainder = (currentScrollX % scaleInterval).toInt()
//如果跟指示器未对其
if (remainder != 0) {
//矫正目标刻度
val correctScrollX =
if (remainder > (scaleInterval / 2)) {
scaleInterval - remainder
} else {
-remainder
}
scroller.startScroll(currentScrollX.toInt(), 0, correctScrollX, 0)
}
}
获取指示器最近的刻度,计算出差值随后开启Scroller进行矫正。
到这一步就能实现我们最初给的效果图了。由于零碎的代码比较多,所以就没在文章一一贴出,感兴趣的可至 github.com/zskingking/… rulermodule中查看完整代码。
上面这个仓库是一个自定义View集合库,包括自定义View、Layout、LayoutManager等等,几乎涵盖所有自定义View知识点。如果你对自定义View一看就懂一写就不会,来我这就对了,我会秉承我一贯的作风,不写晦涩难懂的代码,尽量标清每一行注释。仓库会持续更新,欢迎关注。