IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 事件分发机制 -> 正文阅读

[移动开发]Android 事件分发机制

前言

点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。

基础概念

涉及到类

事件分发过程实际上就是Activity、ViewGroup、View对MotionEvent事件的分发,拦截,消费的过程。
事件分发机制
其中,只有ViewGroup能进行事件拦截,因此只有ViewGroup有onInterceptTouchEvent方法。

方法
Activity拥有dispathTouchEvent和onTouchEvent方法
ViewGroup拥有dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent方法
View拥有dispathTouchEvent和onTouchEvent方法
事件类型含义
ACTION_DOWN按下屏幕时,触发此事件,只会触发一次
ACTION_MOVE移动手指时,触发此事件,会多次触发
ACTION_UP手指离开屏幕时,触发此事件,只会触发一次
ACTION_CANCEL取消事件时,触发此事件,只会触发一次

涉及到方法

方法含义
dispathTouchEvent事件分发
onInterceptTouchEvent事件拦截
onTouchEvent事件消费

方法详解:

  • dispathTouchEvent

用来进行事件的分发。如果事件能够传递给当前ViewGrop或者View,那么此方法一定会被调用,返回结果受当前ViewGrop或者View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

  • onInterceptTouchEvent

用来判断是否拦截某个事件,如果当前ViewGrop拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

  • onTouchEvent

用来处理点击事件,返回结果是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前ViewGrop或者View无法再次接收事件。

小结

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity—>Window---->DecorView ,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给DecorView 。DecorView 接收到事件后,就会按照事件分发机制去分发事件。

事件分发过程源码解析

点击事件传递到Activity过程

当我们点击屏幕时,通过Android消息机制,从Looper从MessageQueue中取出该事件,发送给WindowInputEventReceiver。

// ViewRootImpl.java  源码

final class WindowInputEventReceiver extends InputEventReceiver {
        Override
        public void onInputEvent(InputEvent event) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility");
            List<InputEvent> processedEvents;
            try {
                processedEvents =
                    mInputCompatProcessor.processInputEventForCompatibility(event);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            if (processedEvents != null) {
                if (processedEvents.isEmpty()) {
                    // InputEvent consumed by mInputCompatProcessor
                    finishInputEvent(event, true);
                } else {
                    for (int i = 0; i < processedEvents.size(); i++) {
                        enqueueInputEvent(
                                processedEvents.get(i), this,
                                QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
                    }
                }
            } else {
                enqueueInputEvent(event, this, 0, true);
            }
        }
}

void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {
        // 从队列中取出输入事件
        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.
        QueuedInputEvent last = mPendingInputEventTail;
        if (last == null) {
            mPendingInputEventHead = q;
            mPendingInputEventTail = q;
        } else {
            last.mNext = q;
            mPendingInputEventTail = q;
        }
        mPendingInputEventCount += 1;
        Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                mPendingInputEventCount);
        
        if (processImmediately) {// 是否立即处理事件
            doProcessInputEvents();
        } else {
            scheduleProcessInputEvents();
        }
    }

WindowInputEventReceiver是ViewRootImpl的内部类,通过enqueueInputEvent方法,将输入事件加入输入事件队列中,doProcessInputEvents() 方法并进行处理和转发事件。

// ViewRootImpl.java  源码

void doProcessInputEvents() {
     while (mPendingInputEventHead != null) {
         ...
         // 发送输入事件
         deliverInputEvent(q);
         
     }
     ...
}

private void deliverInputEvent(QueuedInputEvent q) {
    ...
    InputStage stage;
    if (q.shouldSendToSynthesizer()) {
      stage = mSyntheticInputStage;
    } else {
      stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
    }  
    
    if (stage != null) {
       handleWindowFocusChanged();
       stage.deliver(q);
    } else {
      finishInputEvent(q);
    }

}

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;

            mAttachInfo.mUnbufferedDispatchRequested = false;
            mAttachInfo.mHandlingPointerEvent = true;
            // mView 就是DecorView(实际上是FrameLayout)
            boolean handled = mView.dispatchPointerEvent(event);
            maybeUpdatePointerIcon(event);
            maybeUpdateTooltip(event);
            mAttachInfo.mHandlingPointerEvent = false;
            if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
                mUnbufferedInputDispatch = true;
                if (mConsumeBatchedInputScheduled) {
                    scheduleConsumeBatchedInputImmediately();
                }
            }
            return handled ? FINISH_HANDLED : FORWARD;
        }
}

