Android View 事件分发机制

源码基于 sdk 30(Android 11.0/R)。

概述

Android 事件机制包含系统启动流程、输入管理(InputManager)、系统服务和 UI 的通信(WindowManagerService + ViewRootImpl + Window)、事件分发等一系列的环节。

Android 系统中将输入事件定义为 InputEvent,根据输入事件的类型又分为了 KeyEvent(键盘事件) 和 MotionEvent(屏幕触摸事件)。这些事件统一由系统输入管理器 InputManager 进行分发。

在系统启动的时候,SystemServer 会启动 WindowManagerService,WMS 在启动的时候通过 InputManager 来负责监控键盘消息。

InputManager 负责从硬件接收输入事件,并将事件通过 ViewRootImpl 分发给当前激活的窗口处理,进而分发给 View。

Window 和 InputManagerService 之间通过 InputChannel 来通信,底层通过 socket 进行通信。

Android Touch 事件的基础知识:

  1. 所有的 Touch 事件都封装到 MotionEvent 里面。
  2. 事件类型分为 ACTION_DOWN(手指按下屏幕的瞬间), ACTION_MOVE(手指在屏幕上移动),ACTION_UP(手指离开屏幕瞬间), ACTION_CANCEL(取消手势,一般由程序产生,不会由用户产生)等。每个事件传递过程都是由一个 ACTION_DOWN 开始,经过 n 个 ACTION_MOVE,最终以一个 ACTION_UP/ACTION_CANCEL 结束。
  3. 事件处理包括三种情况,分别为:
    1. 传递 View#dispatchTouchEvent(MotionEvent event)
    2. 拦截 ViewGroup#onInterceptTouchEvent(MotionEvent ev)
    3. 消费 View#OnTouchListener.onTouch(View v, MotionEvent event) 和 View#onTouchEvent(MotionEvent event)。
  4. 所有的触摸事件都会到达 Activity,Activity 内部通过 Window 连接着一个 View 的根布局 DecorView,事件会从 Activity 一层一层传递到最内层的 View。
View 事件分发

事件架构

Android 事件分发体系

InputEvent:输入事件抽象

/**
 * Common base class for input events.
 */
public abstract class InputEvent implements Parcelable {}

/**
 * Object used to report key and button events.
 */
public class KeyEvent extends InputEvent implements Parcelable {}

/**
 * Object used to report movement (mouse, pen, finger, trackball) events.
 */
public final class MotionEvent extends InputEvent implements Parcelable {}

KeyEvent 对应了键盘的输入事件;MotionEvent 就是手势事件,鼠标、笔、手指、轨迹球等相关输入设备的事件都属于 MotionEvent。

InputManager:输入管理器

InputEvent 统一由 InputManager 进行分发,负责与硬件通信并接收输入事件。

system_server 进程启动时会创建 InputManagerService 服务。

// SystemServer.java
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
    InputManagerService inputManager = new InputManagerService(context);
    // 实例化 InputManagerCallback
    WindowManagerService wm = WindowManagerService.main(context, inputManager, ...);
    inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());
    inputManager.start();
}
public class InputManagerService extends IInputManager.Stub {
    public InputManagerService(Context context) {
        this.mContext = context;
        this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());
        mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
    }

    private static native long nativeInit(InputManagerService service,
            Context context, MessageQueue messageQueue);
}

system_server 进程启动时同时会启动 WMS,WMS 在启动的时候就会通过 IMS 启动 InputManager 来监控键盘消息。

InputEventReceiver:事件接收器

App 端与服务端建立了双向通信之后,InputManager 就能够将产生的输入事件从底层硬件分发过来,Android 提供了 InputEventReceiver 类,以接收分发这些消息:

/**
 * Provides a low-level mechanism for an application to receive input events.
 * @hide
 */
