事件分发

177 阅读7分钟

Touch事件如何从屏幕到达App

硬件与内核部分

触摸屏幕/按键操作时,首先触发的是硬件驱动,驱动收到事件后,把相应的事件写入到输入设备节点

SystemServer

系统启动时,在SystemServer进程会启动一系列系统服务,比如AMS、WMS和管理事件输入的InputManagerService.这个服务负责与硬件通信,接受屏幕输入事件;在这个服务内部,会启动一个读线程,也就是InputReader,它会从dev/input目录下拿到任务,给InputDispatcher,然后进行统一的事件分发调度.

跨进程通信传给App

系统进程拿到输入事件了,但还需要传递给我们的App进程,我们App中的Window与InputManagerService之间的通信使用的就是InputChannel.

在Activity的启动中,会调用ViewRootImpl.serView()方法,在这里会注册InputChannel.

传递到对应的页面

事件会传到ViewRootImpl中

责任链分发

在ViewRootImpl的doProcessInputEvents中,涉及到责任链分发

void doProcessInputEvents() { ... // Deliver all pending input events in the queue. while (mPendingInputEventHead != null) { QueuedInputEvent q = mPendingInputEventHead; mPendingInputEventHead = q.mNext; deliverInputEvent(q); } .... }

private void deliverInputEvent(QueuedInputEvent q) {
    InputStage stage;
    ....
    //stage赋值操作
    ....
    if (stage != null) {
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}
​
abstract class InputStage {
    private final InputStage mNext;
​
    public InputStage(InputStage next) {
        mNext = next;
    }
​
    public final void deliver(QueuedInputEvent q) {
        if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
            forward(q);
        } else if (shouldDropInputEvent(q)) {
            finish(q, false);
        } else {
            traceEvent(q, Trace.TRACE_TAG_VIEW);
            final int result;
            try {
                result = onProcess(q);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            apply(q, result);
        }
    }
}

QueuedInputEvent是一种输入事件,链表结构,交给InputStage处理.

InputStage就是处理输入的责任链,调用deliver时遍历责任链传递事件.

事件分发完以后会调用finishInputEvent,告知系统进程移除此事件完成消费.

组装责任链

组装的过程发生在ViewRootImpl.setView()中,触摸事件由ViewPostImeInputStage处理.

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);
            } 
        }
    }
​
private int processPointerEvent(QueuedInputEvent q) {
        final MotionEvent event = (MotionEvent)q.mEvent;
        boolean handled = mView.dispatchPointerEvent(event)
        return handled ? FINISH_HANDLED : FORWARD;
    }
    
    //View.java
    public final boolean dispatchPointerEvent(MotionEvent event) {
            if (event.isTouchEvent()) {
                return dispatchTouchEvent(event);
            } else {
                return dispatchGenericMotionEvent(event);
        }
    }

经过回调,首先收到消息的是DecorView(),通过ViewRootImpl传递,交给mView,也就是DecorView的dispatchTouchEvent

DecorView、Activity、Window

//DecorView.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //cb其实就是对应的Activity/Dialog
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
​
​
//Activity.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
​
//PhoneWindow.java
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
​
//DecorView.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }    
​

可以看出来事件经过了DecorView->Activity->Window->DecorView的流程.

事件流转

ViewRootImpl只持有DecorView,所以只能先把事件交给DecorView;交给了Activity后,因为Activity不持有DecorView,只能先交给Window,再交给DecorView.

Touch事件到达页面内部

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
​
        final boolean intercepted;
        //只有ActionDown或者mFirstTouchTarget为空时才会判断是否拦截
        if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
            } 
        } 
​
        
        if (!canceled && !intercepted) {
            //事件传递给子view
            ....
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
              ...
              //如果子View消耗了则给mFirstTouchTarget赋值
              newTouchTarget = addTouchTarget(child, idBitsToAssign);
              ...
            }
        }
​
        //mFirstTouchTarget不为空时会调用dispatchTransformendTouchEvent
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        }
    }
​
    private boolean dispatchTransformedTouchEvent(View child) {
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else { 
            handled = child.dispatchTouchEvent(event);
        }
    }
​
  1. 只有在ActionDown或者mFirstTouchTarge不为空时才判断是否拦截.

    mFirstTouchTarget != null时,意味着已经有一个子视图正在处理当前的触摸事件序列。在这种情况下,再次判断是否拦截触摸事件的原因如下:

    1. 保持一致性:在处理一系列相关的触摸事件时(比如一次拖动操作),系统需要保证这些事件都由同一个视图处理。即使父视图想要拦截后续的触摸事件,为了保证操作的连续性,仍然需要检查和决定是否拦截。
    2. 动态调整:在某些情况下,父视图可能希望根据当前的触摸事件动态调整拦截逻辑。例如,在拖动操作中,父视图可能会在某些条件下决定开始拦截事件(例如检测到手势的变化)。
    3. 复杂交互:在一些复杂的交互场景中,触摸事件的处理逻辑可能需要更加灵活。例如,某些自定义控件可能需要在特定条件下重新评估触摸事件的处理策略,而不仅仅是单纯地传递给mFirstTouchTarget

    总结来说,当mFirstTouchTarget != null时判断是否拦截触摸事件,是为了在处理触摸事件序列时保持一致性,并提供灵活性以适应复杂的交互需求。

  2. mFirstTouchTarget为空时表示没有子View消费事件

  3. onInterceptTouchEvent方法控制ViewGroup是否拦截