InputStage 是个抽象类,具体工作交由它的子类ViewPostImeInputStage来负责处理。ViewPostImeInputStage 类的onProcess()方法判断事件是哪种类型的。根据判断手指点击屏幕条件,会调用processPointerEvent()方法,事件会交由DecorView的dispatchPointerEvent()向下级View 进行分发处理。 接着往下看

// View.java 源码

 public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }

//  DecorView.java 源码
 @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);
    }
    

从上面的代码看,事件下发到View的dispatchPointerEvent()方法。ViewGroup重写View的dispatchTouchEvent()方法,事件传递到ViewGroup的dispatchTouchEvent()方法。DecorView是ViewGroup的子类,并且重写了dispatchTouchEvent()方法。通过Window回调Callback方法,事件传递到Activity的dispatchTouchEvent()方法中。

Activity的事件分发过程

点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件派发,具体的工作是由Activity内部的Window来完成。Window 会将事件传递给DecorView,DecorView一般就是当前界面的底层容器(即 setContentView 所设置的View父容器),通过Activity.getWindow.getDecorView()可以获得。

// Activity 源码
public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback,
        AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {
    
    private Window mWindow;        
    ....
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    ...
    public Window getWindow() {
        return mWindow;
    }
            
    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
            
}

上面的代码来看,事件开始交给Activity所附属的Window进行分发,如果返回true,整个事件循环就结束了。返回false意味着事件没人处理,所有View的onTouchEvent都返回false,那么Activity的onTouchEvent就会被调用。

// Window 源码
public abstract class Window {
    .... 
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
    ....
}

// PhoneWindow 源码
public class PhoneWindow extends Window implements MenuBuilder.Callback {

    ....
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    ....
    @Override
    public final @NonNull View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
    ....
    
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    ....
}


// DecorView 源码
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
  
 ....
 public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
}

 ....
}

从上面的代码来看,主要是Window将事件传递给ViewGroup的过程。Window是个抽象类,而Window的superDispatchTouchEvent()方法也是个抽象方法。实际上是由PhoneWindow来具体实现的,而PhoneWindow将事件直接传递给了DecorView。由于DecorView继承自FrameLayout且是父View,所以最终事件传递给View。从这里开始,事件已经传递到顶级View了,即在Activity 中通过setContentView所设置的View,另外顶级View也叫根View,顶级View一般来说都是ViewGroup。

ViewGroup的事件分发过程

ViewGroup 对点击事件的分发过程,其主要实现在ViewGroup 的dispatchTouchEvent() 方法中。这个方法比较长,分段说明一下。

// ViewGroup 源码
if (onFilterTouchEventForSecurity(ev)) {
     ....
    // Check for interception.
    final boolean intercepted;
    // 不拦截事件并将事件交由子元素处理时mFirstTouchTarget != null条件成立
    if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
        // 注释1            
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
              intercepted = onInterceptTouchEvent(ev);
              ev.setAction(action); // restore action in case it was changed
            } else {
              intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }     
 }

上面这段代码主要是当前ViewGroup是否拦截点击事件这个逻辑。注释1: FLAG_DISALLOW_INTERCEPT标记位是通过requestDisallowInterceptTouchEvent 方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件原因是,因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT标记位。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent 方法来询问自己是否要拦截事件。

 // ViewGroup 会在ACTION_DOWN 事件到来时做重置状态的操作
 if (actionMasked == MotionEvent.ACTION_DOWN) {
      // Throw away all previous state when starting a new touch gesture.
      // The framework may have dropped the up or cancel event for the previous gesture
      // due to an app switch, ANR, or some other state change.
      cancelAndClearTouchTargets(ev);
      // 在下面方法中会对FLAG_DISALLOW_INTERCEPT 进行重置
      resetTouchState();
 }

