Android事件分发机制

Activity对点击事件的分发

在点击事件产生了,首先最先接触到点击事件的就是Activity,Activity通过 dispatchTouchEvent 来进行事件的分发。源码如下:

<b>Activity # dispatchTouchEvent</b>

   public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

由上面的源码可以得知,从getWindow().superDispatchTouchEvent(ev) 中得知, Activity事件的分发是由内部的Window来完成的。如果他返回的是true,那个事件循环就此结束了。如果返回false ,那么意味着所有View 的onTouchEvent都返回了false,都不处理事件,那么最后 return onTouchEvent(ev); 就会执行,既Activity中onTouchEvent就会执行。

从上面可以知道,getWindow().superDispatchTouchEvent(ev)的执行就是用来把事件分发给子View的。

<b>Window # superDispatchTouchEvent</b>

 public abstract boolean superDispatchTouchEvent(MotionEvent event);

但是这个方法是abstract方法,那么就有继承于Window的类实现了这个方法,从Activity的启动中,我们可以得知 PhoneWindow 就是这个类。

<b>PhoneWindow # superDispatchTouchEvent</b>

private DecorView mDecor;

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

那么PhoneWindow直接把事件传递给了 DecorView

从PhoneWindow 的构造方法中可以看到

mDecor = (DecorView) preservedWindow.getDecorView();

Window # getDecorView

 /**
 * Retrieve the top-level window decor view (containing the standard
 * window frame/decorations and the client's content inside of that), which
 * can be added as a window to the window manager.
 *
 * <p><em>Note that calling this function for the first time "locks in"
 * various window characteristics as described in
 * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}.</em></p>
 *
 * @return Returns the top-level window decor view.
 */
public abstract View getDecorView();

从Window中getDecorView的注解可以看到DecorView拿到的就是Activity所设置的View,而这个View就是我们在Activity中通过setContentView设置的View。那么目标事件就会传递给了子View。

从这里就知道Activity把事件传递给了顶级View。顶级View也称根View,通常这个View是ViewGroup。

顶级VIew对事件的分发过程

从上述中,事件已经传递给了DecorView,而DecorView是继承于FrameLayout,FrameLayout又继承于ViewGroup。处理事件的分发原理就可以从 ViewGroup 中找到。

ViewGroup # dispatchTouchEvent

我们逐步来看这个方法。

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

....
newTouchTarget = addTouchTarget(child, idBitsToAssign);
....


private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

从上面可以知道,ViewGroup中 onInterceptTouchEvent 是否被调用,看的是事件的传递是否为MotionEvent.ACTION_DOWN 或者 mFirstTouchTarget != null 。 MotionEvent.ACTION_DOWN 这个很好判断。至于mFirstTouchTarget != null 要从后面的代码分析 。 从后面的代码中可以看到,当事件由ViewGroup的子元素处理成功时,在addTouchTarget方法中,mFirstTouchTarget就指向这个子元素。所以一旦当前ViewGroup拦截了这个事件,不会再调用onInterceptTouchEvent 方法 ,那么这一系列的事件将不再进行分发,全部交给ViewGroup处理。

当然除了上面 if 的两个条件,影响是否 调用 onInterceptTouchEvent 方法的就是 FLAG_DISALLOW_INTERCEPT 标志位。 这个标记位是通过requestDisallowInterceptTouchEvent 方法来设置的,一般用在子View中。 FLAG_DISALLOW_INTERCEPT 标志位 一旦设置后,那么除了ACTION_DOWN事件外,其他事件都被拦截掉了。Why?因为在ViewGroup进行事件分发的时候,如果是ACTION_DOWN事件,那么就会重置 FLAG_DISALLOW_INTERCEPT ,将导致子View中设置这个标志位无效。因此,我们可以利用这个来解决滑动冲突。

当ViewGroup不再拦截的时候,就会继续执行下面的代码:

ViewGroup # dispatchTouchEvent

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

    .....

    resetCancelNextUpFlag(child);
    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();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }

    ....

}

ViewGroup # dispatchTransformedTouchEvent

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

再从上面代码可以清楚的看到,ViewGroup就会遍历所有的子View。拿到子View的时候,源码中做了几个判断(上述省略了),判断这个子View是否符合条件。如果不满足则直接选取下一个子View。 拿到符合条件的子View,那么将调用 dispatchTransformedTouchEvent 这个方法。 在dispatchTransformedTouchEvent方法中就调用了child.dispatchTouchEvent(event)。接下来就把事件分发到了子View身上了。

如果遍历了所有的子View事件都没有被合适的处理,可能有以下两种情况:

  1. ViewGroup没有子View
  2. 子View处理了点击事件,但是在 onTouchEvent 中返回了false。

在这两种情况下,那么ViewGroup就会处理自己的点击事件。源码如下:

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

从上面的dispatchTransformedTouchEvent中的第三个参数原本为Child,而现在改为null ,那么 将调用 super.dispatchTouchEvent(event) 这个方法,交给自己的View处理。

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

    return result;
}

从View对事件的分发来说,首先会判断有没有设置 OnTouchListener,如果OnTouchListener中的onTouch返回true,那么onTouchEvent就不会被调用,可见OnTouchListener 的优先级高于onTouchEvent。

下面来看看 onTouchEvent 的源码:

View # onTouchEvent

 if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    return (((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}

上面源码注释写的很清楚了,当View处于不可用的状态下,点击事件照样会消耗点击事件。

继续接下来:

View # onTouchEvent

if (((viewFlags & CLICKABLE) == CLICKABLE ||
    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
    case MotionEvent.ACTION_UP:
        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

            ....
    
            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();
                    }
                    if (!post(mPerformClick)) {
                        performClick();
                    }
                }
            }
    
            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;
    .....
    }
    .....
    
    return true;
}

从上面if的条件可以看出来,只用View的CLICKABLE和LONG_CLICKABLE有一个为true,那么就会消耗这个事件,既onTouchEvent返回true。

从case MotionEvent.ACTION_UP : 和 注解 Only perform take click actions if we were in the pressed state

从上面也可以看到,当View接受到了Down 和 Up 事件的时候 ,会触发performClick方法。这个方法内部其实就是调用了onClick方法。既实现了View 的点击事件。

 public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

可以得出以下的结论:

  1. View的enable 属性不影响onTouchEvent的默认返回值,哪怕是一个View是disable状态,只要它的clickable或者longClickable有一个为true,那么他的onTouchEvent就返回true。
  2. onClick会发生前提是View是可以点击的,并且他收到了down 和 up 事件。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容