第三章:View的事件体系
View的位置参数:
- View的位置参数left, top, right, bottom是相对于VIew的父容器来说的,因此是一种相对坐标
- 宽高和坐标的关系:
- width = right - left
- height = bottom - top
- 这四个参数对应于源码的mLeft, mRight, mTop, mBottom这四个成员变量,获取方式:
- left = getLeft()
- right = getRight()
- Top = getTop()
- Bottom = 个体Bottom()
MotionEvent和TouchSlop
- 通过MotionEvent我们可以得到点击事件发生的坐标。
- getX和getY得到的是相对于当前View的坐标
- getRawX和getRawY是相对于屏幕的坐标
- TouchSlop是系统所能识别出的被认为是滑动的最小距离,下面是获取的方式:
- ViewConfiguration.get(getContext()).getScaledTouchSlop();
VeloccityTracker和GuestureDetector和Scroller
- VelocityTracker:速度追踪,用于追踪手指在滑动中的速度
- 使用:首先在View的onTouchEvent方法中追踪当前事件的速度:
|
|
- 注意:获取速度之前必须计算速度;第二点:这里的速度指的是一段时间内手指所划过的像素数
- 最后不用的时候,应当调用clear和recycle来回收
GestureDetector
- 使用:
|
|
- 首先创建一个对象,并实现其监听接口,接着在接管View的onTouchEvent方法中,接管该事件
Scroller
- 实现View的滑动有三种方式:
- View自身提供的scrollerTo/scrollerBy
- 通过动画平移
- 改变View的layoutParams
- scrollerTo/scrollerBy
|
|
- scrollTo是绝对滑动
- scrollBy是相对于自身位置滑动
- 在滑动过程中,mScrollX的值等于View左边缘和View内容左边缘的水平距离,而mscrollY的值总是等于View上边缘和内容边缘在竖直方向上的距离
- 使用这两种方法不能使得View本身滑动,只能是内容滑动
- 使用动画:
- 改变布局参数:通过改变Margin来滑动
|
|
总结:
- scrollTo/scrollBy:操作简单,适合对View内容的滑动
- 动画:操作简单。主要适合用于没有交互的View和实现复杂的动画效果
- 改变布局参数:操作稍微复杂,用于有交互的View
实现弹性滑动
- 使用Scroller
- 用法:
|
|
- 首先构造一个Scroller对象,接着调用它的startScroll,但是该方法内部知识保存了几个传递的参数
- 让View是实现弹性滑动的是invalidate(),该方法或导致View的重绘,在View的draw中又会去调用computeScroll,而computeScroll是我们自己实现的一个方法,在computeScroll中又会去获得当前的scrollX, scrollY,然后通过scrollTo实现滑动,接着又调用postInvalidate再进行重绘,如此反复,直到滑动完成
- computeScrollOffset:该方法通过计算时间的流逝来计算出当前scrollX和scrollY的值,类似插值器的工作原理
总结:
- Scroller本身并不能够实现View的滑动,它需要配合View的computerScroll来实现弹性滑动,它不断地让View重绘,而每次重绘距滑动起始时间会有一个间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo来实现滑动。就这样,每次View的重绘都会导致View进行小幅度的滑动,而多次的小滑动就组成了View的弹性滑动
- 使用动画:
- 使用延时策略:通过使用Handler来实现
View的事件分发机制
- 相关的三个重要的方法:
- public boolean dispatchTouchEvent(MotionEvent event):用于进行事件的分发。如果时间能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent()方法的影响,表示是否消费当前事件
- public boolean onInterceptHoverEvent(MotionEvent event):在上述方法内部调用,用于判断是不是要拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被调用,返回结果表示是否拦截当前事件。
- public boolean onTouchEvent(MotionEvent event):在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否拦截当前事件,如果不消耗,那么在同一个事件序列当中,当前View将无法接受到事件
- 概述:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent会被调用,如果这个ViewGroup的onInterceptHoverEvent返回true表示它要拦截当前事件,接着该事件就会交给这个ViewGroup处理,即它的onTouchEvent会被调用;如果返回false表示不拦截此事件,这时该事件会传给它的子View,接着子View的dispatchTouchEvent会被调用,如此直到该事件被最终处理
源码分析:
1.Activity对点击事件的分发过程
- 当一个点击事件发生时,事件最先传递给当前的Activity,由该Activity的dispatchTouchEvent来进行事件派发,具体的是又Activity内部的Window来完成的。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(framelayout),通过Activity.getWindow().getDecorView()可以获得。 下面是Activity的dispatchTouchEvent的源码:
|
|
- 首先事件开始交给Activity所依附的Window进行分发,如果返回true的话,整个事件循环就结束了,返回false的话意味着事件没有人处理,所有的View都返回onTouchEvent都返回false,那么Activity的onTouchEvent就会被调用。
- 接下来看Window是怎么样传给ViewGroup对象的,首先Window是一个抽象类,superDispatchTouchEvent方法也是抽象方法,其具体实现类是PhoneWindow,那么我们来看看其superDispatchTouchEvent:
|
|
- PhoneWindow将事件直接传递给DecorView,我们来看看DecorView是什么?
- 我们知道,通过getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)这种方式就可以获取Activity所设置的View,这个mDecorView显然就是getWindow().getDecorView()返回的View,而我们通过setContentView()就是它的一个子View
- 在这里开始,事件已经传递到顶级View了,即在Activity中通过setContentView所设置的View,另外顶级View也叫根View,顶级View一般说的是ViewGroup
顶级View对点击事件的分发过程
- 首先看看其dispatchTouchEvent:
|
|
- 从上面的代码可以看出,在两种情况下ViewGroup会拦截此事件:事件类型为:ACTION_DOWN或者mFirstTouchTarget != null。当是事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向该子元素。
- 也就是说,当ViewGroup不拦截事件并将事件交给子元素处理时,mFirstTouchTarget != null。反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget != null就不成立。
- 那么当MOVE和UP事件传递过来的时候,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理
- 还有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent来设置的,一旦被设置后,ViewGroup将无法拦截除了DOWN事件以外的点击事件。这时因此ViewGroup在分发事件时,如果是DOWN事件的话就会重置这个标记位,将导致子View中设置的标记无效,面对DOWN事件,ViewGroup总会询问自己是不是要拦截:
- 小结:当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理,并且不再调用onInterceptTouchEvent。FLAG_DISALLOW_INTERCEPT这个标记的作用是让ViewGroup不再拦截事件,
- 接着看ViewGroup不拦截事件的时候,事件会向下分发交给它的子View进行处理
|
|
- 首先遍历ViewGroup所有子View,然后判断子View是不是能够接收点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是不是落在子元素的区域。如果子View满足两个条件的话,那么事件将交给它处理,
- 实际调用子元素的dispatchTouchEvent是在dispatchTransformedTouchEvent中,
|
|
- 如果传递的字View不为null的话,就会调用子View的dispatchTouchEvent,这样事件就交给子View处理了
- 如果子View的dispatchTouchEvent返回true的话,那么mFirstTouchTarget就会被赋值并跳出循环:
|
|
- 上面的代码完成了对mFirstTouchTarget赋值并终止了对子View的遍历。
- 如果子View返回false的话,ViewGroup会将事件分给下一个子View(还有下一个子 View的话)
- 如果遍历所有子View后事件都没有被合适处理,这包含两种情况:
- ViewGroup没有子View
- 子元素处理了点击事件,但是在dispatchTouchEvent中返回false。
- 上面这两种情况下ViewGroup会自己处理点击事件
|
|
- 上面的代码中第三个参数为null,显然调用super.dispatchTouchEvent
View对点击事件的处理过程
- 先看看其dispatchTouchEvent:
|
|
- 可以看到,首先会判断有没有设置OnTouchEventListener,如果OnTouchEventListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便外界处理点击事件
- 接下来分析onTouchEvent:
|
|
- 上面是View在不可用的状态下点击事件的处理过程,显然不可用的View照样会消费事件,只是没有为点击做出回应
- 下面看看onTouchEvent对点击事件的具体处理:
|
|
- 只要View的CLICKABLE和LONG——CLICKABLE有一个为true的话,那么它就会消费这个事件,即onTouchEvent返回true,不管状态是不是DISABLE状态。然后就是当ACTION_UP事件发生时,就会出发performClick,如果View设置了OnClickListener的话,performClick内部就会调用onClick
|
|
- View的LONG_CLICKABLE默认设置为false,而CLICKABLE则要看具体的View,可点击的View的CLICKABLE为true,不可点击的为false。
setOnClickListener会自动将View的CLICKABLE设置为true,setOnLongClickListener也会将View的LONG_CLICKABLE设置为true,在源码可以找到,这里就不贴代码了。
结论:
- 正常情况下,一个事件序列只能被一个View拦截且消费,因为一旦一个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一事件序列中的事件不能分别由两个View同时处理,但是通过特殊的手段可以实现,比如一个View将本该自己处理的事件通过onTouchEvent返回false,强行传给其他View
- 某个View一旦决定拦截,那么一个事件序列都只能有它处理,并且onInterceptTouchEvent不会被调用。
- 某个View一旦开始处理事件,如果不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会交给它处理,并且事件将重新交给父元素去处理。
- 如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件由Activity来处理。
- ViewGroup内部默认不拦截事件
- View中不能拦截事件,一旦有事件交给它,它的onTouchEvent就会被调用
- View的onTouchEvent都默认消费事件,除非它是不可点击的。
- View的enable不影响onTouchEvent的默认返回值。
- onClick的发生前提时当前View可点击,并且它收到了down和up事件
- 事件传递过程是由外向内的,即事件总是先传递给父元素,容纳后再由父元素进行分发,通过requestDisalowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发,但是ACTION_DOWN除外
滑动冲突
- 在界面中,只要内外两层同时可以滑动,这个时候就会产生滑动冲突
常见的滑动冲突场景:
- 外部滑动和内部滑动方向不一致;
- 外部滑动方向和内部滑动方向一致;
- 上面两种情况的嵌套。
滑动冲突的处理规则
- 对于场景一,处理的规则是:当用户左右(上下)滑动时,需要让外部的View拦截点击事件,当用户上下(左右)滑动的时候,需要让内部的View拦截点击事件。根据滑动的方向判断谁来拦截事件。
- 对于场景二,由于滑动方向一致,这时候只能在业务上找到突破点,根据业务需求,规定什么时候让外部View拦截事件,什么时候由内部View拦截事件。
- 场景三的情况相对比较复杂,同样根据需求在业务上找到突破点。
滑动冲突的解决方式
- 外部拦截法:所谓的外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。下面是伪代码:
|
|
- 内部拦截法:内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是伪代码:
|
|
- 除了子元素需要做处理外,父元素也要默认拦截ACTION_DOWN以外的事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需要的事件,因此,父元素需要修改:12345678public boolean onInterceptTouchEvent(MotionEvent even) {int action = even.getAction();if(about == MotionEvent.ACTION_DOWN) {return false;} else {return true;}}