文章目录
一、View的位置参数
top、bottom、left、right都是相对于ViewGroup的,易知:
width = right - left
height = bottom - top
另外还有x、y、translationX、translationY参数,x、y表示View左上角的坐标,translationX和translationY表示View的偏移量。在View平移的过程中,top、bottom、left、right不会改变,只有translationX、translationY、x、y会改变。
x = left + translationX
y = top + translationY
二、触摸View时,MotionEvent事件的位置信息
注:rawY是包含状态栏和ActionBar高度的。
三、最小滑动距离touchSlop
距离高于touchSlop的滑动才算有效滑动,此参数与设备有关,获取代码如下:
ViewConfiguration.get(context).scaledTouchSlop
四、速度追踪器VelocityTracker
使用方式如下:
customView.setOnTouchListener { v, event ->
val velocityTracker = VelocityTracker.obtain()
// 将event事件添加到VelocityTracker中追踪速度
velocityTracker.addMovement(event)
// 获取速度之前必须先计算速度,传入的参数表示速度的时间单位,这里表示:n像素/1000毫秒。注意它不是指多长时间计算一次速度,只是表示速度单位的不同
velocityTracker.computeCurrentVelocity(1000)
// 向下滑动时xVelocity为正,向上滑动时xVelocity为负。向右滑动时yVelocity为正,向左滑动时yVelocity为负
Log.d("~~~","xVelocity = ${velocityTracker.xVelocity}, yVelocity = ${velocityTracker.yVelocity}")
// 使用完后需要手动回收
velocityTracker.clear()
velocityTracker.recycle()
true
}
注:向下滑动时xVelocity为正,向上滑动时xVelocity为负。向右滑动时yVelocity为正,向左滑动时yVelocity为负。
速度 = (终点位置 - 起点位置) / 时间段
五、手势检测GestureDetector
手势检测回调接口如下:
1.OnGestureListener
-
onDown(MotionEvent e):手指按下屏幕,由ACTION_DOWN触发
-
onShowPress(MotionEvent e):手指按下屏幕,尚未松开或拖动,强调的是没有松开或者拖动的状态。快速拖动时此回调不一定触发
-
onLongPress(MotionEvent e):用户长按后触发,触发之后不会触发其他OnGestureListener回调,直至松开(UP事件)。
-
onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY):手指按下屏幕并拖动,由一个ACTION_DOWN,多个ACTION_MOVE触发,表示拖动行为
-
onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY):用户执行快速滑动操作之后的回调,MOVE事件之后手松开(UP事件)那一瞬间的x或者y方向速度,如果达到一定数值,就是快速滑动操作,由一个ACTION_DOWN、多个ACTION_MOVE和一个ACTION_UP触发。
-
onSingleTapUp(MotionEvent e):用户手指松开(UP事件)的时候如果没有执行onScroll()和onLongPress()这两个回调的话,就会回调这个,表示一个点击抬起事件。
2.OnDoubleTapListener,这个Listener监听双击和单击事件。
-
onSingleTapConfirmed(MotionEvent e):可以确认这是一个单击事件的时候会回调,注意和onSingleTapUp的区别,即这只可能是单击,而不可能是双击中的一次单击。
-
onDoubleTap(MotionEvent e):双击,它不可能和onSingleTapConfirmed共存。
-
onDoubleTapEvent(MotionEvent e):onDoubleTap()回调之后的输入事件(DOWN、MOVE、UP)都会回调这个方法(这个方法可以实现一些双击后的控制,如让View双击后变得可拖动等)。
3.OnContextClickListener,用于检测外部设备上的按钮是否按下
- onContextClick(MotionEvent e):当外部设备点击时候的回调,如外接键盘、外接蓝牙触控笔等等
4.SimpleOnGestureListener
实现了上面三个接口的类,拥有上面三个的所有回调方法。
使用SimpleOnGestureListener只需要选取我们所需要的回调方法来重写就可以了,减少了代码量。
例如:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val gestureDetector =
GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onShowPress(e: MotionEvent?) {
Log.d("~~~", getCurrentMethod())
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
Log.d("~~~", getCurrentMethod())
return false
}
override fun onDown(e: MotionEvent?): Boolean {
Log.d("~~~", getCurrentMethod())
return true
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
Log.d("~~~", getCurrentMethod())
return false
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
Log.d("~~~", getCurrentMethod())
return false
}
override fun onLongPress(e: MotionEvent?) {
Log.d("~~~", getCurrentMethod())
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
Log.d("~~~", getCurrentMethod())
return false
}
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
Log.d("~~~", getCurrentMethod())
return false
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
Log.d("~~~", getCurrentMethod())
return false
}
override fun onContextClick(e: MotionEvent?): Boolean {
Log.d("~~~", getCurrentMethod())
return super.onContextClick(e)
}
})
view.setOnTouchListener { v, event ->
// down 0, up 1, move 2
Log.d("~~~", getCurrentMethod() + ", ${event.action}")
gestureDetector.onTouchEvent(event)
}
}
fun getCurrentMethod(): String {
return Thread.currentThread().stackTrace[3].methodName
}
}
单击一次,Log如下:
~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 1
~~~: onSingleTapUp
~~~: onSingleTapConfirmed
双击一次,Log如下:
~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 1
~~~: onSingleTapUp
~~~: onTouch, 0
~~~: onDoubleTap
~~~: onDoubleTapEvent
~~~: onDown
~~~: onTouch, 1
~~~: onDoubleTapEvent
长按,Log如下:
~~~: onTouch, 0
~~~: onDown
~~~: onShowPress
~~~: onLongPress
~~~: onTouch, 1
快速滑动,Log如下:
~~~: onTouch, 0
~~~: onDown
~~~: onShowPress
~~~: onTouch, 2
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 1
~~~: onFling
或者:
~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 1
~~~: onFling
实际开发中,可以不使用GestureDetector,完全可以在View的onTouchEvent方法中实现所需的监听。《Android开发艺术探索》中建议:如果只是监听滑动相关的,建议在onTouchEvent中实现,如果监听双击行为的话,那就使用GestureDetector。
六、改变View位置的三种方式
1.使用Scroller的scrollTo和scrollBy
Scroller用来实现View的弹性滑动,Scroller的典型使用如下:
private val scroller by lazy { Scroller(context) }
private fun smoothScrollTo(destX: Int, destY: Int) {
scroller.startScroll(scrollX, scrollY, destX - scrollX, destY - scrollY)
invalidate()
}
override fun computeScroll() {
super.computeScroll()
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
postInvalidate()
}
}
Scroller的工作原理是根据起始位置和目标位置生成一系列变化的值,通过scrollTo
方法一次滑动一小段直至滑动完成。
startScroll
方法仅仅用来做初始化操作,然后我们调用了View的invalidate
方法使View重绘,重绘时会调用computeScroll
方法。我们重写了这个方法,在这个方法中调用computeScrollOffset
,computeScrollOffset
方法会根据插值器和时间间隔获取当前变化的值。如果computeScrollOffset
返回true,表示尚未到达目标值,这时我们使用scrollTo(scroller.currX, scroller.currY)
滑动一小段距离,然后调用postInvalidate
使View再次重绘,重绘时又调用computeScroll
方法,…,直至滑动完成。
例如,使用Scroller实现一个简易的ViewPager:
class ScrollLayout @JvmOverloads constructor(
context: Context,
attr: AttributeSet,
defStyleAttr: Int = 0
) : ViewGroup(context, attr, defStyleAttr) {
private val scroller by lazy { Scroller(context) }
private val touchSlop by lazy { ViewConfiguration.get(context).scaledTouchSlop }
private var xLastTouch = 0f
private var leftBorder = 0
private var rightBorder = 0
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (!changed || childCount == 0) return
children.withIndex().forEach {
val index = it.index
val view = it.value
view.layout(
index * view.measuredWidth,
0,
(index + 1) * view.measuredWidth,
view.measuredHeight
)
}
leftBorder = children.first().left
rightBorder = children.last().right
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
children.forEach {
measureChild(it, widthMeasureSpec, heightMeasureSpec)
}
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
xLastTouch = ev.rawX
}
MotionEvent.ACTION_MOVE -> {
val distance = Math.abs(ev.rawX - xLastTouch)
xLastTouch = ev.rawX
if (distance > touchSlop) return true
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_MOVE -> {
val scrolledX = (xLastTouch - event.rawX).toInt()
if (slideToBorder(scrolledX)) return true
scrollBy(scrolledX, 0)
xLastTouch = event.rawX
}
MotionEvent.ACTION_UP -> {
val targetIndex = (scrollX + width / 2) / width
val distanceX = targetIndex * width - scrollX
scroller.startScroll(scrollX, 0, distanceX, 0)
invalidate()
}
}
return super.onTouchEvent(event)
}
/**
* 是否滑到了边界
*/
private fun slideToBorder(scrolledX: Int): Boolean {
if (scrolledX + scrollX < leftBorder) {
scrollTo(leftBorder, 0)
return true
} else if (scrollX + width + scrolledX > rightBorder) {
scrollTo(rightBorder - width, 0)
return true
}
return false
}
override fun computeScroll() {
super.computeScroll()
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
invalidate()
}
}
}
具体参见郭神的文章:Android Scroller完全解析,关于Scroller你所需知道的一切
2.View属性动画
早期版本的Android系统只支持补间动画,造成的问题是View执行完动画后,只有视图移到了新位置,“真身”还在老位置,所以点击新位置不能触发点击事件。Android3.0之后加入了属性动画,实现了View“真身”随着视图一起移动。早期的应用为了让属性动画兼容Android3.0之前的版本,需要使用JakeWharton大神的nineOldAndroids库。而现在的Android应用基本都设置为支持Android4.4以上,这个库也已经被JakeWharton标记为过时。所以我们只需学习属性动画即可。
属性动画使用很简单,例如:
ObjectAnimator.ofFloat(button, "translationX", 0f, 100f).start()
内部原理是使用ValueAnimator生成了一系列变化的值,这一点和Scroller是类似的。例如上面的代码和下面的代码是等价的:
val valueAnimator = ValueAnimator.ofFloat(0f, 100f)
valueAnimator.addUpdateListener {
button.translationX = it.animatedValue as Float
}
valueAnimator.start()
由此可知,属性动画不受属性限制,任何属性都可以使用属性动画。常见的属性有translationX
、rotation
、alpha
,分别对应平移、旋转、透明度动画。
ValueAnimator可以设置动画时长、插值器、监听器等,例如:
// 单位是毫秒,从源码中可以看到默认值是300ms
valueAnimator.duration = 1000
valueAnimator.interpolator = BounceInterpolator()
valueAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationRepeat(animation: Animator?) {
super.onAnimationRepeat(animation)
}
override fun onAnimationEnd(animation: Animator?) {
super.onAnimationEnd(animation)
}
override fun onAnimationCancel(animation: Animator?) {
super.onAnimationCancel(animation)
}
override fun onAnimationPause(animation: Animator?) {
super.onAnimationPause(animation)
}
override fun onAnimationStart(animation: Animator?) {
super.onAnimationStart(animation)
}
override fun onAnimationResume(animation: Animator?) {
super.onAnimationResume(animation)
}
})
这里使用的BounceInterpolator是Android自带的插值器,效果是反复弹起。Android自带以下插值器:
- 反复弹起的插值器BounceInterpolator
- 不断加速的插值器AccelerateInterpolator
- 不断减速的插值器DecelerateInterpolator
- 先加速再减速的插值器AccelerateDecelerateInterpolator
- 先后退再前冲的插值器AnticipateInterpolator
- 正弦曲线插值器CycleInterpolator(1f)
- 先超过目标位置再后退插值器OvershootInterpolator
- 匀速插值器LinearInterpolator
默认插值器是匀速插值器LinearInterpolator。如果想要自定义插值器可以查看郭神的这篇文章:Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法。
AnimatorListenerAdapter和手势监听器的SimpleOnGestureListener类似,是一个实现了Animator.AnimatorListener,Animator.AnimatorPauseListener接口的类,使用AnimatorListenerAdapter可以仅重写自己需要的回调,减少代码量。
使用AnimatorSet实现组合动画,例如:
val moveIn = ObjectAnimator.ofFloat(button, "translationX", -500f, 0f)
val rotate = ObjectAnimator.ofFloat(button, "rotation", 0f, 360f)
val fadeInOut = ObjectAnimator.ofFloat(button, "alpha", 1f, 0f, 1f)
val animSet = AnimatorSet()
animSet.play(rotate).with(fadeInOut).after(moveIn)
animSet.duration = 5000
animSet.start()
AnimatorSet这个类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象,将会返回一个AnimatorSet.Builder的实例,AnimatorSet.Builder中包括以下四个方法:
- after(Animator anim) 将现有动画插入到传入的动画之后执行
- after(long delay) 将现有动画延迟指定毫秒后执行
- before(Animator anim) 将现有动画插入到传入的动画之前执行
- with(Animator anim) 将现有动画和传入的动画同时执行
3.改变布局参数
以ConstraintLayout为例:
val set= ConstraintSet().apply { clone(button.parent as ConstraintLayout) }
set.constrainWidth(R.id.button, 500)
set.constrainHeight(R.id.button, 500)
set.applyTo(button.parent as ConstraintLayout)
七、事件分发机制
View的事件分发机制使用的是典型的责任链模式,可以使用以下伪代码表示:
fun dispatchTouchEvent(event: MotionEvent): Boolean {
var consume: Boolean
if (onInterceptTouchEvent(event)) {
consume = onTouchEvent(event)
} else {
consume = child.dispatchTouchEvent(event)
}
return consume
}
具体可参考笔者的另一篇文章:通俗讲解 Android 事件分发机制 —— 责任链模式的典型应用
参考文章
《Android开发艺术探索》
Android手势检测——GestureDetector全面分析
Android Scroller完全解析,关于Scroller你所需知道的一切
Android属性动画完全解析