public abstract class InputEventReceiver {
    public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        mInputChannel = inputChannel;
        mMessageQueue = looper.getQueue();
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
            inputChannel, mMessageQueue);
    }

    // Called from native code.
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        onInputEvent(event);
    }

    public void onInputEvent(InputEvent event) {
        finishInputEvent(event, false);
    }

    public final void finishInputEvent(InputEvent event, boolean handled) {
        nativeFinishInputEvent(mReceiverPtr, seq, handled);
        event.recycleIfNeededAfterDispatch();
    }
}
// ViewRootImpl.java
final class WindowInputEventReceiver extends InputEventReceiver {
    @Override
    public void onInputEvent(InputEvent event) {
        // 将输入事件加入队列,开始事件分发
        enqueueInputEvent(event, this, 0, true);
    }
}

InputChannel

Window 和 IMS 之间通过 InputChannel 通信。InputChannel 是一个 pipe,底层通过 socket 进行通信。在 ViewRootImpl.setView() 过程中注册 InputChannel。

/**
 * An input channel specifies the file descriptors used to send input events to a window in another process.
 * It is Parcelable so that it can be sent to the process that is to receive events.
 * Only one thread should be reading from an InputChannel at a time.
 **/
public final class InputChannel implements Parcelable {}
// ViewRootImpl.java
public void setView(){
    requestLayout();
    InputChannel inputChannel = new InputChannel();
    // 通过 Binder 在 system_server 进程中完成 InputChannel 的注册
    res = mWindowSession.addToDisplayAsUser(mWindow, inputChannel, ...);
    if (mInputQueueCallback != null) {
        mInputQueue = new InputQueue();
        mInputQueueCallback.onInputQueueCreated(mInputQueue);
    }
    mInputEventReceiver = new WindowInputEventReceiver(inputChannel, Looper.myLooper());

    // 设置 InputStage
    mSyntheticInputStage = new SyntheticInputStage();
    InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
    // nativePostImeStage earlyPostImeStage imeStage viewPreImeStage nativePreImeStage
}

InputStage

public class ViewRootImpl{
    /**
     * Base class for implementing a stage in the chain of responsibility
     * for processing input events.
     */
    abstract class InputStage {
        protected int onProcess(QueuedInputEvent q) {
            return FORWARD;
        }
    }

    /**
    * Delivers post-ime input events to the view hierarchy.
    */
    final class ViewPostImeInputStage extends InputStage {
        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);
            } else {
                final int source = q.mEvent.getSource();
                if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                    return processPointerEvent(q);
                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                    return processTrackballEvent(q);
                } else {
                    return processGenericMotionEvent(q);
                }
            }
        }
    }

    private int processPointerEvent(QueuedInputEvent q) {
        final MotionEvent event = (MotionEvent)q.mEvent;
        // mView 是 DecorView 类型
        boolean handled = mView.dispatchPointerEvent(event);
        return handled ? FINISH_HANDLED : FORWARD;
    }
}

public class DecorView {
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
}
  1. ViewPostImeInputStage 是 Touch 事件传递的起点;
  2. Activity 实现了 Window.Callback 接口,DecorView 会把事件分发到 Activity#dispatchTouchEvent(ev) 方法。

事件传递流程

Android 事件传递机制是先分发再处理,先由外部的 View 接收,然后依次传递给其内层的 View,再从最内层 View 反向依次向外层传递。

  1. 事件从 Activity#dispatchTouchEvent() 开始传递,再传递给 DecorView;
  2. DecorView 中先执行 dispatchTouchEvent() 方法,在其内部会先调用 onInterceptTouchEvent() 询问是否拦截事件,若拦截则执行 onTouchEvent() 方法处理这个事件;
  3. 若不拦截,则执行子元素的 dispatchTouchEvent() 方法,进入向下分发的传递,直到事件被处理;
  4. 如果事件从外向内传递过程中一直没有被拦截,且最底层子 View 没有消费事件,这时父 View 可以进行消费,如果还是没有被消费的话,最后会到 Activity#onTouchEvent() 函数。
  5. 如果 View 没有对 ACTION_DOWN 进行消费,之后的其他类型的事件也不会传递过来,也就是说 ACTION_DOWN 必须返回 true,之后的事件才会传递进来。
  6. 事件处理优先级是
    OnTouhListener#onTouch(View v, MotionEvent event) -> onTouchEvent() -> OnClickListener#onClick(View v)

