一、知识前瞻
- 用户对屏幕的操作的事件可以划分为3种最基础的事件:ACTION_DOWN、ACTION_MOVE、ACTION_UP
- 事件分发机制分为三部分:事件生产、事件分发 、事件消费
二、事件分发
1.主要方法
- dispatchTouchEvent:用于进行点击事件的分发
- onInterceptTouchEvent:用于进行点击事件的拦截,只有 ViewGroup才有
- onTouchEvent:用于处理点击事件
2.主要流程
当一个点击事件发生时,事件分发从Action_Down开始,首先会将点击事件传递到Activity中,具体是执行Activity的dispatchTouchEvent()进行事件分发,不拦截不中断的正常分发流程:
- Activity.dispatchTouchEvent()
- Window.superDispatchTouchEvent()
- DecorView.superDispatchTouchEvent()
- ViewGroup.dispatchTouchEvent() 实现了事件传递 activity -> ViewGroup
在ViewGroup的dispatchTouchEvent方法中调用onInterceptTouchEvent判断是否拦截
- 若拦截调用ViewGroup的onTouchEvent方法,该ViewGroup消费掉;
- 若不拦截,该ViewGroup遍历子View,根据点击的位置等条件判断是否有满足接收事件条件的子View?
- 若有,分发给该子View的dispatchTouchEvent()方法,然后会调用View的onTouchEvent方法,在onTouchEvent方法中会判断该子View是否可点击
- 是,则事件最终传递到View的onClick方法消费;
否则,事件返回向上传递,直到消费或者终止。
3.view的onTouchEvent、OnClickListener和onTouchListener的onTouch优先级
- onTouch:指的是View设置的OnTouchListener接口的onTouch()
当一个View绑定了OnTouchLister后,当有touch事件触发时,就会调用onTouch方法。
(当把手放到View上后,onTouch() 被一遍一遍地被调用),如果onTouch返回值为true,表示这个touch事件被onTouch方法处理完毕,不会把touch事件再传递给Activity,否则,touch事件被传递给Activity,onTouchEvent方法被调用
- onTouchEvent:指的是事件分发中的重要方法,消费(dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent)重写了Activity的onTouchEvent方法后,当屏幕有touch事件时,此方法就会被调用。
- onClick:指的是View设置的OnClickListener接口的onClick()
优先级从高到低:onTouch > onTouchEvent > onClick
4.ACTION_CANCEL触发时机
子View在处理一个Touch事件中,父View的onInterceptTouchEvent返回true,此时子View会接收到MotionEvent.Action_Cancel。
5.事件是先到DecorView还是先到Window
ViewRootImpl -> DecorView -> Activity -> PhoneWindow -> DecorView -> ViewGroup
这个流程为什么绕来绕去的,光DecorView就走了两遍。
主要原因就是解耦。
ViewRootImpl并不知道有Activity这种东西存在,它只是持有了DecorView。所以先传给了DecorView,而DecorView知道有AC,所以传给了AC。
Activity也不知道有DecorView,它只是持有PhoneWindow,所以这么一段调用链就形成了
6.点击事件被拦截,但是想传到下面的view,如何操作
重写子类的requestDisallowInterceptTouchEvent()方法返回true就不会执行父类的onInterceptTouchEvent(),即可将点击事件传到下面的View
7.如何解决view的事件冲突
1.外部滑动方向与内部方向不一致。
比如你用ViewPaper和Fragment搭配,而Fragment里往往是一个竖直滑动的ListView,这种情况是就会产生滑动冲突,但是由于ViewPaper本身已经处理好了滑动冲突,所以我们无需考虑,不过若是换成ScrollView,我们就得自己处理滑动冲突了
处理思路:
主要是一个横向一个竖向的,所以我们只要判断滑动方向是竖向还是横向的,再让对应的View滑动即可。判断的方法有很多,比如竖直距离与横向距离的大小比较;滑动路径与水平形成的夹角等等。
2.外部方向与内部方向一致。
因为内部和外部滑动方向一致,系统会分不清你要滑动哪个部分,所以会要么只有一层能滑动,要么两层一起滑动得很卡顿
处理思路:
对于这种情况,比较特殊,我们没有通用的规则,得根据业务逻辑来得出相应的处理规则。举个最常见的例子,ListView下拉刷新,需要ListView自身滑动,但是当滑动到头部时需要ListView和Header一起滑动,也就是整个父容器的滑动。如果不处理好滑动冲突,就会出现各种意想不到情况。
滑动冲突的处理方法:
- 让事件都经过父容器的拦截处理,如果父容器需要则拦截,如果不需要则不拦截,称为外部拦截法
public boolean onInterceptTouchEvent(MotionEvent event) {boolean intercepted = false;int x = (int)event.getX();int y = (int)event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN: {intercepted = false;break;}case MotionEvent.ACTION_MOVE: {if (满足父容器的拦截要求) {intercepted = true;} else {intercepted = false;}break;}case MotionEvent.ACTION_UP: {intercepted = false;break;}default:break;}mLastXIntercept = x;mLastYIntercept = y;return intercepted; }
首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器,子元素就没有机会处理事件了。其次是up事件也返回了false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。
- 父容器不拦截任何事件,将所有事件传递给子元素,如果子元素需要则消耗掉,如果不需要则通过requestDisallowInterceptTouchEvent方法交给父容器处理,称为内部拦截法
首先需要重写子元素的dispatchTouchEvent方法:
@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {int x = (int) event.getX();int y = (int) event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN: {parent.requestDisallowInterceptTouchEvent(true);break;}case MotionEvent.ACTION_MOVE: {int deltaX = x - mLastX;int deltaY = y - mLastY;if (父容器需要此类点击事件) {parent.requestDisallowInterceptTouchEvent(false);}break;}case MotionEvent.ACTION_UP: {break;}default:break;}mLastX = x;mLastY = y;return super.dispatchTouchEvent(event);}
然后修改父容器的onInterceptTouchEvent方法:
@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {int action = event.getAction();if (action == MotionEvent.ACTION_DOWN) {return false;} else {return true;}}