0
点赞
收藏
分享

微信扫一扫

note_44:事件分发

快乐小码农 2022-01-13 阅读 13
androidjava

事件分发


参考:

  • Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
  • Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
  • Android事件分发机制详解:史上最全面、最易懂
  • 图解 Android 事件分发机制
  • Android全面解析之Window机制

文章目录


本文贴出的代码有删减!


一、流程概览

从触摸某一个View的那一刻开始,事件分发流程如下:
事件分发

说明


二、dispatchTouchEvent

流程概览的图中可以看到,有三个地方使用了dispatchTouchEvent,分别是:ActivityViewGroupView。从系统源码中可以看出,这三个地方的对于这个方法的实现完全不一样,含义也不一样。

1. Activity.dispatchTouchEvent

Activity.java

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;
}

(1)onUserInteraction和onUserLeaveHint

onUserInteractiononUserLeaveHint是成对使用的回调方法,需要用户自己实现。可以用于处理或取消状态栏的通知列表。

onUserInteractionkey、touch、trackball event分发给Activity的时候调用。

onUserLeaveHintActivity回到后台之前,执行onPause之前调用。

(2)PhoneWindow

PhoneWindow.java

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    private DecorView mDecor;

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
    }  
}

Window是个abstract class,它唯一的实现类是android.policy.PhoneWindow

PhoneWindow是一个顶级窗口,它持有的mDecor作为该Windowview,是顶级View

DecorView继承于FrameLayout,也就是说它是个ViewGroupFrameLayout中没有重写dispatchTouchEvent,所以DecorView执行super.dispatchTouchEvent时,就是在执行ViewGroup.dispatchTouchEvent

PhoneWindow执行superDispatchTouchEvent实现了从Activity.dispatchTouchEvent过渡到ViewGroup.dispatchTouchEvent。也就是说把事件从Activity传递到ViewGroup。从代码上看,该过程是必然会发生的,没有被拦截的情况出现。

(3)返回值

可以见到Activity.dispatchTouchEvent是有返回值的,但是这里不管返回TRUE还是FALSE,都代表了消费了事件。当返回FALSE时,必然是执行了onTouchEvent。当返回TRUE时,一定分发了事件,但可能执行了onTouchEvent

2. ViewGroup.dispatchTouchEvent

ViewGroup.java

public boolean dispatchTouchEvent(MotionEvent ev) {
    // Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        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;
    }

    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;
    
    if (!canceled && !intercepted) {
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = customOrder
                ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}
        }
    }
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, 
                                              View child, int desiredPointerIdBits) {
    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }
}

ViewGroup在事件分发的时候跟Activity最大的不同是,要先判断是否该事件是否被CANCELEDINTERCEPTED,然后才决定是否要继续往下dispatch

如果要继续往下分发,那么必须找到触摸的那个子view,实现的方式就是最普通、最直接的遍历整个子View数组。如果找到的子view的边界符合触摸事件的坐标条件且可以接收事件,那么就可以分发了。

在继续往下分发的过程中,如果符合条件的子view是空的,那么事件会调用super.dispatchTouchEevnt处理。否则,分发给子view

onInterceptTouchEvent

ViewGroup.java

// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;

/**
 * Implement this method to intercept all touch screen motion events.  This
 * allows you to watch events as they are dispatched to your children, and
 * take ownership of the current gesture at any point.
 *
 * <p>Using this function takes some care, as it has a fairly complicated
 * interaction with {@link View#onTouchEvent(MotionEvent)
 * View.onTouchEvent(MotionEvent)}, and using it requires implementing
 * that method as well as this one in the correct way.  Events will be
 * received in the following order:
 *
 * <ol>
 * <li> You will receive the down event here.
 * <li> The down event will be handled either by a child of this view
 * group, or given to your own onTouchEvent() method to handle; this means
 * you should implement onTouchEvent() to return true, so you will
 * continue to see the rest of the gesture (instead of looking for
 * a parent view to handle it).  Also, by returning true from
 * onTouchEvent(), you will not receive any following
 * events in onInterceptTouchEvent() and all touch processing must
 * happen in onTouchEvent() like normal.
 * <li> For as long as you return false from this function, each following
 * event (up to and including the final up) will be delivered first here
 * and then to the target's onTouchEvent().
 * <li> If you return true from here, you will not receive any
 * following events: the target view will receive the same event but
 * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
 * events will be delivered to your onTouchEvent() method and no longer
 * appear here.
 * </ol>
 *
 * @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) {
    return false;
}

public boolean dispatchTouchEvent(MotionEvent ev) {
    // Handle an initial down.
   if (actionMasked == MotionEvent.ACTION_DOWN) {
        resetTouchState();
   }
    
    // Dispatch to touch targets.
    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 {
                final boolean cancelChild = resetCancelNextUpFlag(target.child) 
                    || intercepted;
                if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, 
                                                  target.pointerIdBits)) {
                    handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                        target.recycle();
                        target = next;
                            continue;
                 }
            }
            predecessor = target;
            target = next;
        }
    }
}

/**
 * Resets all touch state in preparation for a new cycle.
 */