从上面的代码分析来看,当ViewGroup决定拦截事件后,后续的点击事件将默认交给ViewGroup 处理并且不再调用它的onInterceptTouchEvent方法。 注意:onInterceptTouchEvent不是每次事件都会被调用的,如果想提前处理所有的点击事件,要选择dispatchTouchEvent 方法,只有这个方法能确保每次调用。

当ViewGroup 不拦截事件的时候,事件会向下分发交由它的子View 进行处理,源码如下:

final ArrayList<View> preorderedList = buildTouchDispatchChildList();
   final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);
    if (!child.canReceivePointerEvents()
               || !isTransformedTouchPointInView(x, y, child, null)) {
            continue;
        }

    ewTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
      newTouchTarget.pointerIdBits |= idBitsToAssign;
      break;
    }
    resetCancelNextUpFlag(child);
    //  将事件交给每一个view处理,返回true,说明该子view消费了down事件
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
      // Child wants to receive touch within its bounds.
      mLastTouchDownTime = ev.getDownTime();
       if (preorderedList != null) {
           // childIndex points into presorted list, find original index
          for (int j = 0; j < childrenCount; j++) {
               if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                   }
                }
        } else {
          mLastTouchDownIndex = childIndex;
        }

     mLastTouchDownX = ev.getX();
     mLastTouchDownY = ev.getY();
     // 如果有子view消费了down事件,则给mFirstTouchTarget赋值
     newTouchTarget = addTouchTarget(child, idBitsToAssign);
     alreadyDispatchedToNewTouchTarget = true;
     break;
   }
 // The accessibility focus didn't handle the event, so clear
  // the flag and do a normal dispatch to all children.
  ev.setTargetAccessibilityFocus(false);                           
  }
            //经过上面的遍历没有找到能够处理down事件的子view,只能将事件交给自己处理了,
            //此时child为null
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary 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调用requestDisallowInterceptTouchEvent(false),会将interceptd置true,使得cancelChild为true
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        // 此处主要处理move、up、cancel事件,具体分析后面给出
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        // 如果事件cancel了,这里会将mFirstTouchTarget置为null
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            //...省略...
        return handled;
}

     //这个方法才是真正分发处理事件的方法,down,move,up,cancel都由这个方法处理
    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();
        
        // 如果cancel为true,会先将MOVE事件换成ACTION_CANCEL,交给child处理,然后又改成MOVE。
        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;
        }
        //...省略...
        
        //将DOWN、MOVE、UP事件,交给child处理。
        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            //...省略...
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        // Done.
        transformedEvent.recycle();
        return handled;
    }

小结:

  1. 点击事件达到ViewGroup,会调用ViewGroup 的dispatchTouchEvent 方法。如果ViewGroup拦截事件(onInterceptTouchEvent)返回true,则事件由ViewGroup处理。

  2. 如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。也就是如果都设置的话,onTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用。

  3. 如果ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。到此为止,事件已经从ViewGroup传递到下一层View,接下来的传递过程和ViewGroup 是一致的,如此循环,完成整个事件的分发。

View 的事件分发过程

View(这里不包含ViewGroup)是一个单独元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。

// View 的 源码
public boolean dispatchTouchEvent(MotionEvent event) {
    ....
    boolean result = false;
    ....
    
    if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            // 注释1
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
           // 注释1
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        
    ....
    return result;
}

注释1:判断有没有设置OnTouchListener,如果OnTouchListener 中的onTouch方法返回true,那么onTouchEvent就不会调用;反之就不会调用。可见OnTouchListener的优先级高于onTouchEvent。

