美文网首页
Android View 事件分发机制

Android View 事件分发机制

作者: htkeepmoving | 来源:发表于2018-11-03 22:00 被阅读0次
    参考资料

    鸿洋版事件分发机制
    郭霖版事件分发机制
    Android开发艺术探索

    Android事件传递整体流程简介

    Android输入事件的源头是位于/dev/input/下的设备节点,而输入事件的终点是由WMS管理的某个窗口,最终由窗口中的View处理。最初的输入事件为内核生成的原始事件,而最终交付给窗口的则是KeyEvent(键盘)或MotionEvent(鼠标和触摸屏)对象。输入事件由Native层进入到Java层的第一个函数是InputEventReceiver.dispatchInputEvent(),这样我们的手指触摸事件(MotionEvent)就传递到Java层,到应用层的传递过程遵循如下顺序:Activity->Window->View;即View事件最先传递给Activity,然后由Activity传递给Window,最后由Window传递给View;顶级View(DecorView)接收到事件后,就会按照事件分发机制去分发事件;事件到达Java层以后是谁将事件传递给Activity?

    //事件传递整体流程:底层生成原始事件后,经过一列加工处理之后,将事件封装成MotionEvent、keyEvent,然后传递到Java层的InputEventReceiver.dispatchTouchEvent中,事件到达Java层之后,如何一步步传递到Activity?以下调用堆栈可以看到事件如何用从InputEventReceive传递到Activity;事件传递到Activity之后,由Activity传递给Window(PhoneWindow),最后由Window传递给顶级View;顶级View(DecorView)接收到事件后,就会按照事件分发机制去分发事件;本文主要是从Activity开始来分析事件分发机制;
    
    //事件详细信息,action表示事件类型,x,y表示事件发生的位置,deviceId是硬件设备的id值
    EventTest: MainActivity dispatchTouchEvent: MotionEvent { action=ACTION_DOWN, actionButton=0, id[0]=0, x[0]=408.94913, y[0]=416.38184, toolType[0]=TOOL_TYPE_MOUSE, buttonState=BUTTON_PRIMARY, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=498914, downTime=498914, deviceId=13, source=0x2002 }
    System.err: java.lang.Exception: EventTest2
    System.err:     at .*********************.MainActivity.dispatchTouchEvent(MainActivity.java:68)
    System.err:     at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:71)
    System.err:     at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:434)
    System.err:     at android.view.View.dispatchPointerEvent(View.java:12029)
    System.err:     at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4834)
    //在onProcess方法中,进行事件类型的判断,然后根据不同的事件类型调用不同的处理方法
    System.err:     at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4644)
    //在deliver中完成事件处理之后,调用finishInputEvent给输入系统一个反馈;
    System.err:     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4176)  
    System.err:     at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4229)
    System.err:     at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4195)
    System.err:     at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4322)
    System.err:     at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4203)
    System.err:     at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4379)
    System.err:     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4176)
    System.err:     at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4229)
    System.err:     at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4195)
    System.err:     at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4203)
    System.err:     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4176)
    System.err:     at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6707)
    System.err:     at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6681)
    System.err:     at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6642)
    System.err:     at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6810)
    //事件由native层正式进入到Java层
    System.err:     at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:187)
    System.err:     at android.os.MessageQueue.nativePollOnce(Native Method)
    System.err:     at android.os.MessageQueue.next(MessageQueue.java:325)
    System.err:     at android.os.Looper.loop(Looper.java:142)
    System.err:     at android.app.ActivityThread.main(ActivityThread.java:6627)
    System.err:     at java.lang.reflect.Method.invoke(Native Method)
    System.err:     at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
    System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
    

    ViewPostImeInputStage.onProcess():

        final class ViewPostImeInputStage extends InputStage {
            @Override
            protected int onProcess(QueuedInputEvent q) {
                // 当onProcess被回调时,processKeyEvent、processPointerEvent、processTrackballEvent、
                // processGenericMotionEvent至少有一个方法就会被调用,这些方法都是属于ViewPostImeInputStage的。
                // 这些方法中都有一句很关键的一句代码处理按键事件
                // mView.dispatchKeyEvent(event)
                // mView.dispatchPointerEvent(event)
                // mView.dispatchTrackballEvent(event)
                // mView.dispatchGenericMotionEvent(event)
                // mView的实例化在ViewRootImpl的setView方法中,其实就是DecorView
                // 这样一来,可以知道ViewPostImeInputStage将事件分发到了DecorView
                if (q.mEvent instanceof KeyEvent) {
                    // Key事件会调用到processKeyEvent,处理key事件,如键盘事件
                    return processKeyEvent(q);
                } else {
                    final int source = q.mEvent.getSource();
                    if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                        //触摸屏事件(MotionEvent),鼠标事件(MotionEvent),鼠标事件到应用层是MotionEvent,不是KeyEvent
                        return processPointerEvent(q);
                    } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                        //轨迹球事件
                        return processTrackballEvent(q);
                    } else {
                        //其它motion事件,如游戏手柄
                        return processGenericMotionEvent(q);
                    }
                }
            }
        }
    

    事件分发整体流程:底层生成原始事件后,经过一列加工处理之后,将事件封装成MotionEvent、keyEvent,然后传递到Java层InputEventReceiver.dispatchTouchEvent中,事件到达Java层之后,如何一步步传递到Activity?通过调用堆栈可以看到事件如何用从InputEventReceive传递到Activity;事件传递到Activity之后,由Activity传递给Window(PhoneWindow),最后由Window传递给顶级View;顶级View(DecorView)接收到事件后,就会按照事件分发机制去分发事件;本文主要是从Activity开始来分析事件分发机制;
    与事件分发过程相关的几个方法主要有:
    (1)dispatchTouchEvent:用来进行事件分发,如果事件能够传递给当前View,那么次方法一定会被调用,返回值受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响;
    (2)onInterceptTouchEvent:在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前VIew拦截了某个事件,那么在同一事件序列中,此方法不会再被调用,返回结果表示是否拦截当前事件;
    (3)onTouch:在dispatchTouchEvent方法中调用,其调用优先级高于onTouchEvent;
    (4)onTouchEvent:在dispatchTouchEvent方法中调用,用来处理点击事件,反馈结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再次接受事件;
    (5)onclick:在发生点击事件时,会调用该方法,表示有点击事件;onclick发生的前提是当前View可以点击,并且它收到down和up事件;
    (6)onLongClick:当长按事件发生时会回调该方法,手指或鼠标按下500ms之后就会发生长按事件,如果onLongClick返回true,就 不会在调用onClick方法,返回false,就会调用onClick;

    1.Activity对事件的分发过程

    Activity.dispatchTouchEvent代码如下:

        /**
         * 一个点击事件产生后,它的传递过程遵循如下顺序:
         * <h3>Activity——>Window——>View</h3>
         * 即事件总是先传递给Activity,由Activity的dispatchTouchEvent方法来进行事件派发,
         * 具体的工作是由Activity内部的Window来完成的。Activity传递给Window后,Window再
         * 传递给顶级View。顶级View接受事件后,就会按照事件分发机制去分发事件。
         * @param ev The touch screen event.
         *
         * @return boolean Return true if this event was consumed.
         */
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            // 传递给Window对象。getWindow()返回的是PhoneWindow
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }
    
    
    2.Window(PhoneWindow)对事件的分发过程:
        // This is the top-level view of the window, containing the window decor.
        //DecorView是顶级View,也叫根View
        private DecorView mDecor;
          @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            // PhoneWindow将事件直接传递给了DecorView
            return mDecor.superDispatchTouchEvent(event);
        }
    

    通过getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)这种方法可以获取Activity所设置的View,setContentView所设置的View是DecorView的子View,关于DecorView后续会专门进行讲解,目前事件传递已经传递到DecorView这里,及事件已经从Activity传递到View中。

    3.View事件分发机制

    当事件传递到View中时,首先进入View的dispatchTouchEvent

        /**
         * Pass the touch screen motion event down to the target view, or this
         * view if it is the target.
         *
         * 如果事件能够传递给当前View,那么此方法一定会被调用,
         * 返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
         * View是一个单独的元素,它没有子元素因此无法向下传递事件,
         * 所以它只能自己处理事件,所以View的onTouchEvent方法默认返回true。
         *
         * @param event The motion event to be dispatched.
         * @return True if the event was handled by the view, false otherwise.
         */
        public boolean dispatchTouchEvent(MotionEvent event) {
            if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //noinspection SimplifiableIfStatement
                ListenerInfo li = mListenerInfo;
                // 首先会判断当前View有没有设置OnTouchListener,
                // 如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent方法就不会被调用,
                // 可见OnTouchListener的优先级要高于onTouchEvent,这样做的好处是方便在外界处理点击事件。
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
            }
            return result;
        }
    
    
    4.View的onTouchEvent

    接下来是View的onTouchEvent:

        public boolean onTouchEvent(MotionEvent event) {
        
            // 对点击事件的具体处理。只要CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,因为返回了true
            // View的LONG_CLICKABLE属性默认为false,而CLICKABLE属性默认为true,不过具体的View的CLICKABLE又不一定,
            // 确切来说是可点击的View其CLICKABLE属性true,比如Button,不可点击的View的CLICKABLE为false,比如TextView。
            // 通过setClickable和setLongClickable可以设置这两个属性。
            // 另外setOnClickListener和setOnLongClickListener会自动将View的这两个属性设为true。
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
                        if (!clickable) {
                            removeTapCallback();
                            //移除长按监测
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        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 (prepressed) {
                                // The button is being released before we actually
                                // showed it as pressed.  Make it show the pressed
                                // state now (before scheduling the click) to ensure
                                // the user sees it.
                                setPressed(true, x, y);
                            }
    
                            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.
                                    // 当up事件发生时,会触发performClick方法
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
                                        // 如果View设置OnClickListener,那么performClick就会调用View的onClick方法。
                                        performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        // Walk up the hierarchy to determine if we're inside a scrolling container.
                        boolean isInScrollingContainer = isInScrollingContainer();
    
                        // For views inside a scrolling container, delay the pressed feedback for
                        // a short period in case this is a scroll.
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // Not inside a scrolling container, so show the feedback right away
                            //设置当前View的状态,以及更新View的drawable state,例如button按下、悬浮时显示不同的背景,即刷新背景
                            setPressed(true, x, y);
                            //开始长按监测,所以View的长按监测位于
                            checkForLongClick(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {
                            setPressed(false);
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();//当移出当前View时,会删除当前View的长按监测;
                            ...
                        }
                        break;
                }
                return true;
            }
            return false;
        }
    
        private void checkForLongClick(int delayOffset, float x, float y) {
            if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
                mHasPerformedLongPress = false;
    
                if (mPendingCheckForLongPress == null) {
                    mPendingCheckForLongPress = new CheckForLongPress();
                }
                mPendingCheckForLongPress.setAnchor(x, y);
                mPendingCheckForLongPress.rememberWindowAttachCount();
                mPendingCheckForLongPress.rememberPressedState();
                //ViewConfiguration.getLongPressTimeout()时间为500ms,所以当用户按下去500ms之后就会回OnLongClickListener.onLongClick方法;
                //如果500ms之内手指弹起,会发生ACTION_UP事件,此时会移出长按监测,这样就不会发生长按事件
                postDelayed(mPendingCheckForLongPress,
                        ViewConfiguration.getLongPressTimeout() - delayOffset);
            }
        }
        public boolean performClick() {
            if (li != null && li.mOnClickListener != null) {
                playSoundEffect(SoundEffectConstants.CLICK);
                // 如果设置了OnClickListener监听器,就回调onClick方法。
                li.mOnClickListener.onClick(this);
                result = true;
            } else {
                result = false;
            }
            return result;
        }
    

    通过以上代码可以看出:
    (1)当发生ACTION_DOWN的时候,开始长按监测,如果用户手指按下时间超过500ms才会发生长按事件,少于500ms就是onClick事件;可以得知,是否是长按事件是在Java层进行判断的,并且和ACTION_MOVE事件没有关系。
    (2)onLongClick方法返回true之后会将mHasPerformedLongPress属性置为true,在Up的时候就不会执行onClick,如果onlongClick返回false在Up事件到来时就会执行onClick
    (3)onClick是否会发生的前提是当前View是可点击的,并且它收到了Down和Up事件,
    (4)只要View的CLICKABLE和LONG_CLICKABLE有一个为TRUE,那么该View就会消耗掉该事件,可以看出以上无论是Down、Move还是Up事件,最终都return true;

    5.总结:

    (1)整个View的事件分发流程:
    View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
    在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。
    (2)onTouchEvent中的DOWN,MOVE,UP
    DOWN时:
    a、首先设置标志为PREPRESSED,设置mHasPerformedLongPress=false ;然后发出一个115ms后的mPendingCheckForTap;
    b、如果115ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-115ms的,检测长按任务消息;
    c、如果500ms内(从DOWN触发开始算),则会触发LongClickListener:
    此时如果LongClickListener不为null,则会执行回调,同时如果LongClickListener.onClick返回true,才把mHasPerformedLongPress设置为true;否则mHasPerformedLongPress依然为false;
    MOVE时:
    主要就是检测用户是否划出控件,如果划出了:
    115ms内,直接移除mPendingCheckForTap;
    115ms后,则将标志中的PRESSED去除,同时移除长按的检查:removeLongPressCallback();
    UP时:
    a、如果115ms内,触发UP,此时标志为PREPRESSED,则执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;
    b、如果是115ms-500ms间,即长按还未发生,则首先移除长按检测,执行onClick回调;
    c、如果是500ms以后,那么有两种情况:
    i.设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;
    ii.没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;
    d、最后执行setPressed刷新背景,然后将PRESSED标识去除;

    本篇博文完成了对View的事件分发机制的整个流程的说明,并且对源码进行了分析;下篇会讲解ViewGroup的事件分发机制;

    本文部分内容直接从其他文章直接Copy而来,感谢本文内容所参考文章的作者;

    相关文章

      网友评论

          本文标题:Android View 事件分发机制

          本文链接:https://www.haomeiwen.com/subject/rthlxqtx.html