Android事件分发机制

技术Android事件分发机制 Android事件分发机制原文链接:https://juejin.im/post/5eb3e0d6f265da7c002028cd这次说下Android中的事件分发机制从

安卓事件分发机制

原文:https://juejin.im/post/5eb3e0d6f265da7c002028cd

这次我来说说Android中的事件分发机制。

从点击屏幕开始,一系列事件将从“活动”到“去视图”传输到最里面的视图。视图或视图组的每个层将首先调用其dispatchTouchEvent方法,然后确定是否在当前层使用该事件。

view的事件分发

首先伪代码最后一段是在一本书上读到的,我觉得是最好的总结。

公共布尔dispatchttouchevent(MotionEvent事件){ 0

布尔值isConsume=false

if(IsView GrouP){ 0

if(on interceptouchevent(event)){ 0

isConsume=onTouchEvent(事件);

} else {

is consume=child . dispatchttouchevent(事件);

}

} else {

//isView

isConsume=onTouchEvent(事件);

}

return isConsume

}

如果复制代码当前处于视图组级别,它将确定onInterceptTouchEvent是否为真。如果是真的,则意味着事件将在此级别被消耗,不会被传递下去。然后,执行当前视图组的onTouchEvent方法。如果onInterceptTouchEvent为false,则意味着事件继续传递给下一级的dispatchTouchEvent方法,然后相同的代码逻辑继续,直到视图的最内层。

好吧,还没结束。在最内层,将直接执行onTouchEvent方法。在这个时候,观点是否有权拒绝消费事件?根据原则,视图作为底层,应该没有发言权。但是,根据公平正义的原则,观点也可以被拒绝。它可以在onTouchEvent方法中返回false,以指示他不想使用此事件。那么这个事件会如何处理呢?请参见以下伪代码:

public void handleTouchEvent(MotionEvent事件){ 0

if(!ontoucheevent(event)){ 0

getParent.onTouchEvent(事件);

}

}

复制代码如果视图的onTouchEvent方法返回false,那么它的父容器的onTouchEvent将被再次调用,如果父容器的onTouchEvent返回false,它将被移交给下一个更高的级别。直到顶层,也就是活动的本体事件被调用。

至此,消费过程完成。

然而,onTouch、onTouchEvent和onClick的调用关系是什么?

那我们再来看看另一段伪代码:

public void consumeEvent(motion event事件){ 0

if(SetTouchListener){ 0

onTouch();

if(!onTouch()){ 0

事件;

}

} else {

事件;

}

if(setnclicklistener){ 0

onClick();

}

}

复制代码当调用某个视图级别的onInterceptTouchEvent时,这意味着当前级别想要使用该事件。如果它的onTouchListener,将调用onTouch如果OnTouch的返回值返回true,则不会调用onTouch。如果返回false或未设置onTouchListener,将继续调用onTouchEvent。另一方面,如果设置了onClickListener,则通常会调用onClick方法。

下面是一个流程图来总结:

fig caption style=' margin : 10px 0px 0px;padding: 0pxoutline: 0px最大宽度:100%;盒子大小的:边框盒子!重要;溢出-wrap:断字!重要;l

ine-height: inherit; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;"在这里插入图片描述/figcaption

源码分析

一个触摸事件,首先是传到Activity层级,然后传到根view,通过一层层的viewgroup最终到底最里面一层的view,我们来一层层解析
Activity(dispatchTouchEvent)
直接上代码

 //Activity.java 
    public boolean dispatchTouchEvent(MotionEvent ev) {         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             onUserInteraction(); 
        } 
        if (getWindow().superDispatchTouchEvent(ev)) {
             return true;
         } 
        return onTouchEvent(ev);
    }
    public void onUserInteraction() {
    }

复制代码这里可以看到,onUserInteraction方法是空的,主要是调用了getWindow().superDispatchTouchEvent(ev)方法,返回true,就代表事件消费了。返回false,就代表下层没人处理,那就直接到了activity的onTouchEvent方法,这点跟之前的消费传递也是吻合的。
继续看看superDispatchTouchEvent方法,然后就走到了PhoneWindow的superDispatchTouchEvent方法,以及DecorView的superDispatchTouchEvent,看看代码:

  //PhoneWindow.java 
    private DecorView mDecor; 
    @Override
     public boolean superDispatchTouchEvent(MotionEvent event) {         return mDecor.superDispatchTouchEvent(event);
    } 
    //DecorView.java     public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

复制代码这里可以看到,依次经过了PhoneWindow到达了DecorView,DecorView是activity的根view,也是setcontentView所设置的view的父view,它是继承自FrameLayout。所以这里super.dispatchTouchEvent(event)方法,其实就是走到了viewgroup的dispatchTouchEvent 方法。

