Android触摸事件(上)——事件的由来

简介: 从接触Android开发以来,貌似Android的事件就一直伴随着我。从一开始的setOnclickListener到后来的setOnTouchListener以及各种手势的事件,关于Android的事件传递机制,我觉得很多人都看了不止一遍了。

从接触Android开发以来,貌似Android的事件就一直伴随着我。从一开始的setOnclickListener到后来的setOnTouchListener以及各种手势的事件,关于Android的事件传递机制,我觉得很多人都看了不止一遍了。借着这次大总结,我觉得有必要对这部分进行一下总结了。
之前写了关于View的测量、布局和绘制的过程,在绘制完成后,界面的元素就已经展示出来了。光有花里胡哨的页面对于一个完整的App是不够的,因为我们需要的不仅仅是页面展示,还包括了页面交互,不是还有个职位叫UE(交互设计师)嘛。
说起交互,那么可以说的就很多了。比如我点击这个按钮会怎样?长按又如何?这里面有很多东西需要深入探究,我们需要做到知其然并且知其所以然,这里我就假装一次小白(咳咳,现在是大白。。)一点点分析Android的触摸事件。

作为小白,我只知道在View是所有子View的爸爸(这不废话嘛),所以我肯定知道触摸事件在View中一定有实现。翻开源码找一找,果不其然:

public boolean dispatchTouchEvent(MotionEvent event) {
    ......
    // 具体实现暂时不看
}

找到关于触摸事件的分发了,那么问题来了:这个事件分发是在何时调用的呢?我们知道View是一个基类,如果手机接收到触摸事件时肯定直接或间接调用的是基类的dispatchTouchEvent方法,所以需要查找下有那个类调用了dispatchTouchEvent方法。不查不知道,一查吓一跳,N多个类都有调用这个dispatchTouchEvent方法。定睛一看,原来都是View的子类(虚惊一场)。既然外面没有,那就找View当前有没有方法调用吧。别说,还真有一个——dispatchPointerEvent:

// 代码简单,我喜欢。。
public final boolean dispatchPointerEvent(MotionEvent event) {
    // 判断是不是触摸事件
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

这个方法代码很少,我这种小白也是能够理解的:通过判断当前的事件是不是触摸事件,如果是的话则需要将触摸事件分发。好了,现在知道了dispatchPointerEvent方法调用了dispatchTouchEvent方法。所以现在我们需要找到谁调用了dispatchPointerEvent方法

img_b404fc900b46ed33f2d075c39913624d.png
搜索结果

可以从搜索结果中看到,这里就 ViewRootImpl调用了这个方法。从这里我就可以知道, 触摸事件的分发肯定是通过ViewRootImpl进行分发的。那么,先去看下这个方法:

private int processPointerEvent(QueuedInputEvent q) {
    // 获取输入事件,并将其转换成MotionEvent
    final MotionEvent event = (MotionEvent)q.mEvent;

    mAttachInfo.mUnbufferedDispatchRequested = false;
    final View eventTarget =
            (event.isFromSource(InputDevice.SOURCE_MOUSE) && mCapturingView != null) ?
                    mCapturingView : mView;
    mAttachInfo.mHandlingPointerEvent = true;
    // 调用dispatchPointerEvent去处理这次事件
    boolean handled = eventTarget.dispatchPointerEvent(event);
    maybeUpdatePointerIcon(event);
    mAttachInfo.mHandlingPointerEvent = false;
    if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
        mUnbufferedInputDispatch = true;
        if (mConsumeBatchedInputScheduled) {
            scheduleConsumeBatchedInputImmediately();
        }
    }
        // 如果被处理返回FINISH_HANDLED ,否则返回转发状态
    return handled ? FINISH_HANDLED : FORWARD;
}

可以看到这个方法是ViewRootImpl的内部类ViewPostImeInputStage的方法,并且这个方法被onProcess调用,接下来需要看下ViewPostImeInputStage这个类到底有什么作用:

img_8510823cfb3107ad2b4e33df49922fde.png
ViewPostImeInputStage类

看注释的意思是: 将后期输入事件传递给视图层次结构。那么就可以理解为输入事件的传递了。那么,先看下这个类是何时创建并在何处调用了 onProcess方法的。
img_ee440a3ba77d751eb651d91c97797249.png
创建

可以看到,这个创建过程是在 ViewRootImpl的setView方法中,在之前写的 Activity显示到Window的过程中讲到setView是在Activity显示的时候调用的方法,通过 WindowManagerGlobal的addView方法调用了ViewRootImpl的setView方法。在这个方法里面创建了 ViewPostImeInputStage对象,在这里也可以看到一个很有趣的现象: 前面创建好的对象又当作参数传入了下一个创建的对象,所以这边需要看下这里面到底有什么玄机。这里我们看下这些类的父类 InputStage

