View基础知识
View介绍
在介绍 View 的基础知识之前,我们需要知道它到底是什么? View 在 Android 中是所有控件的基类(结构参考上图),不管是简单的 TextView , 还是复杂的 ViewGroup 、 CustomView 亦或者 RecyclerView 它们的共同顶级父类都是 View, 所以说, View 是一种界面层控制的一种抽象,它代表的是一个控件。从上图可知 ViewGroup 是 View 的子类,ViewGroup 在视图层它可以有任意子 View 。
明白 View 的层级关系有助于理解 View 的工作机制。从上图我们也可以知道实现自定义 View 控件可以继承自 View 也可以继承自 ViewGroup 。
View 位置参数
View 的位置主要由它的四个顶点来决定,分别对应于 View 的四个属性: top、left、right、bottom , 其中 top 是左上角纵坐标,left 是左上角横坐标,right 是右下角横坐标,bottom 是右下角纵坐标。需要注意的是,这些坐标都是相对于 View 的父容器,因为它是一种相对坐标,View 的坐标和父容器的关系可以参考下图,在 Android 中 ,x 轴 y 轴 的正方向分别为右和下,这点不难理解,不仅仅是 Android ,其实大部分显示系统都是按照这个标准来定义坐标系的。
根据上图,我们很容易得出 View 的宽高和坐标的关系:
val width = right - left
val height = bottom - top
从 Android 3.0 开始,View 增加了 额外的几个参数,x 、y 、translationX 、translationY , 其中 x 和 y 是 View 左上角的坐标,而 translationX 和 translationY 是 View 左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且 translationX 和 translationY 的默认值是 0 ,和 View 的四个基本的位置参数一样,View 也为他们提供了 set/get 方法,这几个参数的换算关系如下所示:
val x = left + translationX
val y = top + translationY
需要注意的是,View 在平移过程中,top 和 left 表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是 x 、y、translationX 、translationY 这四个参数。
MotionEvent 和 TouchSlop
MotionEvent
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
println("手指按下")
}
MotionEvent.ACTION_UP -> {
println("手指抬起")
}
MotionEvent.ACTION_MOVE -> {
println("手指移动")
}
}
return true
}
在手指接触屏幕后所产生的一系列事件中,常用的并且非常典型的事件类型有如下几种:
- MotionEvent.ACTION_DOWN: 手指刚接触屏幕
- MotionEvent.ACTION_MOVE: 手指在屏幕上滑动
- MotionEvent.ACTION_UP: 手指在屏幕上抬起的一瞬间触发该事件
正常情况下,一次手指触摸屏幕的行为会触发一些列点击事件,考虑有如下几种情况:
- DOWN —> UP: 点击屏幕后立刻抬起手指松开屏幕触发的事件
- DOWN —> MOVE —> MOVE —> MOVE —> UP: 点击屏幕然后随着在屏幕上滑动之后在松开产生的事件
上述三种情况是典型的事件序列,同时通过 MotionEvent 对象我们可以得到点击事件发生的 x 和 y 坐标。因此,系统提供了两组方法 getX / getY 和 getRawX / getRawY 它们的区别其实很简单, 如下:
- getX / getY : 返回相对于当前 View 左上角的 x 和 y 的坐标
- getRawX / getRawY : 返回的是相对于手机屏幕左上角的 x 和 y 坐标。
TouchSlop
TouchSlop 官方解释就是系统所能识别的被认为是滑动的最小距离,通俗点说就是当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就认为你没有在滑动,可以通过下面的 API 获取该常量值,
/**
* 系统所能识别出来的被认为滑动的最小距离
*/
val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop;
这个常量可以帮助我们在处理滑动时,利用该数值来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,我们就可以未达到滑动距离的临界点,因此就可以认为他们不是滑动,这样做可以有更好的用户体验。
VelocityTracker 、GestureDetector 和 Scroller
VelocityTracker
VelocityTracker 的作用是用于追踪滑动过程中的速度,包括水平和竖直方向的速度。它的使用过程很简单,首先,在 View 的 onTouchEvent 方法中追踪当前单击事件的速度;
/**
* 速度追踪
*/
val velocityTracker = VelocityTracker.obtain()
velocityTracker.addMovement(event)
接着,当我们先知道当前的滑动速度时,这个时候可以采用如下方式来获得当前的速度:
velocityTracker.computeCurrentVelocity(1000)
val xVelocity = velocityTracker.getXVelocity()
val yVelocity = velocityTracker.getYVelocity()
这一步有 2 点需要注意,其一 获取速度之前必须先计算速度,既 getXVelocity 和 getYVelocity 这两个方法的前面必须要调用 computeCurrentVelocity 方法,第二点,这里的速度是指一段时间内手指所滑过的像素值,比如将时间间隔设为 1000 ms 时,那么就是在 1s 内手指在水平方向从左向右滑动 500 px 那么水平速度就是 500,注意速度可以为负数,当手指从右往左滑动时,水平方向速度即为负值,这个需要理解一下。速度的计算可以用如下公式:
速度 = ( 终点位置 - 起点位置) / 时间段
根据上面的公式再加上 Android 系统的坐标体系,可以知道,手指逆着坐标系的正方向滑动,所产生的速度就为负值,另外,computeCurrentVelocity 这个方法的参数表示的是一个时间单元或者说时间间隔,它的单位是毫秒 (ms), 计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素值。
针对上面的例子,如果我们通过 obtain.computeCurrentVelocity(1000) 来获取速度,那么得到的速度就是手指在 1000 ms 毫秒内所滑过的 px 值,因此可以直接套上面公式:
水平速度 = 500 px / 每 1000 ms 既水平速度为 500 , 这里需要好好理解一下。
最后,当不需要它的时候,需要调用 clear 方法来重置并回收内存:
velocityTracker.clear()
velocityTracker.recycle()
VelocityTracker 的 API 简单明了,我们可以记住一个套路:
- 在触摸事件为 ACTION_DOWN或是进入 onTouchEvent方法时,通过 obtain获取一个 VelocityTracker
- 在触摸事件为 ACTION_UP时,调用 recycle进行释放 VelocityTracker
在进入 onTouchEvent方法或将 ACTION_DOWN、ACTION_MOVE、ACTION_UP的事件通过 addMovement方法添加进 VelocityTracker - 在需要获取速度的地方,先调用 computeCurrentVelocity方法,然后通过 getXVelocity、getYVelocity获取对应方向的速度
GestureDetector
GestureDetector 的作用用于辅助检测用户的单机、滑动、长按、双击等行为。要使用 GestureDetector 也不复杂,参考如下过程:
- 首先创建一个 GestureDetector 对象并实现 OnGestureListener 接口,根据需要我们还可以实现 OnDoubleTapListener 从而能够监听双击行为;
val mGetDetector = GestureDetector(context,this)
//解决长按屏幕后无法拖动的现象
mGetDetector.setIsLongpressEnabled(false)
- 接管目前 View 的 onTouchEvent 方法,在 View 的 onTouchEvent 方法中添加如下代码:
override fun onTouchEvent(event: MotionEvent) = mGetDetector.onTouchEvent(event)
做完了上面这 2 步,我们就可以有选择的实现 OnGestureListener 和 OnDoubleTapListener 中的方法了,这 2 个接口中的方法介绍如下所示:
OnGestureListener / 方法名 | 描述 |
---|---|
onDown | 手指轻轻触摸屏幕的一瞬间,由 1 个 ACTION_DOWN 触发 |
onShowPress | 手指轻轻触摸屏幕, 尚未松开或拖动,由一个 ACTION_DOWN 触发,它强调的是没有松开或者拖动的状态 |
onSingleTapUp | 手指(轻轻触摸屏幕后)松开,伴随着 1 个 MotinEvent.ACTION_UP 而触发,这是单击行为 |
onScroll | 手指按下屏幕并拖动,由 1 个 ACTION_DOWN ,多个 ACTION_MOVE 触发,这是拖动行为 |
onLongPress | 用户长久的按着屏幕不放,既长按 |
onFling | 用户按下触摸屏、快速滑动后松开,由 1 个 ACTION_DOWN 、多个 ACTION_MOVE 和 1 个 ACTION_UP 触发,这是快速滑动行为 |
OnDoubleTapListener / 方法名 | 描述 |
---|---|
onDoubleTap | 双击,由 2 次连续的单击组成,它不可能和 onSingleTapConfirmed 共存 |
onSingleTapConfirmed | 严格的单机行为(注意它和 onSingleTapUp 的区别,如果触发了 onSingleTapConfirmed, 那么后面不可能再紧跟着另一个单击行为,既这只可能是单击,而不可能是双击中的一次单击) |
onDoubleTapEvent | 表示发生了双击行为,在双击的期间, ACTION_DOWN 、ACTION_MOVE 和 ACTION_UP 都会触发此回调 |
上面图表里面的方法很多,但是并不是所有的方法都会被时常用到,在日常开发中,比较常用的有 onSingleTapUp 单击、onFling 快速滑动 、onScroll 拖动 、onLongPress 长按、onDoubleTap 双击 。另外在说一下,在实际开发中,可以不使用 GestureDector, 完全可以自己在 View 的 OnTouchEvent 方法中实现所需要的监听,这个看实际场景跟个人喜好了。
Scroller
Scroller 用于实现 View 的弹性滑动。我们知道,当使用 View 的 scrollTo / scrollBy 方法进行滑动时,其过程是瞬间完成的,没有一个过渡的效果体验是不友好的,那么这个时候就可以借助 Scroller 来实现过渡效果的滑动。Scroller 本身无法让 View 弹性滑动,它需要和 View 的 computeScroll 方法配合使用才能共同完成这个功能。那么如何使用 Scroller 呢?它的典型代码可以说是固定的,如下所示:
class ScrollerSample_1 : LinearLayout {
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
/**
* 定义滑动 Scroller
*/
private val mScroller = Scroller(context)
public fun smoothScrollTo(destX: Int = -100, destY: Int = -100) {
//滑动了的位置
val scrollX = scrollY;
val delta = destY - scrollY;
//2000 ms 内滑动到 destX 位置,效果就是缓慢滑动
mScroller.startScroll(scrollX, 0, 0, delta, 2000)
invalidate()
}
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.currX, mScroller.currY)
postInvalidate()
}
}
}
主要实现有 3 步:
- 实例化 Scroller
- 调用 Scroller 的 startScroll 方法,让其内部保存新的变量值
- 重写 View 的 computeScroll 方法,调度自身的 scrollTo 方法,让其缓慢弹性滑动