ViewGroup(dispatchTouchEvent)

    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
        if (onFilterTouchEventForSecurity(ev)) { 
            // Check for interception,表示是否拦截的字段 
            final boolean intercepted; 
            if (actionMasked == MotionEvent.ACTION_DOWN 
                    || mFirstTouchTarget != null) { 
              //FLAG_DISALLOW_INTERCEPT标志是通过requestDisallowInterceptTouchEvent设置                 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;
            }
          //mFirstTouchTarget赋值           
 while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget  target == newTouchTarget) {
                    } else {
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            continue;
                        }
                    }
                }
         }

复制代码这里截取了部分关键的代码,首先是两个条件

  • actionMasked == MotionEvent.ACTION_DOWN

  • mFirstTouchTarget != null

如果满足了其中一个条件才会继续走下去,执行onInterceptTouchEvent方法等,否则就直接intercepted = true,表示拦截。
第一个条件很明显,就是表示当前事件位按下事件(ACTION_DOWN)
第二个条件是个字段,根据下面的代码可以得知,当后面有view消费掉事件的时候,这个mFirstTouchTarget字段就会赋值,否则就为空。

所以什么意思呢,当ACTION_DOWN事件时候,一定会执行到后面代码。当其他事件来的时候,要看当前viewgroup是否消费了事件,如果当前viewgroup已经消费了事件,没传到子view,那么mFirstTouchTarget字段就为空,所以就不会执行到后面的代码,就直接消费掉所有事件了。

这就符合了之前的所说的一种机制:
某个view一旦开始拦截,那么后续事件就全部就给它处理了,也不会执行onInterceptTouchEvent方法了

但是,两个条件满足了一个,就能执行到onInterceptTouchEvent了吗不一定,这里看到还有一个判断条件:disallowIntercept。这个字段是由requestDisallowInterceptTouchEvent方法设置的,后面我们会讲到,主要用于滑动冲突,意思就是子view告诉你不想让你拦截,那么你就不拦截了,直接返回false。

ok,继续看源码,之前的内容我们了解到,如果viewgroup不拦截事件,应该会传递给子view,那在哪里传的呢继续看看dispatchTouchEvent的代码:

 if (!canceled  !intercepted) { 
                    final int childrenCount = mChildrenCount;                     if (newTouchTarget == null  childrenCount != 0) {                         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 (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                            newTouchTarget = getTouchTarget(child);
                            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();
                    }
                }
            }

复制代码这里可以看到,进行了一个子view的便利,其中,如果满足两个条件中的一个,就跳出。否则就执行dispatchTransformedTouchEvent方法。先看看这两个条件:

  • !child.canReceivePointerEvents()

  • !isTransformedTouchPointInView(x, y, child, null)

看名字是看不出啥了,直接看代码吧:

 protected boolean canReceivePointerEvents() {         return (mViewFlags  VISIBILITY_MASK) == VISIBLE || getAnimation() != null; 
    } 
    protected boolean isTransformedTouchPointInView(float x, float y, View child,             PointF outLocalPoint) { 
        final float[] point = getTempPoint(); 
        point[0] = x;
         point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView  outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

复制代码哦,原来是这个意思。canReceivePointerEvents方法就代表view是不是可以接受点击事件,比如是不是在播放动画。而isTransformedTouchPointInView方法代表点击事件的坐标是不是在这个view的区域上面。
ok,如果条件都满足,就执行到dispatchTransformedTouchEvent方法了:

 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;
        }
}

复制代码这个方法大家应该都猜到了,其实就是执行了child.dispatchTouchEvent(event)。也就是下一层view的dispatchTouchEvent方法呗,开始事件的层级传递。

View(dispatchTouchEvent)

到view 层级的时候,自然就执行的view的dispatchTouchEvent,上代码

   public boolean dispatchTouchEvent(MotionEvent event) { 
        boolean result = false; 
 
       if (mInputEventConsistencyVerifier != null) {             mInputEventConsistencyVerifier.onTouchEvent(event, 0); 
        } 
        final int actionMasked = event.getActionMasked();         if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags  ENABLED_MASK) == ENABLED  handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null  li.mOnTouchListener != null
                     (mViewFlags  ENABLED_MASK) == ENABLED
                     li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            if (!result  onTouchEvent(event)) {
                result = true;
            }
        }
        if (!result  mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN  !result)) {
            stopNestedScroll();
        }
        return result;
    }

复制代码这里可以看到,首先会判断li.mOnTouchListener != null,如果不为空,就会执行onTouch方法。
根据onTouch方法返回的结果,如果为false,result就为false,那么onTouchEvent才会执行。这个逻辑也是符合我们之前说的传递方式。
最后我们再看看view的onTouchEvent都做了什么事:

 final boolean clickable = ((viewFlags  CLICKABLE) == CLICKABLE                 || (viewFlags  LONG_CLICKABLE) == LONG_CLICKABLE)                 || (viewFlags  CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; 
 if (clickable || (viewFlags  TOOLTIP) == TOOLTIP) { 
            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)) {
                                    performClickInternal();
                                }
                            }
                        }
                    }
                    mIgnoreNextUpEvent = false;
                    break;
            }
            return true;
        }

