note_44:事件分发

24 篇文章 0 订阅

事件分发


参考:



本文贴出的代码有删减!


一、流程概览

(1)总图

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

View
dispatchTouchEvent
onTouchEvent
ViewGroup
dispatchTouchEvent
onTouchEvent
Activity
dispatchTouchEvent
onTouchEvent
true
false
true
false
PhoneWindow.superDispatchTouchEvent
true
false
返回值
返回值
返回值
结束
执行
开始
执行
开始
事件已消费
结果
事件未消费
结束
执行
开始
onInterceptTouchEvent
执行
开始
事件已消费
结果
事件未消费
结束
执行
开始
执行
开始
事件已消费
结果
开始
结束

说明

  1. onTouchEvent就是直接处理触摸事件时调用的方法,跟它比较相似的OnTouchListener.onTouch则是在View接收到触摸事件之前调用的,即优先级高于onTouchEvent
  2. onInterceptTouchEvent是指是否要拦截当前的事件,值得注意的是只有ViewGroup才有这个方法,ActivityView都没有。这个方法的返回值如果是TRUE,那么就表示当前ViewGroup要自己处理这个事件,不再将事件传给后续的子View。默认返回FALSE
  3. dispatchTouchEvent就是事件分发,如果返回TRUE,那就是当前事件被消费了,不会再往下分发。onInterceptTouchEvent在就是在该方法内执行的。通过先确定当前事件没有有被CANCELINTERCEPT,才会继续dispatch。

(2)一个事例

假设有一个Activity,它的布局是一个LinearLayout,中间有一个Button

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">
    <Button
            android:layout_width="180dp"
            android:layout_height="180dp"
            android:text="hello"
            android:textSize="22sp"
            android:gravity="center" />
</LinearLayout>
从点击Button开始的事件分发流程如下:
点击Button
Activity分发事件
PhoneWindow分发事件
LinearLayout分发事件
LinearLayout拦截事件
LinearLayout获得事件
分发结束
Button分发事件
Button获得事件
从点击Buttton开始的事件消费情况如下:
点击Button
省略部分中间过程
LinearLayout拦截事件
LinearLayout获得事件
LinearLayout消费事件
事件被消费
Activity消费事件
Button分发事件
Button获得事件
Button消费事件

二、dispatchTouchEvent

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

1. Activity.dispatchTouchEvent

Activity.dispatchTouchEvent流程图
开始
dispatchTouchEvent
当前是ACTION_DOWN
onUserInteraction
return onTouchEvent
Window.superDispatchTouchEvent
return true
Activity.dispatchTouchEvent中涉及到的部分变量的类图
Activity +dispatchTouchEvent(MotionEvent ev) : boolean +onUserInteraction() +onUserLeaveHint() Window PhoneWindow FrameLayout DecorView

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事件分发流程图:
开始
dispatchTouchEvent
事件被INTERCEPTED
onInterceptTouchEvent
onTouchEvent
结束
child.dispatchTouchEvent
child.onTouchEvent

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.dispatchTouchEvent流程图:
yes
no
true
false
开始
View.dispatchTouchEvent
onTouchListener == null
onTouchEvent
结束
OnTouchListener.onTouch
  • View.dispatchTouchEvent类图:
ViewGroup +dispatchTouchEvent(MotionEvent ev) : boolean +onInterceptTouchEvent(MotionEvent ev) : boolean View +dispatchTouchEvent(MotionEvent ev) : boolean +onTouchEvent(MotionEvent ev) : boolean «interface» OnTouchListener onTouch(MotionEvent ev)

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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值