接着再分析onTouchEvent 的实现。

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);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        // 如果View设置代理,执行TouchDelegate的onTouchEvent 方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
     
     //  clickable为true,onTouchEvent一定返回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.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //如果设置了点击事件,在收到ACTION_UP事件时,执行click事件
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        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:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        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 away
                        setPressed(true, x, y);
                        //如果设置了长按事件,在收到ACTION_DOWN事件时,在500ms时,会检查执行长按事件
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    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);
                    }

                    final int motionClassification = event.getClassification();
                    final boolean ambiguousGesture =
                            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                    int touchSlop = mTouchSlop;
                    if (ambiguousGesture && hasPendingLongPressCallback()) {
                        if (!pointInView(x, y, touchSlop)) {
                            // The default action here is to cancel long press. But instead, we
                            // just extend the timeout here, in case the classification
                            // stays ambiguous.
                            removeLongPressCallback();
                            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                    * mAmbiguousGestureMultiplier);
                            // Subtract the time already spent
                            delay -= event.getEventTime() - event.getDownTime();
                            checkForLongClick(
                                    delay,
                                    x,
                                    y,
                                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        }
                        touchSlop *= mAmbiguousGestureMultiplier;
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, touchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }

                    final boolean deepPress =
                            motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                    if (deepPress && hasPendingLongPressCallback()) {
                        // process the long click action immediately
                        removeLongPressCallback();
                        checkForLongClick(
                                0 /* send immediately */,
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                    }

                    break;
            }

            return true;
        }
    
}

总体调用流程

事件分发机制

Activity 层分析

Activity.dispatchTouchEvent()
  • 第一种情况:返回true

在自己的Activity中重写了父类的dispatchTouchEvent(MotionEvent ev)方法,因此不会再调用getWindow().superDispatchTouchEvent(ev)方法了,即不会将事件传递到View层,同时也不会调用onTouchEvent(ev);该事件直接被消费了。后续再来的MOVE和UP事件也不会再向下分发,而是会直接经过DecorView.dispatchTouchEvent方法,通过回调交给Activity的dispatchTouchEvent,由于返回了true,会直接消费,也不会将事件交给onTouchEvent了。

  • 第二种情况:返回false

直接消费了,不会将事件向下传了,也不会将事件交给onTouchEvent了

  • 第三种情况:返回super.dispatchTouchEvent(ev)

如果调用默认的方法,即不对dispatchTouchEvent做任何处理,那么就会调用父类Activity的dispatchTouchEvent方法,将会调用getWindow().superDispatchTouchEvent(ev)方法,将event事件传递给decorView(ViewGroup)的dispatchTouchEvent,如果getWindow().superDispatchTouchEvent(ev)返回true,表示有子view消费了此事件,那么Activity的dispatchTouchEvent会返回true,事件到此结束,否则,会将事件传递给Activity的onTouchEvent,不论onTouchEvent返回值如何,事件分发都到此结束。

Activity.onTouchEvent()
  • 第一种情况:返回true

直接消费了,终结了事件传递

  • 第二种情况:返回false

直接消费了,终结了事件传递

ViewGroup 层分析

ViewGroup.dispatchTouchEvent()
  • 第一种情况:返回true

在自定义的ViewGroup重写了dispatchTouchEvent,并返回true,就不会走父类中分发、拦截、消费的流程,直接告诉父容器自己处理了这个事件。

  • 第二种情况:返回false

在自定义的ViewGroup重写了dispatchTouchEvent,并返回返回false,同样也不会走父类中分发、拦截、消费的流程,直接告诉父容器自己不处理这个事件,将事件交给父容器自己处理。

  • 第三种情况:返回super.dispatchTouchEvent(ev)

在自定义的ViewGroup重写了dispatchTouchEvent,并返回super.dispatchTouchEvent,即会走父类默认的分发流程。

ViewGroup.onInterceptTouchEvent()
  • 第一种情况:返回true

表示拦截事件,交给自己的onTouchEvent处理

  • 第二种情况:返回false

表示不拦截事件,会将事件分发给子view

  • 第三种情况:返回super.onInterceptTouchEvent(ev)

表示不拦截事件,会将事件分发给子view

ViewGroup.onTouchEvent()
  • 第一种情况:返回true

在自定义ViewGroup重写onTouchEvent,并返回true,表示消费事件。

  • 第二种情况:返回false

在自定义ViewGroup重写onTouchEvent,并返回false,表示不消费事件。事件交由父容器处理。

  • 第三种情况:返回super.onTouchEvent(ev)

在自定义ViewGroup重写onTouchEvent,并返回super.onTouchEvent(ev),表示调用父类默认的事件处理方法。