三个方法的关系如下:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consum = false;
    if(onInterceptTouchEvent(event)){
        consum = onTouchEvent(event);
    } else {
        consum = child.dispatchTouchEvent(event);
    }
    return consum;
}
思维导图

源码分析

Activity 的事件处理

public class Activity {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 调用 mDecor.superDispatchTouchEvent(event);
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
}

public class PhoneWindow extends Window {
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
}

public class Window{
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        final boolean isOutside =
                event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
                || event.getAction() == MotionEvent.ACTION_OUTSIDE;
        if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
            return true;
        }
        return false;
    }
}
  1. Activity 接收到 ACTION_DOWN 事件后,调用 onUserInteraction() 方法,表示和用户进行了交互;
  2. Activity 接收到事件后经 PhoneWindow 传递给 DecorView;
  3. 传递给 DecorView 后如果此事件没有被消费,则事件交给 Activity#onTouchEvent() 处理。

ViewGroup 的事件处理

分发事件:

// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    // ACTION_DOWN 说明是新事件,重置状态
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // 清空 TouchTarget 链表
        cancelAndClearTouchTargets(ev);
        // 重置事件状态
        resetTouchState();
    }

    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 判断是否拦截事件
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            // 如果拦截,事件由当前 ViewGroup 处理
            intercepted = onInterceptTouchEvent(ev);
            // 防止onInterceptTouchEvent()的时候改变Action
            ev.setAction(action);
        } else {
            intercepted = false;
        }
    } else {
        //没有 TouchTarget 而且非 ACTION_DOWN 类型的事件,由 ViewGroup 处理
        intercepted = true;
    }

    // 如果给子View发送cancel事件后mFirstTouchTarget会变null,
    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    // 事件没有取消也没有拦截,向子 View 传递
    if (!canceled && !intercepted) {
        if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            final int childrenCount = mChildrenCount;
            // 遍历查找第一个消费这个事件的子View,并设置为 Target
            if (newTouchTarget == null && childrenCount != 0) {
                final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                final View[] children = mChildren;
                // 按 View 添加顺序倒序查找
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final View child = getAndVerifyPreorderedView(...);
                    // 判断事件坐标是否在 View 内
                    if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }
                    // 更新 target
                    newTouchTarget = getTouchTarget(child);
                    // 分发事件到点击的 View
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // View 处理该事件,添加到 TouchTarget 调用链,赋值 mFirstTouchTarget
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
                }
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    // ...
                }
            }
        }

        if (mFirstTouchTarget == null) {
            // 没有找到处理此事件的子 View。比如:点击按钮之外
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // 给子 View 发送 cancel 事件
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
    }
    return handled;
}

// 第二个参数cancel包含两种含义,一种是外部收到了取消事件,另一种是事件被拦截
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    // cancel 事件处理
    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;
    }

    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    return handled;
}

应用了树的深度优先搜索算法(Depth-First-Search,简称 DFS 算法),每个 ViewGroup 都持有一个 mFirstTouchTarget, 当接收到 ACTION_DOWN 时,通过递归遍历找到 View 树中真正对事件进行消费的 Child,并保存在 mFirstTouchTarget 属性中,依此类推组成一个完整的分发链。在这之后,当接收到同一事件序列的其它事件如 ACTION_MOVE、ACTION_UP 时,则会跳过递归流程,将事件直接分发给下一级的 Child。

