关于android:Android事件分发机制

39次阅读

共计 17365 个字符,预计需要花费 44 分钟才能阅读完成。

原文链接:https://juejin.im/post/5eb3e0…

这次说下 Android 中的事件散发机制
从开始点击屏幕开始,就会产生从 Activity 开始到 decorview 始终到最里层的 view 一连串事件传递。每一层 view 或者 viewgroup 都会首先调用它的 dispatchTouchEvent 办法,而后判断是否就在以后一层生产掉事件

view 的事件散发

首先上一段伪代码,是在书上看到的,也是我感觉总结的最好的

 public boolean dispatchTouchEvent(MotionEvent event) {
    boolean isConsume = false; 
    if (isViewGroup) {if (onInterceptTouchEvent(event)) {isConsume = onTouchEvent(event); 
        } else {isConsume = child.dispatchTouchEvent(event); 
        } 

    } else {        
//isView       
isConsume = onTouchEvent(event);   
     }    
    return isConsume;
}

复制代码如果以后是 viewgroup 层级,就会判断 onInterceptTouchEvent 是否为 true,如果为 true,则代表事件要生产在这一层级,不再往下传递。接着便执行以后 viewgroup 的 onTouchEvent 办法。如果 onInterceptTouchEvent 为 false,则代表事件持续传递到下一层级的 dispatchTouchEvent 办法,接着一样的代码逻辑,始终到最外面一层的 view。
ok,还没完哦,到最外面一层就会间接执行 onTouchEvent 办法,这时候,view 有没有权力回绝生产事件呢?按情理 view 作为最底层的,应该是没有发言权才对。然而呢,秉着偏心公正准则,view 也是能够回绝的,能够在 onTouchEvent 办法返回 false,示意他不想生产这个事件。那么这个事件又会怎么解决呢?见上面一段伪代码:

public void handleTouchEvent(MotionEvent event) {if (!onTouchEvent(event)) {getParent.onTouchEvent(event);
    }
}

复制代码如果 view 的 onTouchEvent 办法返回 false,那么它的父容器的 onTouchEvent 又会被调用,如果父容器的 onTouchEvent 又返回 false,则又交给上一级。始终到最上层,也就是 Activity 的 onTouchEvent 被调用。

至此,生产流程结束
然而,对于 onTouch,onTouchEvent 和 onClick 又是怎么样的调用关系呢?
那就再来一段伪代码:

 public void consumeEvent(MotionEvent event) {if (setOnTouchListener) {onTouch(); 
        if (!onTouch()) {onTouchEvent(event);
         } 
    } else {onTouchEvent(event); 
    }

    if (setOnClickListener) {onClick();
    }
}

复制代码当某一层 view 的 onInterceptTouchEvent 被调用,则代表以后层级要生产事件。如果它的 onTouchListener 被设置了的话,则 onTouch 会被调用,如果 onTouch 的返回值返回 true,则 onTouchEvent 不会被调用。如果返回 false 或者没有设置 onTouchListener,则会持续调用 onTouchEvent。而 onClick 办法则是设置了 onClickListener 则会被失常调用。

这里用一张流程图总结下:

<figcaption style=”margin: 10px 0px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; line-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 技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!

正文完
 0