abstract class InputStage {
    private final InputStage mNext;

    protected static final int FORWARD = 0;
    protected static final int FINISH_HANDLED = 1;
    protected static final int FINISH_NOT_HANDLED = 2;
    
    // 构造方法里面传入了InputStage作为下一个将要转发的InputStage
    /**
     * Creates an input stage.
     * @param next The next stage to which events should be forwarded.
     */
    public InputStage(InputStage next) {
        mNext = next;
    }

    /**
     * Delivers an event to be processed.
     * 提供要被处理的事件。
     */
    public final void deliver(QueuedInputEvent q) {
        // 如果事件的flag是完成了,则转发事件
        if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
            forward(q);
        //如果需要丢弃这个事件
        } else if (shouldDropInputEvent(q)) {
            // 完成,但是传入的是false
            finish(q, false);
        } else {
            // 应用处理,里面调用了onProcess即在过程中执行,当初次进入时应该会进入这个方法
            apply(q, onProcess(q));
        }
    }

    /**
     * Marks the the input event as finished then forwards it to the next stage.
     */
    protected void finish(QueuedInputEvent q, boolean handled) {
        q.mFlags |= QueuedInputEvent.FLAG_FINISHED;
        if (handled) {
            q.mFlags |= QueuedInputEvent.FLAG_FINISHED_HANDLED;
        }
        forward(q);
    }

    /**
     * Forwards the event to the next stage.
     * 转发事件到下一个阶段
     */
    protected void forward(QueuedInputEvent q) {
        onDeliverToNext(q);
    }

    /**
     * Applies a result code from {@link #onProcess} to the specified event.
     */
    protected void apply(QueuedInputEvent q, int result) {
        if (result == FORWARD) {
            forward(q);
        } else if (result == FINISH_HANDLED) {
            finish(q, true);
        } else if (result == FINISH_NOT_HANDLED) {
            finish(q, false);
        } else {
            throw new IllegalArgumentException("Invalid result: " + result);
        }
    }

    /**
     * Called when an event is ready to be processed.
     * 返回这个处理事件的代码 
     * 如FORWARD(转发)FINISH_HANDLED(完成,已经处理)FINISH_NOT_HANDLED(完成,没有处理)
     * 这个方法具体实现应当由其子类实现
     * @return A result code indicating how the event was handled.
     */
    protected int onProcess(QueuedInputEvent q) {
        return FORWARD;
    }

    /**
     * Called when an event is being delivered to the next stage.
     * 传递到下一个InputState
     */
    protected void onDeliverToNext(QueuedInputEvent q) {
        if (DEBUG_INPUT_STAGES) {
            Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q);
        }
        // 如果下一个不为空,则调用deliver去处理;否则,完成这次输入事件的处理
        if (mNext != null) {
            mNext.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

    protected boolean shouldDropInputEvent(QueuedInputEvent q) {
        // 如果当前传入的View为空或者没有被添加,此时应该抛弃调输入事件
        if (mView == null || !mAdded) {
            Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);
            return true;
        } else if ((!mAttachInfo.mHasWindowFocus
                && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) || mStopped
                || (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON))
                || (mPausedForTransition && !isBack(q.mEvent))) {
            // This is a focus event and the window doesn't currently have input focus or
            // has stopped. This could be an event that came back from the previous stage
            // but the window has lost focus or stopped in the meantime.
            // 这是一个焦点事件,窗口当前没有输入焦点或已经停止。 
            // 这可能是一个事件,从前一个阶段回来,但窗口失去了重点或停止在此期间。
            if (isTerminalInputEvent(q.mEvent)) {
                // Don't drop terminal input events, however mark them as canceled.
                // 不要丢弃终端输入事件,但将它们标记为已取消。
                q.mEvent.cancel();
                Slog.w(mTag, "Cancelling event due to no window focus: " + q.mEvent);
                return false;
            }

            // Drop non-terminal input events.
            Slog.w(mTag, "Dropping event due to no window focus: " + q.mEvent);
            return true;
        }
        return false;
    }

    void dump(String prefix, PrintWriter writer) {
        if (mNext != null) {
            mNext.dump(prefix, writer);
        }
    }

    private boolean isBack(InputEvent event) {
        if (event instanceof KeyEvent) {
            return ((KeyEvent) event).getKeyCode() == KeyEvent.KEYCODE_BACK;
        } else {
            return false;
        }
    }
}