ViewGroup 分发事件的主要的任务是找一个 Target,并且用这个 Target 处理事件,主要逻辑如下 :

  1. 在 ACTION_DOWN 事件并且当前 ViewGroup 不拦截时会查找 TouchTarget。按 View 添加顺序倒序遍历子 View,判断子 View 的 dispatchTouchEvent() 是否为 true。查找 TouchTarget 的过程只有在 ACTION_DOWN 事件中才会触发。
  2. 如果子 View 的 dispatchTouchEvent() 返回 true,则这个子 View 就是当前 ViewGroup 的 Target。
  3. 如果 TouchTarget 不存在,则调用当前 ViewGroup#onTouchEvent() 方法,仍不消费,会向上调传递直到 Activity#onTouchEvent()。
  4. 子 View 可以调用父 View 的 requestDisallowInterceptTouchEvent(true) 请求父 View 不拦截此事件,交给 子 View 处理。只限于一个 Touch 过程(Down->Up/Cancel)。

为什么倒序查找 TouchTarget?
如果按添加顺序遍历,当 View 重叠时(FrameLayout),先添加的 View 总是能消费事件,而后添加的 View 不可能获取到事件。

// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = ...;
        final View child = ...;
        // 重叠的子 View 都包含点击区域
        if (!child.canReceivePointerEvents()
                || !isTransformedTouchPointInView(x, y, child, null)) {
            continue;
        }
        // 非容器 View 默认都会消费事件,从而跳出循环
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
    }
}

拦截事件:

// 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;
}
  1. 默认返回 false,不拦截事件。
  2. ViewGroup 子类可以重新该方法,决定是否拦截事件。
  3. 子 View 通过 parentView.requestDisallowInterceptTouchEvent(true) 强制父 View 不拦截此事件。

View 的事件处理

// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.isTargetAccessibilityFocus()) {
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        event.setTargetAccessibilityFocus(false);
    }
    boolean result = false;

    // 停止嵌套滑动
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        stopNestedScroll();
    }

    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    // 如果 View 可用并设置了 OnTouchListener,如果回调方法 onTouch() 返回 true 则消费事件
    // 并且不会调用 onTouchEvent() 方法
    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;
    }
    return result;
}
  1. 安全监测。如果 View#setFilterTouchesWhenObscured(true) 开启了安全检测,当 View 所在的 Window 被覆盖时不处理 Touch 事件;
  2. 停止嵌套滑动(5.0 以后添加);
  3. View 可用时才调用 OnTouchListener#onTouch() 方法;
  4. 不管 View 是否可用,只要 OnTouchListener 不消费事件,就会让 onTouchEvent() 处理;
  5. View 不可用时,不处理 OnTouchListener#onTouchEvent(),直接调用 View#onTouchEvent() 方法。
// View.java
public boolean onTouchEvent(MotionEvent event) {
    // 判断是否可以点击
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

    // View 不可用时可以消费事件,只是没有响应
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        return clickable;
    }

    // 事件代理
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    // 不管是否可用,只有是可点击的就消费事件
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                // 只有在 press 的情况下,才 click,longClick
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // 如果我们在当前View还没获取焦点,并且能在touch下foucus
                    // 那么第一次点击只会将这个View的状态改成focus,而不会触发click
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    // 设置 Pressed 状态,更新 View 背景
                    if (prepressed) {
                        setPressed(true, x, y);
                    }

                    // 检查 longClick 是否已执行
                    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();
                            }
                        }
                    }

                    removeTapCallback();
                }
                break;

            case MotionEvent.ACTION_DOWN:

                if (!clickable) {
                    // 400 ms
                    checkForLongClick(ViewConfiguration.getLongPressTimeout(),...);
                    break;
                }

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                boolean isInScrollingContainer = isInScrollingContainer();

                if (isInScrollingContainer) {
                    // ...
                } else {
                    setPressed(true, x, y);k
                    checkForLongClick(ViewConfiguration.getLongPressTimeout(),...);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                if (clickable) {
                    setPressed(false);
                }
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                final int motionClassification = event.getClassification();
                final boolean ambiguousGesture = ...;
                int touchSlop = mTouchSlop;
                if (ambiguousGesture && hasPendingLongPressCallback()) {
                    if (!pointInView(x, y, touchSlop)) {
                        removeLongPressCallback();
                        checkForLongClick(...);
                    }
                }

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, touchSlop)) {
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                }

                final boolean deepPress = ;
                if (deepPress && hasPendingLongPressCallback()) {
                    // process the long click action immediately
                    removeLongPressCallback();
                    checkForLongClick(...);
                }

                break;
        }

        return true;
    }

    return false;
}
  1. 如果 View 不可用但是可点击,会直接消费事件,只是不做其他任何操作;
  2. 如果 View 可用并设置了 TouchDelegate,则事件交给 TouchDelegate 处理;
  3. 不管 View 是否可用,只要 View 可点击,默认都会消费事件;
  4. 处理 focus,press,click,longclick 等。