复制代码从代码可以得知,如果设置了CLICKABLE或者LONG_CLICKABLE,那么这个view就会消费事件,并且执行performClickInternal方法,然后执行到performClick方法。这个performClick方法大家应该都很熟悉,就是触发点击的方法,其实内部就是执行了onClick方法。

     private boolean performClickInternal() { 
        notifyAutofillManagerOnClick();
        return performClick(); 
    } 
    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;
        }
        return result;
    }

复制代码至此,源代码也看的差不多了,内部其实有很多细节,这里也就不一一说明了,大家有空可以去研究下。

事件分发的应用(requestDisallowInterceptTouchEvent)

那既然学会了事件分发机制,我们实际工作中会怎么应用呢其实最常见的就是解决滑动冲突的问题。一般有两种解决办法:

  • 一种是外部拦截:从父view端处理,根据情况决定事件是否分发到子view

  • 一种是内部拦截:从子view端处理,根据情况决定是否阻止父view进行拦截,其中的关键就是requestDisallowInterceptTouchEvent方法。

第一种方法,其实就是在onInterceptTouchEvnet方法里面进行判断返回true还是返回false。
第二种方法,就是用到了requestDisallowInterceptTouchEvent方法,这个方法的意思就是让父view不要去拦截事件了,在dispatchTouchEvent方法里面就有这个标志位:FLAG_DISALLOW_INTERCEPT,如果disallowIntercept字段为true,就不会去执行onInterceptTouchEvent方法,而是返回false,不拦截事件。
上代码:

     //外部拦截法:父view.java         
    @Override 
    public boolean onInterceptTouchEvent(MotionEvent ev) { 
        boolean intercepted = false; 
       //父view拦截条件 
        boolean parentCanIntercept; 
        switch (ev.getActionMasked()) {             
    case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentCanIntercept) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        return intercepted;
   }

复制代码外部拦截很简单,就是判断条件,然后决定是否进行拦截。

     //父view.java             
    @Override 
    public boolean onInterceptTouchEvent(MotionEvent ev) {         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { 
            return false; 
        } else { 
            return true; 
        } 
    }
    //子view.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //父view拦截条件        
boolean parentCanIntercept;
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (parentCanIntercept) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

复制代码感觉内部拦截有点复杂呀,还要重写父view的方法,这里分析下,为什么要去这么写:

  • 父view ACTION_DOWN的时候,不能拦截,因为如果拦截,那么后续事件也就跟子view无关了

  • 父view 其他事件的时候,要返回true,表示拦截。因为onInterceptTouchEvent方法的调用是被FLAG_DISALLOW_INTERCEPT标志位所控制,所以子view需要父view拦截的时候,才会走到这个onInterceptTouchEvent方法中来,那么这时候要保证方法中一定是要拦截的。

至此,事件的分发机制也就说的差不多了。

文末

您的点赞收藏就是对我最大的鼓励!
欢迎关注我,分享Android干货,交流Android技术
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/136275.html

(0)

相关推荐

  • miniui datagrid的客户端分页解决方法是怎样的

    技术miniui datagrid的客户端分页解决方法是怎样的这期内容当中小编将会给大家带来有关miniui datagrid的客户端分页解决方法是怎样的,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大

    攻略 2021年11月16日
  • vue中如何实现后台进程定时爬取头条文章

    技术vue中如何实现后台进程定时爬取头条文章这篇文章将为大家详细讲解有关vue中如何实现后台进程定时爬取头条文章,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

    攻略 2021年11月24日
  • 大圈是什么意思,太阳周围有个大圈是什么意思

    技术大圈是什么意思,太阳周围有个大圈是什么意思日晕是一种大气光学现象,是日光通过卷层云时,受到冰晶的折射或反射而形成的大圈是什么意思。当光线射入卷层云中的冰晶后,经过两次折射,分散成不同方向的各色光。有卷层云时,天空中会

    生活 2021年10月21日
  • 如何解决C# 7.2中的结构体性能问题

    技术如何解决C# 7.2中的结构体性能问题本篇内容介绍了“如何解决C# 7.2中的结构体性能问题”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔

    攻略 2021年11月26日
  • 如何在线将传统模式复制改为GTID复制模式

    技术如何在线将传统模式复制改为GTID复制模式这篇文章将为大家详细讲解有关如何在线将传统模式复制改为GTID复制模式,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1.在每一台服务器上设

    攻略 2021年11月2日
  • ar发音,有“ar”的英语单词有哪些

    技术ar发音,有“ar”的英语单词有哪些1、car 汽车2、bar 酒吧3、cartoon 卡通4、park 公园5、hard 努力的6、large 大的7、far 远的8、star 星星9、mark 标记10、farm

    生活 2021年10月21日