ViewGroup拦截后

  1. ViewGroup拦截以后,事件不会再下发给子View.
  2. 接下来mFirstTouchTarget为null,则会调用dispatchTransformedTouchEvent(),然后调用super.dispatchTouchEvent(),最终走到Viewgroup.onTouchEvent
  3. mFirstTouchTarget == null表示1. ViewGroup拦截,2.子View没有处理事件,最终都会走到ViewGroup.onTouchEvent()

伪代码

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean isConsume = false;
    if (isViewGroup) {
        if (onInterceptTouchEvent(event)) {
            isConsume = super.dispatchTouchEvent(event);
        } 
    } 
    return isConsume;
}

ViewGroup不拦截

如果ViewGroup不拦截会传递到子View,则会把事件传递给子View

  1. 遍历子View,判断事件坐标,是否在子View范围内(子View是否能接触到)
  2. 传递事件给子View处理,调用dispatchTransformedTouchEvent()方法
  3. dispatchTransformedTouchEvent做了两个事情,如果child为空,则交给父类分发,如果child不为null,则事件分发到child
  4. mFirstTouchTarget是单链表结构,记录了消费链,

子View是否拦截

public boolean dispatchTouchEvent(MotionEvent event) {    
  if (onFilterTouchEventForSecurity(event)) {
        
        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;
}

子View的dispatchTouchEvent逻辑比较简单

  1. 如果设置了setOnTouchListener并返回了true(表示消费了这个事件),那么子View的onTouchEvent就不会执行
  2. 否则就执行onTouchEvent,比如onClickListener就是在onTouchEvent()里触发的

子View消费事件

如果子View消费事件,也就是dispatchTouchEvent方法返回true,最后回到Activity的dispatchTouchEvent,返回true.

到此事件消费到此结束

子View不消费事件

子View不拦截事件,那么mFirstTouchTarget就为null,退出循环后调用dispatchTransformedTouchEvent()方法,然后就调到了super.dispatchTouchEvent,那么接下来ViewGroup就跟子View的逻辑一样了,默认执行onTouchEvent,如果设置了setOnTouchLister则执行onTouch

ViewGroup和子View都不拦截

如果ViewGroup与子View都不拦截,即mFirstTouchTarget == null,dispatchTouchEvent也返回false,那么会执行Activity源码

后续事件

后续move事件不会再走对View的循环判断的方法,因为已经找到了目标View,就能直接通过mFirstTouchTarget分发

如果某个View开始处理拦截事件,后续事件序列只能由它处理.

小结

  1. 事件分发的本质是一个递归方法,通过往下传递,调用了dispatchTouchEvent方法(分发)
  2. 分发过程中ViewGroup通过onInterceptTouchEvent判断是否拦截
  3. View默认通过onTouchEvent处理事件
  4. 如果底层不消费,则向上执行父元素的onTouchEvent方法
  5. 如果所有View的onTouchEvent方法都返回false,则会执行Activity的onTouchEvent方法

滑动冲突解决

如果一个页面同时有横向与竖向两个方向的滑动,要根据实际情况在Action_Move时对事件进行判断和拦截.

常用方法:外部拦截、内部拦截

外部拦截

外部拦截在onInterceptTouchEvent进行

//外部拦截法:父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;
​
}
​
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
          //1.判断拦截
            intercepted = onInterceptTouchEvent(ev);
        } 
​
        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
          //4.后续事件就直接交给ViewGroup处理了
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } else {
            while (target != null) {
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                  //2.cancelChild为ture
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                       if (predecessor == null) {
                           //3.mFirstTouchTarget被置为null
                           mFirstTouchTarget = next;
                       } else {
                           predecessor.next = next;
                       }
                       target.recycle();
                       target = next;
                       continue;
                    }
                }
            }
        }
    }
​
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        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;
        }
    }
​
  1. 通过onInterceptTouchEvent拦截事件
  1. intercepted为true时,cancelChild也为true(final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;)
  2. cancelChild为空后将mFirstTouchTarget`置为空
  3. mFirstTouchTarget为空后后续事件都由ViewGroup处理

内部拦截

//父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:
            getParent().requestDisallowInterceptTouchEvent(!parentCanIntercept);
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return super.dispatchTouchEvent(event);
}

内部拦截法是将主动权交给子View,如果子View需要事件就直接消耗,否则交给父容器处理 内部拦截法主要通过requestDisallowInterceptTouchEvent方法控制

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
​
    final boolean intercepted;
    //只有ActionDown或者mFirstTouchTarget为空时才会判断是否拦截
    if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
        } 
    } 
}

1.子View通过requestDisallowInterceptTouchEvent控制mGroupFlags的值,从而控制disallowIntercept的值 2.disallowIntercepttrue时就不会走到onInterceptTouchEvent,外部也就无法拦截了,当需要外部处理时,将disallowIntercept置为false即可