发布时间:2025-12-09 20:45:45 浏览次数:4
Android 事件详细总结。
该文章基于android-28,仅分析Framework java层代码逻辑,仅供参考,不敢保证百分百正确无误。
一般说来当用户触摸屏幕或者按键操作,首次触发的是底层硬件驱动,驱动收到事件后并将该相应事件写入到输入设备节点, 便产生最原生态的内核事件。接着,输入(Input)系统取出原生态的事件再经过层层封装后成为KeyEvent或者MotionEvent ,最后交付给相应的目标窗口(Window)来消费该输入事件。其中Input模块的主要组成有:
简而言之,Android 中事件传递按照从上到下进行层级传递,事件处理从 Activity 开始到 ViewGroup 再到 View。事件传递方法包括dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent,其中前两个是 View 和 ViewGroup 都有的,最后一个是只有 ViewGroup 才有的方法。其中onTouch 方法要先于 onClick 触发执行,onTouch 在 dispatchTouchEvent 中被调用,而 onClick 在 onTouchEvent 方法中被调用,因此onTouchEvent 要后于 dispatchTouchEvent 方法的调用。
MotionEvent 继承自InputEvent并实现Parcelable接口,作为事件分发机制的核心对象,在Android中当用户触摸屏幕上的View 或 ViewGroup(及其子View),将产生点击事件(Touch事件)并根据Touch事件的发生时触摸的位置、时间、类型等相关细节封装不同类型的MotionEvent对象进行传递,该对象用于记录所有与移动相关的事件信息(事件分发和处理由Activity去调用native层完成),比较典型的有以上四种:
| MotionEvent.ACTION_DOWN | 在屏幕被按下时(所有事件的源头) | 1次 |
| MotionEvent.ACTION_MOVE | 在屏幕上进行滑动时 | 0次或者多次 |
| MotionEvent.ACTION_UP | 从屏幕上抬起时 | 0次或者1次 |
| MotionEvent.ACTION_CANCEL | 结束事件(非人为原因) | 0或者1次 |
所以一次完整的MotionEvent事件,是从用户触摸屏幕到离开屏幕。整个过程的动作序列:ACTION_DOWN(1次) —> ACTION_MOVE(N次) -> ACTION_UP(1次)。
而对于多点触摸,每一个触摸点Pointer会有一个id和index。对于多指操作,通过pointerindex来获取指定Pointer的触屏位置。比如单点操作时通过getX()获取x坐标,而多点操作时通过getX(pointerindex)获取x坐标。
MotionEvent 还有很多的ACTION类型,MotionEvent需要继承native 层InputEvent(其实是内核层的Input机制),因为Android Framework 通过JNI 进行处理,同时需要跨进程因而MotionEvent实现了Parcelable序列化接口。
Activity——作为Android四大基本组件之一,当手指触摸到屏幕时,屏幕硬件逐行不断地扫描每个像素点,获取到触摸事件后,从底层产生中断上报。再通过native层调用Java层InputEventReceiver中的dispatchInputEvent方法。最后经过层层调用交由Activity的dispatchTouchEvent方法来处理。
View——作为所有视图对象的父类,实现了Drawable.Callback(动画相关的接口)、KeyEvent.Callback(按键相关的接口)和AccessibilityEventSource(交互相关的接口)。
ViewGroup——ViewGroup是一个继承了View并实现了ViewParent(用于与父视图交互的接口), ViewManager(用于添加、删除、更新子视图到Activity的接口)的抽象类,作为盛放其他View的容器,可以包含View和ViewGroup,是所有布局的父类或间接父类。
Touch事件都源自按下屏幕里Activity中的View或者ViewGroup(及其子View),所以事件的处理都是由Activity、View或ViewGroup对象完成的,换言之,只在Activity、View或ViewGroup里拥有处理事件系列方法如下表所示:
从源码中可以得知在Activity、 ViewGroup 和View中都存在 dispatchTouchEvent 和 onTouchEvent 方法(但是在 ViewGroup 中还有一个 onInterceptTouchEvent 方法),他们都接受了一个MotionEvent类型的参数用于标记各种动作事件且返回值都是boolean型,true则代表不往下传递,false则表示继续往下传递,那么这些方法有何功能的呢?
boolean dispatchTouchEvent(MotionEvent event)——负责Touch事件的分发,Android 中所有的事件都必须经过此方法的分发,然后决定是自身消费当前事件还是继续往下分发给子View返回 true 则表示不继续分发,反之返回 false 则继续往下分发;而如果ViewGroup 则是先分发给 onInterceptTouchEvent 进行判断是否拦截该事件。
boolean onTouchEvent(MotionEvent event)—— 负责Touch事件的处理,返回 true 则表示消费当前事件,反之返回 false 则不处理交给子View继续进行分发。
boolean onInterceptTouchEvent(MotionEvent event)—— 负责Touch事件的拦截 返回 true 则表示拦截当前事件就不继续往下分发,交给自身的 onTouchEvent 进行处理;而返回 false 则不拦截,继续往下传。这是 ViewGroup 特有的方法,因为 ViewGroup 作为容器可以存放其他子 View,而View 则不能,换言之,只有 ViewGroup才有拦截事件的能力。
事件分发的核心流程都是围绕这些方法进行的,由不同的对象直接或者间接调用进行处理。
事件分发有多种类型, 以下是Touch相关的事件分发大致流程:
Activity——>Window(PhoneWindow)——>DecorView——>ViewGroup——>View。
事件的分发是由Activity开始的,当用户点击屏幕的时候首先接触到的就是Activity,从而触发Activity#dispatchTouchEvent方法,
Activity#dispatchTouchEvent返回结果之前,会先分发到ViewGroup&View,而最终的执行分为两个分支(抛开中间细节):
再回到分发流程,Activity#dispatchTouchEvent执行后,接着会通过getWindow()方法得到当前Activity的顶层窗口(即PhoneWindow)并调用它的superDispatchTouchEvent方法把事件分发到PhoneWindow,执行PhoneWindow#superDispatchTouchEvent方法传入到DecorView(根ViewGroup)…
DecorView继承自 FrameLayout 并实现了RootViewSurfaceTaker和WindowCallbacks接口,是当前界面的最外(顶)层容器,即setContentView方法所设置的View的父容器根ViewGroup。
//com.android.internal.policy.PhoneWindow#superDispatchTouchEvent@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {//分发至DecorViewreturn mDecor.superDispatchTouchEvent(event);}分发到DecorView,再通过DecorView#superDispatchTouchEvent方法
//com.android.internal.policy.DecorView#superDispatchTouchEventpublic boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);//实际上调用的是ViewGroup#dispatchTouchEvent}接下来分发到了ViewGroup中,ViewGroup的分发流程比较复杂,篇幅有限会省略部分逻辑。
ViewGroup&View的事件分发流程的起点是从PhoneWindow#superDispatchTouchEvent开始的,首先分发至根ViewGroup(DecorView)并执行ViewGroup#dispatchTouchEvent方法。
在ViewGroup每次调用ViewGroup#dispatchTouchEvent方法时,当requestDisallowInterceptTouchEvent方法返回true时则说明允许事件拦截,就会先执行ViewGroup#onInterceptTouchEvent判断是否进行事件拦截,
/*** Implement this method to intercept all touch screen motion events. ** @param ev The motion event being dispatched down the hierarchy.* @return Return true to steal motion events from the children and have* them dispatched to this ViewGroup through onTouchEvent().* The current target will receive an ACTION_CANCEL event, and no further* messages will be delivered here.*/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;}返回true则进行拦截,就会越过 if (!canceled && !intercepted)分支,直接执行ViewGroup#dispatchTransformedTouchEvent方法
/*** Transforms a motion event into the coordinate space of a particular child view,* filters out irrelevant pointer ids, and overrides its action if necessary.* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.* ViewGroup 真正分发事件的逻辑* @param child null则调用View#dispatchTouchEvent;反之则调用child里的dispatchTouchEvent方法*/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;}// Calculate the number of pointers to deliver.final int oldPointerIdBits = event.getPointerIdBits();final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;// If for some reason we ended up in an inconsistent state where it looks like we// might produce a motion event with no pointers in it, then drop the event.if (newPointerIdBits == 0) {return false;}// If the number of pointers is the same and we don't need to perform any fancy// irreversible transformations, then we can reuse the motion event for this// dispatch as long as we are careful to revert any changes we make.// Otherwise we need to make a copy.final MotionEvent transformedEvent;if (newPointerIdBits == oldPointerIdBits) {if (child == null || child.hasIdentityMatrix()) {if (child == null) {handled = super.dispatchTouchEvent(event);} else {final float offsetX = mScrollX - child.mLeft;final float offsetY = mScrollY - child.mTop;event.offsetLocation(offsetX, offsetY);handled = child.dispatchTouchEvent(event);event.offsetLocation(-offsetX, -offsetY);}return handled;}transformedEvent = MotionEvent.obtain(event);} else {transformedEvent = event.split(newPointerIdBits);}// Perform any necessary transformations and dispatch.if (child == null) {// 第一次触发Touch事件时执行到这里,去调用父类的dispatchTouchEventhandled = super.dispatchTouchEvent(transformedEvent);} else {final float offsetX = mScrollX - child.mLeft;final float offsetY = mScrollY - child.mTop;transformedEvent.offsetLocation(offsetX, offsetY);if (! child.hasIdentityMatrix()) {transformedEvent.transform(child.getInverseMatrix());}handled = child.dispatchTouchEvent(transformedEvent);}// Done.transformedEvent.recycle();return handled;}其核心逻辑取决于传入的child,当child =null则调用View#dispatchTouchEvent;反之则调用child自身(有可能是View或ViewGroup)的dispatchTouchEvent方法,假设调用的是View#dispatchTouchEvent方法
/*** Pass the touch screen motion event down to the target view, or this view if it is the target.* @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 the event should be handled by accessibility focus first.if (event.isTargetAccessibilityFocus()) {// We don't have focus or no virtual descendant has it, do not handle the event.if (!isAccessibilityFocusedViewOrHost()) {return false;}// We have focus and got the event, then use normal event dispatch.event.setTargetAccessibilityFocus(false);}//是否消费此事件标识,最终的返回值boolean result = false;if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesturestopNestedScroll();}if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//ListenerInfo(可从View#getListenerInfo方法获取)为所有View的事件监听封装类,包含了所有常见事件的监听接口ListenerInfo li = mListenerInfo;//如果外部调用了setOnTouchListener方法时,会先执行OnTouchListener#onTouch,当onTouch返回true时则不会执行View#onTouchEvent方法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;}如果外部调用了setOnTouchListener方法时设置onTouchListener监听时,会先执行View$$OnTouchListener#onTouch,当OnTouchListener#onTouch返回true时则不会执行View#onTouchEvent方法;反之还会去调用View#onTouchEvent方法
/*** Implement this method to handle touch screen motion events.** @param event The motion event.* @return True if the event was handled, false otherwise.*/public boolean onTouchEvent(MotionEvent event) {final float x = event.getX();final float y = event.getY();final int viewFlags = mViewFlags;final int action = event.getAction();// 是否可点击的标志final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;if ((viewFlags & ENABLED_MASK) == DISABLED) {if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;// A disabled view that is clickable still consumes the touch// events, it just doesn't respond to them.return clickable;}if (mTouchDelegate != null) {if (mTouchDelegate.onTouchEvent(event)) {return 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 checkremoveLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!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) {//PerformClick实现了Runnable接口mPerformClick = new PerformClick();}if (!post(mPerformClick)) {//在该方法内处理Click事件触发OnClickListener.onClick方法performClickInternal();}}}if (mUnsetPressedState == null) {mUnsetPressedState = new UnsetPressedState();}if (prepressed) {postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {// If the post failed, unpress right nowmUnsetPressedState.run();}removeTapCallback();}mIgnoreNextUpEvent = false;break;case MotionEvent.ACTION_DOWN:if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {mPrivateFlags3 |= PFLAG3_FINGER_DOWN;}mHasPerformedLongPress = false;if (!clickable) {checkForLongClick(0, x, y);break;}if (performButtonActionOnTouchDown(event)) {break;}// 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 awaysetPressed(true, x, y);//处理OnLongClickListener.onLongClick方法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:if (clickable) {drawableHotspotChanged(x, y);}// Be lenient about moving outside of buttonsif (!pointInView(x, y, mTouchSlop)) {// Outside button// Remove any future long press/tap checksremoveTapCallback();removeLongPressCallback();if ((mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;}break;}return true;}return false;}所以基本就是在View#onTouchEvent方法的处理onClickListener
private final class PerformClick implements Runnable {@Overridepublic void run() {performClickInternal();}}public boolean performClick() {// We still need to call this method to handle the cases where performClick() was called// externally, instead of through performClickInternal()notifyAutofillManagerOnClick();final boolean result;final ListenerInfo li = mListenerInfo;if (li != null && li.mOnClickListener != null) {playSoundEffect(SoundEffectConstants.CLICK);//处理OnClickListener接口li.mOnClickListener.onClick(this);result = true;} else {result = false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);notifyEnterOrExitForAutoFillIfNeeded(true);return result;}onLongClickListener等其他监听接口也类似。
拦截事件可以通过两种形式:
继承ViewGroup或其子类重写onInterceptTouchEvent方法并返回true。
在子View中调用父ViewGroup的requestDisallowInterceptTouchEvent方法并返回true。