【view】- 触摸事件分发(2)

简介

上一篇文章【view】- 触摸事件分发(1)讲解了从底层到上层的触摸事件传递。这篇文章将具体Activity,View组件的触摸事件的传递,以及事件的分类,拦截,不同的处理会引起触摸事件流程那些变化等。

注意

如果对源码分析不感兴趣,只想知道结论,可以直接翻到文章最后看总结,如果想了解源码实现,可以参照这篇文章,自己追踪源码。

Activity

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

如果是按下事件,在进行事件分发前调用onUserInteraction()方法,在Activity该方法是没有任何实现,我们可以重新该方法。

getWindow().superDispatchTouchEvent(ev)会把触摸事件交给整个View组件之间传递,如果返回true,在Activity中触摸事件传递结束,如果反复false者调用onTouchEvent(MotionEvent event)方法。

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

Activity中逻辑很简单,下面讲解在DecorView顶层View之间的触摸事件传递。

DecorView

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

调用父类ViewGroup中的dispatchTouchEvent(MotionEvent ev)

辅助功能,残障。

if (mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
    ev.setTargetAccessibilityFocus(false);
}

如果窗口是被遮盖,不可见,onFilterTouchEventForSecurity返回false,放弃这次触摸事件的处理。

public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
        return false;
    }
    return true;
}

如果是第一次按下操作,清除所有以前的状态,由于应用程序切换,ANR或某些其他状态更改,框架可能已放弃上一个手势的上移或取消事件,所以进行一些清理操作。

if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

int类型mGroupFlags变量的初始化,我把常量换成具体的值

rivate void initViewGroup() {
   ...
   mGroupFlags |= 0x1; 
   mGroupFlags |= 0x4;
   mGroupFlags |= 0x10
   mGroupFlags |= 0x40
   mGroupFlags |= 0x4000
   if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB) {
       mGroupFlags |=  0x200000
   }
   setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
   ...
public void setDescendantFocusability(int focusability) {
   ...
    mGroupFlags &= ~0x60000;
    mGroupFlags |= (0x20000 & 0x60000);
}

检查拦截,如果是MotionEvent.ACTION_DOWN或者mFirstTouchTarget不等于null,调用onInterceptTouchEvent方法,判断当前View是否要要拦截触摸事件。

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

如果被拦截,开始正常事件分发。另外,如果已经有一个正在处理手势的视图,则进行常规事件调度。

if (intercepted || mFirstTouchTarget != null) {
    ev.setTargetAccessibilityFocus(false);
}

如果事件没有撤销并且没有拦截,进入if代码块,把触摸事件分发给子控件。

获取手指索引,从注释看出,对于按下操作actionIndex ==0,单指操作。

final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

对子控件进行排序,因为控件会存在相互叠加的部分,优先顶层

 final ArrayList<View> preorderedList = buildTouchDispatchChildList();

从顶层View,也就是最先收到触摸事件的View向下遍历,获取子控件。

for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
    ...    
}

将不可见,在动画中,手指在控件之外不能接收触摸事件的控件过滤掉

if (!canViewReceivePointerEvents(child)
        || !isTransformedTouchPointInView(x, y, child, null)) {
    ev.setTargetAccessibilityFocus(false);
    continue;
}

将触摸事件分发给符合要求的子控件。

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))

在dispatchTransformedTouchEvent方法中,如果触摸事件已经取消或者撤销了,那么调用父类dispatchTouchEvent方法。

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

分发事件给子控件,调用子控件的dispatchTouchEvent方法

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

如果子控件没有重写这个方法,那么会调用到View的dispatchTouchEvent方法。接下来分析View中的dispatchTouchEvent方法。

如果是按下操作,停止嵌套滚动

if (actionMasked == MotionEvent.ACTION_DOWN) {
    stopNestedScroll();
}

调用控件的onTouchEvent方法

if (!result && onTouchEvent(event)) {
    result = true;
}

看一下View中的onTouchEvent方法。

如果设置了委托触摸事件处理实例,者直接调用委托的onTouchEvent。

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {return true;}
}