View 层分析

View.dispatchTouchEvent()
  • 第一种情况:返回true

在自定义View重写了dispatchTouchEvent,并返回true,就不会走分发,消费的流程,直接告诉父容器自己处理了这个事件。

  • 第二种情况:返回false

在自定义View重写了dispatchTouchEvent,并返回false,直接告诉父容器自己不处理这个事件。

  • 第三种情况:返回super.dispatchTouchEvent(ev)

在自定义View重写了dispatchTouchEvent,并返回super.dispatchTouchEvent,即会走父类View默认的分发流程。

View.onTouchEvent()
  • 第一种情况:返回true

在自定义view重写onTouchEvent,并返回true,表示消费事件。

  • 第二种情况:返回false

在自定义view重写onTouchEvent,并返回false,表示不消费事件。事件交由父容器处理。

  • 第三种情况:返回super.onTouchEvent(ev)

在自定义view重写onTouchEvent,并返回super.onTouchEvent,表示调用父类默认的事件处理方法。

滑动冲突解决

常见的滑动冲突场景

  • 外部滑动方向和内部滑动方向不一致
  • 外部滑动方向和内部滑动方向一致
  • 上面2种情况的嵌套
    在这里插入图片描述

解决方法

内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。这种方法需要配合requestDisallowInterceptTouchEvent 方法才能正常工作。

//内部拦截法(父容器的onInterceptTouchEvent)
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //down事件不能拦截,否则,子view就接收不到事件了
        if (ev.getAction() == MotionEvent.ACTION_DOWN){
            return false;
        }
        return true;
    }
    
    //2.内部拦截法(父容器-->onInterceptTouchEvent,子View-->dispatchTouchEvent)
   
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // true 会使得父容器 dispatchTouchEvent中的disallowIntercept为true,
                // 导致父容器不会调用onInterceptTouchEvent,即不会拦截
                getParent().requestDisallowInterceptTouchEvent(true);
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float moveX = lastX - x;
                float moveY = lastY - y;
                //false 会使得父容器 dispatchTouchEvent中的disallowIntercept为false,
                //导致父容器会调用onInterceptTouchEvent,即会拦截事件
                if (父容器需要当前的点击事件) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                lastX = ev.getX();
                lastY = ev.getY();

                break;
            case MotionEvent.ACTION_UP:
                
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

上面的代码是内部拦截法的典型逻辑,当面对不同的滑动策略只需要修改里面的条件即可

外部拦截法

外部拦截法指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截。这种方法需要重写父容器的onInterceptTouchEvent 方法,在内部做相应的拦截即可。

//外部拦截法 (父容器-->onInterceptTouchEvent)

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        boolean intercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
               
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
               
                if (父容器需要当前的点击事件){
                    intercept = true;
                } else {
                    intercept = false;
                }
               
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
            default:
                break;
        }
        return intercept;
    }

上面的代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前的点击事件这个条件即可。在onInterceptTouchEvent方法中,首先ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会交由父容器处理。这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false。

考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,一旦父容器开始拦截任何一个事件,那么后续的事件都会交给它处理。

结论

  1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up 事件结束。

  2. 某个ViewGroup一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用。就是说当一个ViewGroup决定拦截一个事件后,那么系统会把同一个事件序列内其他方法都直接交给它来处理,因此就不再调用这个ViewGroup的onInterceptTouchEvent 去询问他是否要拦截了。

  3. 某个View 一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理, 那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。

  4. 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

  5. ViewGroup 默认不拦截任何事件。Android 源码中ViewGroup的onInterceptTouchEvent 方法默认返回false。

  6. View 没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

  7. View 的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable 同时为false)。

  8. View的enable 属性不影响onTouchEvent的默认返回值。哪怕一个View是disable 状态的,只要它的clickable或longClickable有一个为true,那么它的onTouchEvent返回true。

  9. onClick 会发生的前提是当前View是可点击的,并且它收到了down和up的事件。

  10. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发子view,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但ACTION_DOWN事件除外。

参考资料

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-12-08 13:55:57  更:2021-12-08 13:57:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 7:54:25-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码