第三章view的事件体系
view基础知识
view是所有控件的基类,除了view还有viewGroup,其也继承自view。
view的位置对应的四个顶点的位置,分别是top,left,right,bottom。他们可以通过get方法获取。 view内容的位置为x
和y
,偏移值为translationX(默认为0)
和translationY(默认为0)
x= left+ translationX
y= top+ translationY
以上的各值都是相对于父容器来说的。
手指在接触屏幕后会产生一系列的点击事件,成为事件序列。 典型的基本事件类型有ACTION_DOWN,ACTION_MOVE,ACTION_UP。 事件序列有两种DOWN-> UP
,DOWN-> MOVE-> ...-> MOVE-> UP
通过MotionEvent对象我们可以得到点击事件发生的x和y坐标。即getX/getY
, 此处返回的是相对于当前view左上角的x和y坐标。同时还有getRawX
和getRawY
,区别在于,其是相对于手机屏幕左上角的坐标。
系统设定的,能被视为滑动的最小距离。 可以通过ViewConfiguration.get(getContext()).getScaledTouchSlop()
来获取这个值。
获取VelocityTracker对象VelocityTracker velocityTraker = VelocityTracker.obtain();
在View的onTouchEvent中追踪当前单击事件的速度velocityTracker.addMovement(event)
获取当前速度,这里的速度是指一段时间内手指滑过的像素数。可以为负值,为从右往左。
velocityTracker. computerCurrentVelocity ( 1000 ) ;
int xVelocity = ( int ) velocityTraker. getXVelocity ( ) ;
int yVelocity = ( int ) velocityTraker. getYVelocity ( ) ;
不需要的时候要调用clear方法来重置并回收内存。
velocityTraker. clear ( ) ;
velocityTraker. recycle ( ) ;
手势检测,用于辅助用户的单击,滑动,长按,双击等行为。 如果只是监听滑动
相关的,建议自己在onTouchEvent中实现,如果要监听双击等这种行为,可以使用GestureDetector。 创建一个GestureDetector对象并实现相应想监听的接口,并重写里面的方法(方法有很多,可以查看相关文档)。
GestureDetector mGestureDetector = new GestureDetector ( this ) ;
mGestureDetector. setIsLongpressEnbled ( false ) ;
接管目标view的onTouchEvent方法
boolean consume = mGestureDetector. onTouchEvent ( event) ;
return consume;
弹性滑动对象,用来实现有过渡效果的滑动。典型代码固定,可以看下面的弹性滑动。
view的滑动
通过view本身提供的scrollTo/scrollBy
方法。 通过动画给view施加平移效果来实现滑动 改变view的LayoutParams使得view重新布局从而实现滑动。
scrollBy实际上使用的还是scrollTo方法 这两者只能改变view内容的位置,而并不能改变view的位置。 mScrollX和mScrollY,是在相应方向上view与view内容之间的距离。负值view内容在右/下,正值在左/上。
既可以采用传统view动画,又可以采用属性动画。 采用传统view动画可以利用android:duration
, android:fromXDelta
, android:toXDelta
等属性在xml文件中设置。由于传统view动画只能对view的影像
做操作,并不能真正改变view的位置和宽高(即改变后的影像你不能点击),所以不是很推荐。并且如果希望保留变化后的状态,还必须将fillAfter
属性设为true。属性动画可以很好地解决这个问题。
改变布局参数。在第四章做分析,这里了解就行。 三种方法的对比
scrollTo/scrollBy: 适合对view内容的滑动 动画:适用于没有交互的view和实现复杂的动画 改变布局参数:适用于有交互的view
弹性滑动
典型实现如下所示 Scroller.startScroll
方法只是保存了传递的参数invalidate()
方法会让view重绘,调用draw
方法。draw
方法又会去调用computeScroll
方法,完成第一次重绘,然后postInvalidate
会再次进行第二次重绘。过程与第一次相同。一直反复这个过程。scroller.computeScrollOffset
方法会根据流逝的时间去计算当前scrollX和scrollY的值(根据过去的时间占设定时间的百分比)。返回true
代表滑动还未结束。知道了滑动位置就可以通过scrollTo方法进行移动。 多次小幅度的移动就实现了弹性滑动。scroller.computeScrollOffset
返回false
,滑动结束。
动画本身就是渐进的,并且可以设置是均匀滑动还是变式滑动。
Handler或view的postDelayed方法。通过延时发送消息,然后在消息中进行view的滑动。 使用线程中的sleep方法。通过在while循环中不断的滑动view和sleep就可以达到目的。 核心的思想就是通过发送一系列延时消息从而达到渐进的效果
view的事件分发机制
分析的对象是MotionEvent
,点击事件的分发过程就是对MotionEvent事件的分发过程。 当一个MotionEvent产生后,系统需要把这个事件传递给一个具体的view。
一个事件序列 (DOWM, MOVE,UP) 视为一个整体 onIntercepterTouchEvent
如果返回true
, 则一个事件序列中的后续动作到来时不会再调用 onIntercepterTouchEvent, 自己直接调用onTouchEvent
处理onIntercepterTouchEvent
中判断是否拦截该事件序列并返回boolean, onTouchEvent
中处理和是否消耗该事件序列并返回boolean. 例如: onIntercepterTouchEvent确定拦截时, 该ViewGroup会调用onTouchEvent,若在DOWN阶段什么都没返回, 而在MOVE的时候返回true, 那么UP事件中的代码就不会再处理. 该次事件序列直接被消耗如果存在一个ViewGroup没有子view, 或者所有的子view在onTouchEvent中返回false, 则自己直接调用onTouchEvent处理 ViewGroup的onIntercepterTouchEvent默认返回false, 所以 根view会层层向下, 直接将事件序列整体传给最底层的子view, 而可点击状态下的View会消耗点击事件(返回true), 如果最底层view不处理(返回false), 则会层层向上, 交给父ViewGroup的onTouchEvent处理。 如果都不处理, 最终返回false, 交给Activity自己处理 如果向下
过程中有view拦截(onInterceptTouchEvent中判断并返回true), 那么事件序列归该view处理(在onTouchEvent中处理并消耗) 如果向上
过程中(子view都返回了false), 父ViewGroup.onTouchEvent返回了true, 那么事件序列归该ViewGroup消耗.
当一个点击操作发生时,事件最先传递给当前Activity
,Activity的dispatchTouchEvent
会进行事件派发。派发时,Activity内部的window
会将事件传递给decor view
(是底层容器,即顶级view、根view。实质上是一个framelayout。分为菜单栏和内容栏content(setContentView设置的view就是这个)),至此到达了根view。 对点击事件的分发过程分为两种:viewGroup对点击事件的分发过程 和 view对点击事件的处理过程。 viewGroup
的分发:点击事件到达顶级view后会调用dispatchTouchEvent方法。此时会判断是自己拦截事件还是分发给子view。在源码中可以分析出,viewGroup默认不拦截点击事件,这样就可以分发给子view(在处理滑动冲突时可以根据需要重写此方法进行拦截)。分发给子view时会遍历所有的子view,然后进行判断 canViewReceivePointerEvents
(这个方法里进行了是否播放动画的判定)和 isTransformedTouchPointInView
(点击事件的坐标是否落在view里面) 只有当view在播放动画
且点击坐标不在它里面
时,此时不接收点击事件
。
private static boolean canViewReceivePointerEvents ( @NonNull View child) {
return ( child. mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child. getAnimation ( ) != null;
}
当子view接收点击事件时,如果子view的dispatchTouchEvent
返回true,mFirstTouchEvent
(其为单链表结构,如果最终为null,那么viewGroup就默认拦截接下来同一序列中的所有点击事件)就会被赋值同时跳出遍历view的for循环。此时点击事件就传递给子view去处理。 子view可能还是个viewGroup,那么还是按上述方法进行。但也可能是view,那么就按view的分发来。 view
的处理过程:需要再次申明,这里的view不包含viewGroup。 1.同样有dispatchTouchEvent
方法。 2.没有onInterceptTouchEvent
方法 2.由于没有 子view,只能自己处理事件。 3.源码分析得出:OnTouchListener的优先级高于onTouchEvent(源码实现为首先判断有没有设置OnTouchListener,有,然后如果OnTouchListener.onTouch方法返回ture,那么onTouchEvent就不会被调用。) 4.不管view是不是不可用状态,只要CLICKABLE
和LONG_CLICKABLE
有一个为true,那么他就会消耗这个点击事件。 5.一般可点击的view其CLICKABLE为true(比如 button为true,而 textview为false),而所有view的LONG_CLICKABLE默认为false。这两个可以通过相应的set方法进行更改 6.setOnClickListener
和setOnLongClickListener
会分别将view相应的CLICKABLE和LONG_CLICKABLE设置为true。
if ( actionMasked == MotionEvent. ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = ( mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 ;
if ( ! disallowIntercept) {
intercepted = onInterceptTouchEvent ( ev) ;
ev. setAction ( action) ;
} else {
intercepted = false ;
}
} else {
intercepted = true ;
}
以上是对源码的分析,而我们在实际运用中只要针对不同的逻辑重写相应的方法,然后返回相应的值,就能达到拦截以解决滑动冲突的效果。
view的滑动冲突
外部滑动方向与内部滑动方向不一致 外部滑动方向与内部滑动方向一致 前面两种的嵌套
解决方法 将相应的滑动规则改变去应对不同场景,而滑动规则需要根据实际需求来分析。 处理方法可以分为外部拦截法和内部拦截法
外部拦截法 1.在父容器中拦截需要的事件 2.重写onInterceptTouchEvent进行条件判断并拦截(返回true)。 3.在ACTION_DOWN事件中,必须返回false。 4.重写onTouchEvent之类的方法进行功能上处理。 内部拦截法 1.核心是在子view中通过requestDisallowInterceptTouchEvent
来对父元素是否拦截做相应的处理。 2.首先设置父元素默认拦截除了ACTION_DOWN以外的其他事件。 3.由于父元素自己不拦截ACTION_DOWN元素,对MOVE和UP做拦截。当事件到来时,最先事件就分发给了子元素,子元素对所有动作进行判断,当符合父元素拦截的条件时,将父view.requestDisallowInterceptTouchEvent设置为false,即无影响,事件序列的后续动作到来父元素就拦截。不符合时设置为true,即请求父元素不拦截,此时交由子元素处理。 4.在3中的处理是重写子元素(这里指viewGroup,view没有onInterceptTouchEvent方法)的dispatchTouchEvent
,原因是要对事件序列中的所有动作进行判断,为什么不在onInterceptTouchEvent
里判断呢?因为他只要判定为true,事件序列后续的动作都交由他处理而不会再调用onInterceptTouchEvent
。想提前处理所有
的动作就不能在onInterceptTouchEvent进行判断。所以在子元素,不管是view,还是viewGroup都是重写dispatchTouchEvent。