执行控件的点击事件监听回调中的onClick方法。

if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    ...
        if (mPerformClick == null) {mPerformClick = new PerformClick();}
        if (!post(mPerformClick)) {performClickInternal();}
    }
}

View中的onTouchEvent方法剩余的逻辑自己分析,不在讲解。

回调ViewGroup中的dispatchTouchEvent方法中。如果dispatchTransformedTouchEvent方法返回true,代表被子空间或者父类处理了触摸事件,那么退出循环,不在分发事件给下一个子控件。

总结

下面容器以ViewGroup为中dispatchTouchEvent讲解,默认其它容器没有重写该容器里的方法。

ACTION_DOWN
  • 第一次触发down事件

    1. 如果当前触摸事件窗口是被遮盖(比如有其它可见窗口遮挡在上面),即onFilterTouchEventForSecurity返回false,那么丢弃该次触摸事件处理,handled=false,返回handled。把事件交给Activity处理。

    2. onFilterTouchEventForSecurity返回true,即当前控件可以对触摸事件进行处理。
      当前控件是否允许拦截触摸事件。

      boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
      

      没有其它设置的话,此时mGroupFlags值是0x244053,&0x80000等于0,所以disallowIntercept值是false,表示ViewGroup默认允许拦截触摸事件,但是需要询问ViewGroup的onInterceptTouchEvent是否可以拦截,如果不重写该方法,返回false,依旧表示ViewGroup不拦截事件。

      正常情况下,变量canceled = false,split = true。遍历容器中所有的子控件,将触摸事件分发给子控件。在分发dispatchTransformedTouchEvent方法中,如果传入的View是null,者调用ViewGroup的父类的dispatchTouchEvent,如果不是null,调用子控件的dispatchTouchEvent,并返回dispatchTouchEvent返回的值。

      如果子控件中有消耗了触摸事件(即dispatchTouchEvent返回true),者停止遍历,后面的子控件将接收不到该触摸事件并创建TouchTarget对象target,将消耗事件的View和手指编号存在这个target中,且target.next = mFirstTouchTarget,将新创建的target赋值给mFirstTouchTarget并返回target。

      这个时候其实newTouchTarget和mFirstTouchTarget是同一个实例对象,且alreadyDispatchedToNewTouchTarget赋值为true。

      根据上面,可以得知handled被直接赋值为true,而下一个target是null,while循环终止。

      最后直接返回handled值。

      如果子控件没有消耗,后面的逻辑更简单,直接交给容器的父类处理。

      如果容器一开始要拦截该触摸事件呢?即intercepted被赋值为true,那么mFirstTouchTarget也会为null,直接将事件交给父类处理,返回父类处理的结果。

      • 父类处理触摸事件
        这里ViewGroup的父类是View。首先停止嵌套滚动,如果View符合接收触摸事件的要求,调用View的onTouchEvent方法,如果消耗了事件,result赋值true,然后返回result值。

        onTouchEvent方法中,如果View的可点击属性没有使能,直接返回clickable(是否点击使能)值。

        如果设置的委托事件处理实例,将触摸事件交给委托实例,然后直接返回true。

        后面的逻辑就会执行点击事件,回调点击事件监听方法,包括点击,长按等,最后返回true。所以有些时候,重写了onTouchEvent发现点击事件不能回调问题。

  • 第二次触发down事件
    比如在界面滑动等,可能会触发第二次down,第二次由于

    newTouchTarget = getTouchTarget(child);
    

    返回不为null,所以会直接终止循环。

    if (newTouchTarget != null) {
      ...
      break;
    }
    
ACTION_UP

按下并松开手指,这时候触发ACTION_UP事件。通过上面的分析这时候,如果ViewGroup在ACTION_DOWN不拦截,那么mFirstTouchTarget不等于null,那么不管现在ViewGroup是否拦截,不好意思,我都只会把触摸事件交给上一个消耗了ACTION_DOWN事件的View,如果都不消耗,我给Activity。

总结:如果你连ACTION_DOWN事件都不消耗,那么后面的事件序列你也别想消耗。

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

推荐阅读更多精彩内容