private void resetTouchState() {
    clearTouchTargets();
}

/**
  * Clears all touch targets.
  */
private void clearTouchTargets() {
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        mFirstTouchTarget = null;
    }
}

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;
    }
}

只有ViewGrouponInterceptTouchEvent这个方法,且返回值默认是FALSE。源码中全靠注释,代码只有一句return false;

ViewGroup.dispatchTouchEvent中可以看出,事件处理的周期从ACTION_DOWN开始,于下一个ACTION_DOWN的前一个事件结束。onInterceptTouchEvent的返回值表示是否要拦截这个事件。以一个周期为例,如果ViewGroup.onInterceptTouchEvent返回TRUE,那么这个触摸事件就会直接派发给ViewGroup自己。ACTION_DOWN事件只派发给ViewGroup,其他子view收不到任何事件。在这个周期结束之前,ViewGroup会持续拦截所有事件,但是只有事件为ACTION_DOWN时,其他子view才收不到;如果事件为ACTION_MOVE等事件,其他子view会收到事件ACTION_CANCEL

onInterceptTouchEvent返回TRUE时,ViewGroup会调用super.dispatchTouchEvent,也就是会调用View.dispatchTouchEvent

3. View.dispatchTouchEvent

View.java

public boolean dispatchTouchEvent(MotionEvent 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;
     }
}

从源码中可以看出,OnTouchListener.onTouch的优先级会高于onTouchEvent

onTouchEvent是处理触摸事件时调用的,OnTouchListener.onTouch可以拦截这个触摸事件,跟ViewGroup.onInterceptTouchEvent有点像,只是它的条件比较多,而且不一定会执行。

(1)ViewGroup调用super.dispatchTouchEvent

如果此时的View.onTouchEvent是由ViewGroupsuper.dispatchTouchEvent触发的,那么当View.onTouchEvent返回TRUE时,这意味着触摸事件已经被消费;如果View.onTouchEvent返回FALSE时,这意味着触摸事件没有被消费,那么ViewGroup.dispatchTouchEvent就会返回FALSE,进而触发Activity.onTouchEvent

(2)View调用dispatchTouchEvent

如果此时的View.dispatchTouchEventView调用的话,那么就证明触摸事件已经分发到对应的子view上了。如果View.onTouchEvent返回TRUE,那么该事件被消费;如果View.onTouchEvent返回FALSE,那么会回到ViewGroup.dispatchTouchEvent中,要么继续遍历找到符合条件的子view派发事件,要么调用ViewGroup.onTouchEvent进行处理。如果是后者,如果ViewGroup.onTouchEvent返回TRUE,那么事件被消费;如果ViewGroup.onTouchEvent返回FALSE,那么就会回到Activity.dispatchTouchEvent,然后调用Activity.onTouchEvent


三、总结

事件分发的过程就是先从Activity.dispatchTouchEvent开始,然后到ViewGroup.dispatchTouchEvent。这时要先判断ViewGroup.onInterceptTouchEvent是否有拦截事件,如果有,事件交由ViewGroup.onTouchEvent处理;如果没有,事件交由View.dispatchTouchEvent处理。在View.dispatchTouchEvent中,如果不考虑OnTouchListener.onTouchEvent的话,就会调用View.onTouchEvent

在调用各种onTouchEvent的过程中,如果返回值为TRUE,就证明事件被消费;如果返回值为FALSE,除了Activity会视作事件被消费,其他的ViewGroupView都会视为事件未被消费,然后将事件交由Activity父viewonTouchEvent进行处理。

在调用各种dispatchTouchEvent的过程中,不考虑Activity,如果返回值为TRUE,意味着该事件被当前view或者子view消费了;如果返回值为FALSE,意味着事件未被消费,需要交由Activity父viewonTouchEvent进行处理。

举报

相关推荐

0 条评论