事件分发机制



  1. onClick 和 onTouch都会打印吗
    事件分发开始会走到这里View.dispatchTouchEvent()->
    li.mOnTouchListener.onTouch(this, event)->
    false的话->onTouchEvent(event)->case MotionEvent.ACTION_UP->
    performClick();-> li.mOnClickListener.onClick(this);

    所onTouch返回false时会执行onClick,true则不会

image-20200426160519980

ViewPager包含Listview

  1. onInterceptTouchEvent返回true时 为什么只能左右滑动 不能上下滑

onInterceptTouchEvent 返回true不会问下面了,直接处理 消费掉,

return false 才会传给下面.

  1. onInterceptTouchEvent返回true时 为什么只能上下滑动 不能左右滑
[Activity.java]
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
[Activity.java]  
public Window getWindow() {
        return mWindow;
  }
  private Window mWindow;

Window只有唯一的实现类PhoneWindow

[Window.java]
The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {...}
[PhoneWindow.java]
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
[DecorView.java]
public class DecorView extends FrameLayout ..{...}
public boolean superDispatchTrackballEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

FrameLayout没有重写dispatchTouchEvent,接下来,直接到了

  1. disallowIntercept:是否禁用掉拦截,可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)请求不拦截
  2. 先走ViewGroup的onInterceptTouchEvent,默认返回false
  3. ViewGroup不拦截的话,分发事件,怎么分发的?
    1. buildTouchDispatchChildList():根据Z值对它的子view 进行排序,返回一个ArrayList
    2. 遍历判断
      1. canViewReceivePointerEvents:是否能够接收点击事件
      2. isTransformedTouchPointInView:点击事件是否在点击的view区域中
      3. dispatchTransformedTouchEvent分发调用child.dispatchTouchEvent, 这里child如果是ViewGroup,相当于这里递归调用了VIewGroup的dispatchTouchEvent;向下传递的过程中,如果拦截/响应了事件则终止向下传递,并返回给父,父也不用处理了
      4. 拦截/响应事件后设置参数
        1. newTouchTarget = addTouchTarget(child,) = mFirstTouchTarget;
        2. alreadyDispatchedToNewTouchTarget = true
[ViewGroup.java]
 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
      .................................
         // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
      ...............
        //2.事件分发  
        if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                           .......
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
      ·················  
       // 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);
            }                                                
    		...          
    }

dispatchTransformedTouchEvent是对事件的分发或者处理:

  • 返回值变量handled: false表示无人处理,true表示activity处理了

  • (拦截的话走到这里)child == null时,调用viewdispatchTouchEvent(),就是转到View的事件处理,处理就返回true,赋值给handled变量作为dispatchTransformedTouchEvent的返回值,true表示;