从上面的代码可以看出,InputStage是单向链表结构,从上到下依次处理,并将根据处理结果赋值给事件然后转发到下一个状态处理。当全部处理完成后,会调用finishInputEvent这个方法去完成这次输入事件的处理。现在可以解释刚才的那个现象了:我们传入的Stage作为接收上层Stage的事件转发,并且根据状态去处理。上面的注释挺齐全的,这里需要注意:

  1. deliver方法传递事件是会根据QueuedInputEvent的mFlags 属性来判断是forward还是finish或者apply,这个属性除了在一开始生成的时候赋值,其他修改的地方就是在finish方法中
  2. onProcess在InputStage中只是返回了一个FORWARD状态码,其子类会根据自身处理返回相应的状态码
    看完InputStage后,我们需要看下ViewPostImeInputStage的onProcess方法
@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);
        }
    }
}

关于processPointerEvent我们在上面已经写过,这里不赘述。
好了,现在知道输入事件是在各种InputStage中处理的,那么到底是哪里调用了InputStage的哪个方法呢?还是跟着代码看一看吧:

img_967f2685613098f729f33fcd48199fdc.png
调用位置

看下代码:

private void deliverInputEvent(QueuedInputEvent q) {
    Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
            q.mEvent.getSequenceNumber());
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
    }

    InputStage stage;
    // 如果true,则使用最后一个InputStage
    if (q.shouldSendToSynthesizer()) {
        stage = mSyntheticInputStage;
    } else {
        // 根据shouldSkipIme来判断使用哪个InputStage
        stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
    }
    
    // 调用deliver方法
    if (stage != null) {
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}
// 根据mFlags是否为FLAG_UNHANDLED来判断
public boolean shouldSendToSynthesizer() {
    if ((mFlags & FLAG_UNHANDLED) != 0) {
        return true;
    }
    return false;
}

下面需要查找哪里调用了deliverInputEvent方法(方法调用较多):

void doProcessInputEvents() {
    // Deliver all pending input events in the queue.
    while (mPendingInputEventHead != null) {
        // 从头部得到当前的事件,头部指向next
        QueuedInputEvent q = mPendingInputEventHead;
        mPendingInputEventHead = q.mNext;
        if (mPendingInputEventHead == null) {
            mPendingInputEventTail = null;
        }
        q.mNext = null;
        // 事件数量-1
        mPendingInputEventCount -= 1;
        Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                mPendingInputEventCount);

        long eventTime = q.mEvent.getEventTimeNano();
        long oldestEventTime = eventTime;
        if (q.mEvent instanceof MotionEvent) {
            MotionEvent me = (MotionEvent)q.mEvent;
            if (me.getHistorySize() > 0) {
                oldestEventTime = me.getHistoricalEventTimeNano(0);
            }
        }
        mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
        // 传递事件
        deliverInputEvent(q);
    }

    // We are done processing all input events that we can process right now
    // so we can clear the pending flag immediately.
    if (mProcessInputEventsScheduled) {
        mProcessInputEventsScheduled = false;
        mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
    }
}

// 给输入事件排序
void enqueueInputEvent(InputEvent event,
        InputEventReceiver receiver, int flags, boolean processImmediately) {
    adjustInputEventForCompatibility(event);
    // 生成输入事件,这个通过池来实现
    QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

    // Always enqueue the input event in order, regardless of its time stamp.
    // We do this because the application or the IME may inject key events
    // in response to touch events and we want to ensure that the injected keys
    // are processed in the order they were received and we cannot trust that
    // the time stamp of injected events are monotonic.
    // 始终按顺序排列输入事件,而不管其时间戳。 
    // 我们这样做是因为应用程序或IME可能会响应触摸事件而注入关键事件,
    // 并且我们希望确保注入的键以接收到的顺序进行处理,并且我们不能相信注入事件的时间戳是单调的。
    // 主要意思是说还是需要排序,下面是排序过程(后面会有图解释下)
    QueuedInputEvent last = mPendingInputEventTail;
    // 设置最后一个事件等于事件的尾部
    if (last == null) {
        // 如果为空则头部和尾部都等于这个事件
        mPendingInputEventHead = q;
        mPendingInputEventTail = q;
    } else {
        // 否则的话让事件的尾部的next等于当前这个事件,并将mPendingInputEventTail指向最后一个事件
        last.mNext = q;
        mPendingInputEventTail = q;
    }
    // 数据数量+1
    mPendingInputEventCount += 1;
    Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
            mPendingInputEventCount);

    // 如果是立即执行(这里只看这个)
    if (processImmediately) {
        doProcessInputEvents();
    } else {
        scheduleProcessInputEvents();
    }
}