View 滑动冲突

问题

  1. View.GONE 后仍消费事件;
    可能该 View 执行了动画,要移除。
    android View with View.GONE still receives onTouch and onClick
    View with visibility View.GONE still generates touch events

总结

  1. 一个点击事件产生后,它的传递过程如下:
    Activity->Window->DecorView->View。如果一个 View 的 onTouchEvent 方法返回 false,那么将会交给父容器的 onTouchEvent 方法进行处理,逐级往上,如果所有的 View 都不处理该事件,则交由 Activity 的 onTouchEvent 进行处理。
  2. ViewGroup 默认不拦截任何事件,可以通过重写 onInterceptTouchEvent() 方法拦截事件,执行自己对应的 onTouchEvent() 方法。
  3. 子 View 可以通过调用父 View 的 requestDisallowInterceptTouchEvent(true) 阻止 ViewGroup 对事件进行拦截;
  4. 如果 ViewGroup 找到了能够处理该事件的 View(TouchTarget),则直接交给子 View 处理,自己的 onTouchEvent() 方法不会执行;
  5. 如果某一个 View 不消耗 ACTION_DOWN 事件,则同一事件序列中不会再交给该 View 处理;
  6. 非容器的 View,一旦接收到事件默认都会消耗事件,除非它是不可点击的(clickable 和 longClickable 都为 false),那么就会由父容器的 onTouchEvent() 处理;
  7. View 的 visible 属性对事件传递没有影响;
  8. 点击事件分发过程是 Activity#dispatchTouchEvent() -> DecorView#dispatchTouchEvent() -> View#OnTouchListener.onTouch() -> View#onTouchEvent() -> View#OnClickListener.onClick()
  9. 如果当前 View 是可点击的,并且它收到了 down 和 up 事件,则它的 click 事件就会触发;对于 onLongClick,则只要当前 View 接收到 down 事件超过了系统默认的时间;
  10. 当某个子 View 消费事件时,会中止 Down 事件的分发,同时在 ViewGroup 中记录该子 View。接下去的 Move 和 Up 事件将由该子 View 直接进行处理。
  11. 当子 View 都不消费 Down 事件时,将调用 ViewGroup#onTouchEvent() 方法。触发的方式是调用 super.dispatchTouchEvent() 函数,即父类 View#dispatchTouchEvent 方法。如果所有子 View 都不处理,则调用 Acitivity#onTouchEvent() 方法。
  12. 如果 View 没有对 ACTION_DOWN 进行消费,那后续事件不会传递过来。如果 View 消费了 ACTION_DOWN,在不拦截的情况下,后续事件会直接传递给这个 View;
  13. 接收了 ACTION_DOWN 事件的函数不一定能收到后续的 ACTION_MOVE 和 ACTION_UP 事件。
  14. 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。
  15. 子 View 重叠并都是可点击时,最后添加的 View,即离用户最近的 View 最先消费事件。

参考

[1] Android 事件分发机制的设计与实现
[2] Android 事件拦截机制的设计与实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容