View的事件分发机制以及滑动冲突
[TOC]
点击事件的传递规则
点击时间的分发过程 总是绕不过三个很重要的方法来共同完成:dispatchTouchEvent(MotionEvent ev), onIntercepTouchEvent(MotionEvent ev), onTouchEvent(MotionEvent ev)
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果时间能够分发到当前View,那么此方法一定会被调用,返回的结果受View的OntouchEvent和下级View的dispatchEvent方法的影响,表示是否消耗当前事件。
public boolean onIntercepTouchEvent(MotionEvent ev)
只有ViewGroup才会拥有的方法,用于拦截某个事件,如果当前的View拦截某个事件,那么在同一个时间序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
**public boolean onTouchEvent(MotionEvent ev) **
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接受到事件。
那么这三个方法的调用顺序是如何呢?
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,此时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,及它的onTouchEvent方法就会被调用,如果它的onInterceptTouchEvent返回为false,表示它不拦截当前事件,此时当前事件就会继续传递给它的子元素,此时如果是View,则会直接调用onTouchEvent方法。
OnTouchListener, View.onTouchEvent 和OnclickListener的区别
当一个View需要处理事件。设置了OnTouchListener,则OnTouchListener的onTouch方法会被回调,如果onTouch的方法返回True,则View.onTouchEvent方法不会被调用,反之则会被调用。View.onTouchEvent方法中,如果当前设置的有OnclickListener,其优先级最低。
这三者的优先级: OnTouchListener -> View.onTouchEvent -> OnclickListener
当一个点击事件产生后,它的传递过程遵循如下顺序 Activity -> PhoneWindow -> RootView。 由Activity 传给PhoneWindow Window 最后传给顶级View。
关于时间传递的机制,我们首先在这里给一些结论,
- 同一事件顺序是指手指接触屏幕的那一刻起, 到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个时间序列以Down事件开始,中间含有数量不一的move事件。最后以up事件结束。
- 正常情况下,一个事件序列只能被一个View拦截且消耗,因此一个时间序列的事件不能分别由两个View同时处理,但是我们可以通过代码控制事件传递。
- 某个View一旦决定拦截,那么一个事件序列都只能由它来处理,并且它的onIntercepTouchEvent不会再被调用。当一个View决定拦截一个事件后,同一事件的剩下事件也会交给它来处理,也就是说onIntercepTouchEvent不会被再调用。
- 某个View一旦开始处理事件,如果他不消耗ACTION_DOWN事件(OnTouchEvent返回为false),那么同一事件中的其他时间都不会再交给它来处理,并且事件将重新交由它的父元素去处理。
- 如果View不消耗除了ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理
- ViewGroup默认是不拦截任何事件,默认onInterceptTouchEvent方法默认返回False
- View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
- View的OntouchEvent默认都是会消耗事件,除非它是不可以点击的(clickable longclickable同时为false)。 View的longClickable属性都是false,clickable属性要分情况,Button的clickable属性默认为true,TextView的clickable属性默认为false。 当然 如果给view设置了setOnclickListener 或者setOnLongClickListener 会默认开启。
- 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子元素,通过requestDisallowInterceptTouchEvent方法可以在子元素中干涉父元素的事件分发过程,但是ACTION_DOWN事件除外。
事件分发的源码分析
点击事件是有MotionEvent来表示,当一个点击事件发生,最先传入的是Activity,由Activity的disPatchEvent来进行事件派发,,具体工作是由Activity内部的Window来完成的。 Window会将时间传递给DecorView,一般DecorView就是当前界面的顶级容器(即是setContentView所设置的View的父容器),如图:
public boolean disPatchTouchEvent(MotionEvent ev){
if(ev.getAction == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent(ev)){
return truel
}
return onTouchEvent(ev);
}
Window是如何将事件传递给ViewGroup的呢,Window类其实是一个抽象类 它可以控制顶级view的外观和行为策略,Window的唯一实现是PhoneView类,
publlic boolean superDisPatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}
这个mDecot其实就是我们getWindow().getDecorView()返回的View,我们通过设置setContentView设置的View就是它的一个子View,自此事件传递到了顶级View,即我们设置的SetContentView所设置的View。
ViewGroup的事件分发机制
我们在看一下ViewGroup对点击事件的分发过程,主要实现在disPatchEvent方法中,
final boolean intercepted
if(actionMasked == MotionEvent.Action_Down || mFirsrtTouchTarget != null){
final boolean disallowIntercept = (GroupFlag & FLAG_DISALLOW_INTERCEPT) != 0;
if(!disallowIntercept){
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
}else{
intercepted = false;
}
}else{
intercepted = true;
}
由ViewGroup的子元素成功处理时, mFirsrtTouchTarget 会被赋值并指向子元素。所以一旦事件是由当前的ViewGroup拦截,接下来同一时序的其他事件都会默认交给ViewGroup来处理。
另外一个特殊情况就是FLAG_DISALLOW_INTERCEPT标识符,这这个一般通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View,一般设置ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件,因为ViewGroup在事件分发的时候会重置FLAG_DISALLOW_INTERCEPT标识符,这意味着当面对ACTION_DOWN的时候,ViewGroup一定会调用onInterceptTouchEvent来判断自己是否需要拦截事件
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
首先遍历所有的ViewGroup的所有子元素,判断是否能够接受到点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内,dispatchTransformedTouchEvent就是调用了child的dispatchTouchEvent方法, 如果子元素的disPatchTouchEvent返回false 则会继续分发给下一个子元素(如果有的话), 在addTouchTarget(child, idBitsToAssign) 给mFirsrtTouchTarget 赋值。mFirsrtTouchTarget 是否为null直接影响ViewGroup对事件的拦截策略。
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
{
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
}
如果遍历了所有的子元素事件都没有被合适处理,这包含两种情况: 第一种是ViewGroup没有子元素,第二种是子元素处理了点击事件,但是在dispatchTouchEvenr中返回了false 一般是子元素在OnTouchEvent中返回了false,这是ViewGroup会自己处理点击事件。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
View的事件分发
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
首先判断是否有设置OnTouchListener,如果onTouchListener中的Touch返回true,那么onTouchEvent就不会被调用。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
只要View的CLICKABLE或者是LONG_CLICKABLE 那么他就会消耗这件事,当ACTION_UP事件发生 会触发performClick() 如果设置了OnclickListener ,performClick就是调用Onclick方法。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
}
设置Viewd的OnclickListener和OnLongClickListener()会自动将View的Clickable,LongClickAble为true。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
View的滑动冲突
常见的滑动冲突场景
- 场景1 ------ 外部滑动方向和内部滑动方向不一致
- 场景2------- 外部滑动方向和内部滑动方向一致
- 场景3------- 上面两种情况的嵌套
对应处理的方法:
- 当用户左右滑动时,需要外部拦截点击事件,当用户需要上下滑动时,需要让内部拦截点击事件
- 这种场景无法根据滑动的角度, 距离差已经速度差来做判断 一般需要从业务上找到突破口
- 同上,一般也是从业务的需要上得出相应的处理规则
两种滑动冲突的解决方案:
-
外部拦截法
所谓的外部拦截法就是所有的点击事件都先经过父容器的拦截处理,如果父容器需要此事件则拦截 如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制,外部拦截法需要重写父容器的onInterceptTouchEvent方法。伪代码如下
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: mLastY = y; break; case MotionEvent.ACTION_MOVE: float dy = y - mLastY; if(父容器需要当前点击事件){ return true; } break; } // 默认返回的都是false return super.onInterceptTouchEvent(ev); }
-
内部拦截法
内部拦截法主要是父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗,否则交由父容器进行处理,需要配合requestDisallowInterceptTouchEvent()才能正常工作,一般内部拦截法比较的复杂,它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction(); float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true) break; case MotionEvent.ACTION_MOVE: if(父容器需要当前点击事件){ return parent.requestDisallowInterceptTouchEvent(false); } break; } // 默认返回的都是false return super.dispatchTouchEvent(ev); }
面对不同的滑动策越的时候只需要修改ACTION_MOVE事件即可,其他不需要动也不能改动。除了子元素需要处理外,父元素也要默认拦截除了ACTION_DOWN以外的所有其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)时,父元素才能继续拦截所需要的事件。
-