private boolean dispatchTransformedTouchEvent(MotionEvent event, private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

第一个是Down事件

再来是Move事件,会沿着(前面形成的事件分发链)销售链走

怎么沿着销售链走的?

  1. Move事件照样会走到Activity …ViewGroup的dispatchTouchEvent,还是有拦截事件的权利
  2. 此时 mFirstTouchTarget != null
  3. 如果第2步不拦截, 会走到dispatchTransformedTouchEvent(ev,cancelChild,targe.child,..),target是自己,然后(递归)去分发

当销售链形成后,11号有什么权利?底层才对上层有反向制约的权利,通过对disallowIntercept(是否不允许拦截)布尔变量设置

1.onInterceptTouchEvent返回true就是对事件的拦截(ViewGroup是经销商?),如果disallowIntercept=true,则ViewPager中onInterceptTouchEvent返回true也没用。

[ViewGroup.java] 
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

2.requestDisallowInterceptTouchEvent(boolean disallowIntercept)请求不拦截

  • ViewPager包裹ListView时,让ViewPager不拦截

    在ListView中重写如下方法,

    • ACTION_MOVE时即左右滑动时,viewPager拦截
    • ACTION_DOWN时,上下滑动时,viewPager不拦截

    ![image-20200426115035531](.事件分发机制_images/image-20200426115035531.png

    image-20200426115150064

  1. 销售链的形成需要经历完ACTION_DOWN,如果父view在形成之前拦截了则销售链就只到父view

实际上任何2个view叠加在一起都会产生冲突。

image-20200426120917813

View的事件处理,dispatchTouchEvent->onTouch->onTouchEvent

  1. onTouch和onTouchEvent有什么区别,又该如何使用?

从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

  1. 为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?

如果你阅读了Android滑动框架完全解析,教你如何一分钟实现滑动菜单特效这篇文章,你应该会知道滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。

Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的。

总结:

  1. Android事件分发机制分为View和ViewGroup的事件分发和处理,是从ViewGroup传给view的

  2. 其中View的会调用onTouch()方法,如果返回true,表示这个事件被他消费掉。则onClick不会执行,点击事件失效

  3. ViewGroup的

    ​ 1. 首先看他自己是否要拦截,可重写onInterceptTouchEvent中,返回true表示拦截,不会向下传递,默认是返回false,也可通过requestDisallowInterceptTouchEvent(true)请求禁用掉拦截.该开关优先于onInterceptTouchEvent

    ​ 2. 如果不拦截,则遍历它的子view,递归分发事件,一旦某一层消费掉,就往上回传结果,但父view也就不再能处理。

再举几个例子:

layout中2个button,layout的onInterceptTouchEvent返回true,则button的click事件失效。

Android开发艺术探索-事件分发

1.位置参数

View的位置主要由它的4个顶点来决定,对应View的四个属性:top,left,right,bottom

从3.0开始,

新增x,y是新增x,y是View左上角的坐标,

translationX和translationY是View左上角相对于父容器的偏移量
x = left + translationX;
y = top + translationY;
注:View在平移过程中,top和left表示的是原始左上角的位置信息不会改变,此时改变的是新增的4个参数

2.MotionEvent和TouchSlop

3.VelocityTracker、GestureDetector和Scroller

Scroller

用于实现View的弹性滑动.

View的滑动

4.View的事件分发机制

4.1 点击事件的传递规则

所谓点击事件的事件分发,就是对MotionEvent事件的分发过程,由三个很重要的方法来共同完成.

public boolean dispatchTouchEvent(MotionEvent ev).

用来进行事件的分发,返回是否消耗当前事件.

public boolean onIntecerptTouchEvent(MotionEvent event)

上面方法中的方法,用来判断是否拦截某个事件(ViewGroup 中源码默认返回false表示不拦截)

public boolean onTouchEvent(MotionEvent event)

dispatchTouchEvent方法中调用,用来处理点击事件,返回是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件.

ViewGroup

关于拦截

1.requestDisallowInterceptTouchEvent() 请求禁用拦截

一般用于子View中调用父view的这个方法,让父view不再拦截除了ACTION_DOWN以外的点击事件。

当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onIntecerptTouchEvent询问自己是否拦截事件.

2.当ViewGroup决定拦截事件后,后续的点击事件(不包括ACTION_DOWN)将会默认交给它处理并且不再问自己是否拦截.

3.onIntecerptTouchEvent()不是每次都会调用,如果想提前处理所有的点击事件,要选择dispatchTouchEvent,前提当然是事件能传到当前的ViewGroup.

分发(不拦截时)

事件分发交给它的子View进行处理,怎么分发?

1.循环遍历ViewGroup的所有孩子,交给能接收点击事件(点击事件的坐标是否落在子元素内)的孩子进行处理(dispatchTouchEvent分发)。

2.如果孩子消耗掉这个事件(dispatchTouchEvent返回true),mFirstTouchTarget被赋值并终止循环;

反之,则交给它的下一个孩子进行处理

3,如果遍历所有的子元素后事件都没有被合适地处理(消耗),两种情况

  • 没有孩子
  • 所有孩子处理了但是dispatchTouchEvent中返回了false(没消耗)

mFirstTouchTarget就为null,因为没有赋值。ViewGroup会自己处理点击事件(super.dispatchTouchEvent(event)这里是View的方法,没有拦截,没有分发),进入View的点击事件处理

为什么mFirstTouchTarget为null ,ViewGroup就默认拦截接下来同一序列中的所有点击事件.??

因为

if(事件是ACTION_DOWN || mFirstTouchTarget != if(事件是ACTION_DOWN || mFirstTouchTarget != null){
		先看是否禁用掉了拦截,判断是否拦截
}else{
   intercepted = true;
}

View对点击事件的处理

onTouch(设置了触摸监听器时) 优先于onTouchEvent;

ACTION_UP发生时,会触发performclick方法,内部会调用它的onClick方法;

View的dispatchTouchEvent方法中的switch 对ACTION的分别处理最后都是返回true,表示消耗事件

View的滑动冲突?

1.什么是滑动冲突?

​ 在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。

2.三种场景

  • 父View和子View滑动方向不一致
  • 父View和子View滑动方向一致(系统无法知道让哪一层滑动,卡顿或者只有1层能滑动)
  • 上面两种的嵌套

3.处理规则

  • 场景1:用户左右滑动时,让父View拦截touch事件,上下滑动时,子View拦截/消耗touch事件;根据他们的特征解决滑动冲突。如何判断是竖直还是水平滑动?
  • 场景2:无法根据滑动的角度,距离差(滑动规则),以及速度差来判断,这个时候一般能在业务上找到突破点
  • 场景3:同样是从业务上找突破点

不管多复杂的滑动冲突,他们之间的区别仅仅是滑动规则不同而已。

/**
  * 分析1:onUserInteraction()
  * 作用:实现屏保功能
  * 注:
  *    a. 该方法为空方法
  *    b. 当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
  */
      public void onUserInteraction() { 

      }
b. DecorView继承自FrameLayout,是所有界面的父类
  *     c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup

如果viewgroup不拦截事件, 那么哪些子view可以接收到事件

if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。

Activity对点击事件的分发机制

ViewGroup对点击事件的分发机制

View对点击事件的分发机制

场景一 点击如图的空白区域

image-20200717235536395

W/DemoSocketActivity: W/DemoSocketActivity: dispatchTouchEvent: ACTION_DOWN
W/MyLinearLayout: dispatchTouchEvent: ACTION_DOWN
W/MyLinearLayout: onInterceptTouchEvent: ACTION_DOWN
E/MyLinearLayout: onInterceptTouchEvent: ACTION_DOWN  false
W/MyLinearLayout: onTouchEvent: ACTION_DOWN
E/MyLinearLayout: onTouchEvent: ACTION_DOWN  false
E/MyLinearLayout: dispatchTouchEvent: ACTION_DOWN  false
W/DemoSocketActivity: dispatchTouchEvent: ACTION_DOWNfalse
W/DemoSocketActivity: dispatchTouchEvent: ACTION_UP
W/DemoSocketActivity: dispatchTouchEvent: ACTION_UPfalse

没有ACTION_UP事件是因为MyLinearLayout没处理,MyLinearLayout默认是不可点击的。

点击测试按钮
W/DemoSocketActivity: W/DemoSocketActivity: dispatchTouchEvent: ACTION_DOWN
W/MyLinearLayout: dispatchTouchEvent: ACTION_DOWN
W/MyLinearLayout: onInterceptTouchEvent: ACTION_DOWN
E/MyLinearLayout: onInterceptTouchEvent: ACTION_DOWN  false
W/MyButton: dispatchTouchEvent: ACTION_DOWN
W/MyButton: onTouchEvent: ACTION_DOWN
E/MyButton: onTouchEvent: ACTION_DOWN  true
E/MyButton: dispatchTouchEvent: ACTION_DOWN  true
E/MyLinearLayout: dispatchTouchEvent: ACTION_DOWN  true
W/DemoSocketActivity: dispatchTouchEvent: ACTION_DOWNtrue
W/DemoSocketActivity: dispatchTouchEvent: ACTION_UP
W/MyLinearLayout: dispatchTouchEvent: ACTION_UP
W/MyLinearLayout: onInterceptTouchEvent: ACTION_UP
E/MyLinearLayout: onInterceptTouchEvent: ACTION_UP  false
W/MyButton: dispatchTouchEvent: ACTION_UP
W/MyButton: onTouchEvent: ACTION_UP
E/MyButton: onTouchEvent: ACTION_UP  true
E/MyButton: dispatchTouchEvent: ACTION_UP  true
E/MyLinearLayout: dispatchTouchEvent: ACTION_UP  true
W/DemoSocketActivity: dispatchTouchEvent: ACTION_UPtrue

onTouchEvent返回true是因为Button是可点击的

背诵版总结

从手指接触屏幕 至 手指离开屏幕,这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。事件分发指的是 将点击事件(MotionEvent)传递到某个具体的View & 处理的整个过程。

事件在哪些对象之间进行传递?

答:Activity、ViewGroup、View

事件分发过程主要由3个方法协作完成。

*dispatchTouchEvent() *:事件分发

onInterceptTouchEvent():拦截事件

和onTouchEvent():处理事件

分发从3个方面讲

1.Activity`对点击事件的分发机制

当一个点击事件发生时,事件最先传到ActivitydispatchTouchEvent()进行事件分发

2.ViewGroup`对点击事件的分发机制

先看自己是否拦截,默认可以认为不拦截。一旦拦截,那么这个事件序列(如果事件序列能够传递的话)都只能由它来处理,并且它的onInterceptTouchEvent不再调用,

遍历ViewGroup的所有子元素,判断子元素是否能够接收到点击事件。主要由:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。

3.View`对点击事件的分发机制

某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent 返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父容器去处理。

View的onTouchEvent默认都会消耗事件,除非它是不可点击的。

如何设置了有onTouchListener,且onTouch返回true表示消耗了事件,则不会调用onTouchEvent方法。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!