View 基础
- View的位置由它的四个顶点决定,top, left, right, bottom。左上角坐标为(left, top),右下角坐标为(right, bottom)。这些坐标都是相对于View的父容器来首的,是相对坐标。在Android中x轴和y轴的正方向分别为右和下。
MotionEvent 和 TouchSlop
MotionEvent
- ACTION_DOWN ---> 手指刚接触屏幕。
- ACTION_MOVE ---> 手指从屏幕上移动。
- ACTION_UP ---> 手指从屏幕上松开的一瞬间。
- 用户点击屏幕后离开松开,顺序为:DOWN-->UP
- 用户点击屏幕后滑动一会再松开,顺序为:DOWN->MOVE->MOVE...->MOVE->UP
- MotionEvent: getX/getY 方法返回的是相对于当前View左上角的x和y坐标。
- MotionEvent: getRawX/getRawY 方法返回的是相对于手机屏幕左上角的x和y坐标。
TouchSlop
- TouchSlop这是一个常量,表示系统所能识别出的被认为是滑动的最小距离。不同设备这个值可能是不同的。ViewConfiguration.get(context).getScaledDoubleTapSlop()。可以通过这个方法拿到。
VelocityTracker GestureDetectoe Scroller
VelocityTracker
view.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View, event: MotionEvent): Boolean {
val velocityTracker = VelocityTracker.obtain()
velocityTracker.addMovement(event)
velocityTracker.computeCurrentVelocity(1000)
val xVelocity = velocityTracker.getXVelocity()
val yVelocity = velocityTracker.getYVelocity()
Log.d("joker", "x方向在1000ms走过的像素: $xVelocity")
Log.d("joker", "yVelocity: $yVelocity")
velocityTracker.clear()
velocityTracker.recycle()
return true
}
})
- 公式: 速度=(终点位置 - 起点位置) / 时间段
- 获取速度前必须先计算速度,computeCurrentVelocity的参数表示一个时间间隔,单位是毫秒。计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素数。
- 这个对象不用了记得释放资源(最后两行代码)
GestureDetectoe
- 手势检测,用户复制检测用户的单击,滑动,长按,双击等行为。
override fun onTouchEvent(event: MotionEvent?): Boolean {
return gestureDetector.onTouchEvent(event)
}
OnGestureListener
GestureDetector(context, object : GestureDetector.OnGestureListener {
override fun onDown(e: MotionEvent?): Boolean {
Log.d("joker", "onDown")
return true
}
override fun onShowPress(e: MotionEvent?) {
Log.d("joker", "onShowPress")
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
Log.d("joker", "onSingleTapUp")
return false
}
override fun onScroll(e1: MotionEvent?,e2: MotionEvent?,distanceX: Float,distanceY: Float): Boolean {
Log.d("joker", "onScroll")
return true
}
override fun onLongPress(e: MotionEvent?) {
Log.d("joker", "onLongPress")
}
override fun onFling(e1: MotionEvent?,e2: MotionEvent?,velocityX: Float,velocityY: Float): Boolean {
Log.d("joker", "onFling")
return true
}
})
OnDoubleTapListener
gestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
log("onSingleTapConfirmed")
return true
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
log("onDoubleTap")
return true
}
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
log("onDoubleTapEvent")
return true
}
})
- 也可以不使用上面两个监听器,而使用View的onTouchEvent来实现所需的监听。
- 建议:如果是监听滑动相关,在onTrouchEvent中就行,如果是双击这种行为,就是用gesturedector。
view 的滑动
scrollTo / scrollBy
- mScroolX的值 总 等于View左边缘和View内容左边缘在水平方向的距离。
- mScroolY的值 总 等于View上边缘和View内容上边缘在竖直方向的距离。
- scrollTo / scrollBy 只能改变View内容的位置,而不能改变View在布局中的位置。
- 因为不能改变View在布局中的位置,所以不管怎么滑,也不能将当前View滑到附近View所在的区域。
使用动画
- 使用补间动画,移动的只是view的影相。如果不设置fillAfter,动画完成的一刹那,就会还原。比如动画后的位置不会响应事件,因为只是影相到那里去了。
改变布局参数
Scroller 典型代码 以及 说明
val scroller: Scroller = Scroller(context)
fun startScroll() {
scroller.startScroll(0, 0, 100, 100)
invalidate()
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
postInvalidate()
}
super.computeScroll()
}
- invalidate()方法会导致view重绘,在View的draw方法中会调用computeScroll方法,在这个方法中会去向Scroller获取当前scrollX和scrollerY然后通过scrollergTo实现滑动,然后在调用postInvalidate方法再次通知重绘。
view 的事件分发
public boolean dispatchTouchEvent(MotionEvent ev)
- 此方法用来做进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
- 在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
public boolean onTouchEvent(MotionEvent event)
- 在dispatchTouchEvent方法内部调用,用来处理当前点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
表示上面三个方法关系的 伪代码
fun dispatchTouchEvent(ev: MotionEvent): Boolean {
var consume = false
if (onInterceptTouchEvent()) {
consume = onTouchEvent(ev)
} else {
consume = child.dispatchTouchEvent(ev)
}
return consume
}
说明
- 传递顺序: Activity -> Window -> View 。到了View这一层后,如果所有的子View都不处理这个事件,那么这个事件会再次传回到Activity中,即Activity的onTouchEvent会被调用。
- 同一个事件序列,以down事件开始,中间有数量不定的move,以up事件结束。
- 一个事件序列只能被一个view拦截并且消耗,一旦一个元素拦截了此事件,那么同一个事件序列的其他事件都会直接交给他来处理。
- 某个view一旦决定拦截,那么这一个事件序列都只能由他来处理,并且他的onInterceptTouchEvent不会再被调用。
- 某个View一旦开始处理事件,如果他不消耗ACTION_DOWN事件(那次的onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给他来处理,并且事件将重新交他的父元素来处理,即父元素的onTouchEvent会被调用。事件一旦交给一个View来处理,那么他就必须消耗掉,否则同一个事件序列剩下的事件就不会再交给他来处理。
- 如果View不消耗除ACTION_DOWN的其他事件,这个View还是能接到后续的事件,并且这些未消耗的其他事件都会传递给Activity来处理。
- ViewGroup默认不拦截任何事件,即ViewGroup的onInterceptTouchEvent方法默认返回false。
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给他,那么他的onTouchEvent方法就会被调用。
- View的onTouchEvent默认都会返回true,即消耗事件,除非他是显示声明为不可点击的。(clickable 和 longClickable都会flase)
- view的enable属性不影响onTouchEvent默认返回true。
- 事件的传递是由外向内的,即事件总是先传递给父元素,再由父元素分发给子View。通过requestDisallowInteceptTouchEvent方法可以子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。
滑动冲突的解决方法
- 原理:比如上面,处理方法为,当用户左右滑动的时候,需要让外部的View拦截点击事件,当用户上下滑动的时候,需要让内部的View拦截点击事件。即,根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截点击事件。