// 从名字上可以看出这是一个关于输入事件的接收者
final class WindowInputEventReceiver extends InputEventReceiver {
    public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
        super(inputChannel, looper);
    }

    @Override
    public void onInputEvent(InputEvent event) {
        enqueueInputEvent(event, this, 0, true);
    }

    @Override
    public void onBatchedInputEventPending() {
        if (mUnbufferedInputDispatch) {
            super.onBatchedInputEventPending();
        } else {
            scheduleConsumeBatchedInput();
        }
    }

    @Override
    public void dispose() {
        unscheduleConsumeBatchedInput();
        super.dispose();
    }
}
InputEventReceiver.java
// 看注释可以知道这个方法被native层调用,所以我觉得到这里就应该算是java层的起点了
// Called from native code.
@SuppressWarnings("unused")
private void dispatchInputEvent(int seq, InputEvent event) {
    mSeqMap.put(event.getSequenceNumber(), seq);
    onInputEvent(event);
}

img_72b5ebd6e5a9bbc2d3c50bad9354767e.png
排序图

上面是排序图(灵魂画家。。),通过上面的代码可以看到,我们的事件是 native层调用InputEventReceiver中的dispatchInputEvent方法进行传递的,在传递过程中需要对事件进行排序处理,即 先来先处理
好了,到了这里基本上就是我们找到的整个触摸事件的“根源”了。当然,这里肯定不是最初的起点,但是起点可能太深了,需要一点点挖掘,量力而为。
整个过程已经完成了,我觉得有必要从后面到前面在梳理一遍:
img_6b4dfba7601fde494b01c7a785b845f9.png
流程图

到此为止,事件从java层的事件产生到传递的整个流程就已经全部展示完成,下面会分析一个嚼烂了的点——Android事件的分发。
img_3815a54d0837b9046404e3c728feccb8.png

顺便贴一张debug下方法调用图,整个过程如下面所示:
img_edc578c68c8a9509bd99218a57c861d4.png

关于为什么贴这张图?因为今天再写下面的一篇文章的时候,搜到了一些关于这方面的知识。结果,有人连整个分发过程还不清楚。。不想多说。
img_7253b454e50b81acd09838694e11dd6c.png

目录
相关文章
|
4月前
|
XML Java Android开发
Android Studio App开发之捕获屏幕的变更事件实战(包括竖屏与横屏切换,回到桌面与切换到任务列表)
Android Studio App开发之捕获屏幕的变更事件实战(包括竖屏与横屏切换,回到桌面与切换到任务列表)
39 0
|
1天前
|
存储 Java Linux
Android系统获取event事件回调等几种实现和原理分析
Android系统获取event事件回调等几种实现和原理分析
6 0
|
4月前
|
小程序 JavaScript 前端开发
微信小程序(十七)小程序监听返回键跳转事件(安卓返回也适用)
onUnload:function(){ wx.redirectTo({ url: '../index/index' }) wx.navigateTo({ url: '../index/index' }) wx.switchTab({ url: '../../member/member' }) }
305 0
|
4月前
|
XML Java Android开发
Android App事件交互Event之模仿京东App实现下拉刷新功能(附源码 可直接使用)
Android App事件交互Event之模仿京东App实现下拉刷新功能(附源码 可直接使用)
33 0
Android App事件交互Event之模仿京东App实现下拉刷新功能(附源码 可直接使用)
|
4月前
|
XML Java Android开发
Android App事件交互中辨别缩放与旋转手指的讲解与实战(附源码 可直接使用)
Android App事件交互中辨别缩放与旋转手指的讲解与实战(附源码 可直接使用)
36 0
|
4月前
|
XML Java Android开发
Android App事件交互中区分点击和长按动作以及识别手势滑动方向的讲解及实战(附源码 可直接使用)
Android App事件交互中区分点击和长按动作以及识别手势滑动方向的讲解及实战(附源码 可直接使用)
60 0
|
4月前
|
XML Java Android开发
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
43 0
|
4月前
|
XML 监控 Java
Android App开发之事件交互Event中检测软键盘和物理按键讲解及实战(附源码 演示简单易懂)
Android App开发之事件交互Event中检测软键盘和物理按键讲解及实战(附源码 演示简单易懂)
126 0
|
8月前
|
Android开发
Android 获取include标签中的控件属性并设置事件
Android 获取include标签中的控件属性并设置事件
138 0
|
11月前
|
设计模式 缓存 前端开发
Android 架构之 MVI 究极体 | 状态和事件分道扬镳,粘性不再是问题
Android 架构之 MVI 究极体 | 状态和事件分道扬镳,粘性不再是问题
474 0