博客
关于我
深入理解ViewGroup的dispatchTouchEvent()
阅读量:661 次
发布时间:2019-03-15

本文共 18271 字,大约阅读时间需要 60 分钟。

几点解读

1. mFirstTouchTarget

  • 它会在DOWN事件中被处理,如果视图存在可消费事件的child,则它将被赋值,记录该child
  • DOWN事件处理完成后, 如果 mFirstTouchTarget == null ,代表 此视图不含有Child 或者 Child没有消费事件,
    它实际代替了 是否存在child消费事件的 布尔值

2. (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

  • parent.requestDisallowInterceptTouchEvent()与之关联 true: 禁止父布局拦截 false: 允许父布局拦截
  • parent.requestDisallowInterceptTouchEvent(true) 设置后,会跳过parent对于onInterceptTouchEvent()的判断,直接放弃对此次事件的拦截
  • 由于在Down事件中,该值会被重置,所以生效时一定是在处理非Down事件(例如Move事件)
  • parent.requestDisallowInterceptTouchEvent(false) 设置后,如果本身的onInterceptTouchEvent()也标记拦截,则parent会给child派发 CANCEL 事件,并将parent的mFirstTouchTarget的值置为记录的TouchTarget的next(正常事件下为null),自此,该child及其子View 在 本次系列事件 中将不再接收事件;事件进入parent的mFirstTouchTarget为null时的判断,由parent来处理此事件

如果子View在处理Move事件时,在某些条件下,需要将事件交由父View处理, 可应用此知识点

  1. 将父布局设置为Down事件不拦截(即默认状态),事件传递给子View处理,并随即设置父布局为拦截状态
  2. 子View接收到事件后,设置父布局不允许拦截,使得父布局的拦截方法失效,保证子View可持续接收事件
  3. 当子view不在需要接收事件时,设置父布局允许拦截(因为此时事件不再是Down事件),事件则开始被父布局拦截并消费

3. 查看父布局是否允许拦截,如果允许,查看自身是否拦截

  • 它会发生在触摸事件的每一个阶段
//正常情况下,触摸事件的各个状态都会执行此判断 ;因为大多数情况下,下面这两个条件 总有一个是成立的 if (         //如果是按下事件         actionMasked == MotionEvent.ACTION_DOWN ||                 //不是按下事件,且记录了下一个可消费View(即 存在触摸目标)                 //此时正常情况下,事件来到了 Move 等状态                 mFirstTouchTarget != null) {        //查看子view是否允许父布局拦截     //此处如果是DOWN事件,则一定 允许拦截,因为 FLAG_DISALLOW_INTERCEPT 标记会在上面被重置     final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;     //当父布局允许拦截时,查看是否需要拦截     if (!disallowIntercept) {            //通过onInterceptTouchEvent查看是否需要拦截         intercepted = onInterceptTouchEvent(ev);         ev.setAction(action); // restore action in case it was changed     }     //当父布局不允许拦截时,则直接不拦截,不再关心onInterceptTouchEvent()方法的返回值     else {            intercepted = false;     } } //如果不是 DOWN 事件,且也没有 下一个触摸目标,则此时应该直接拦截 else {        intercepted = true; }

4. 在Down事件中,如何遍历Child查出可以消耗事件的View

//如果子View不可接受点击 || 点击的点不在子View的范围内,跳过此次循环...if (        //canReceivePointerEvents():  确保子View可以接收事件        !child.canReceivePointerEvents()        //isTransformedTouchPointInView : 如果子视图在转换到它的坐标空间时包含指定的点,则返回true。        || !isTransformedTouchPointInView(x, y, child, null)) {       ev.setTargetAccessibilityFocus(false);    continue;}...

5. 查出可消耗事件的View后,如何进行分发

  • 该方法在没找到可消耗事件View时,也会被调用,此时Child参数为null
  • 该方法在通过 TouchTarget 找到下级可消费View时也会被调用,Child参数为 TouchTarget.child
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,            View child, int desiredPointerIdBits) {           final boolean handled;        //此处单独处理取消事件        //因为取消事件比较特殊,不需要执行转换和过滤操作,重要的是动作本身,而不是内容        final int oldAction = event.getAction();        //如果是取消事件        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {               event.setAction(MotionEvent.ACTION_CANCEL);            //不包含child,交由 父类(View类)处理            if (child == null) {                   handled = super.dispatchTouchEvent(event);            }            //包含child, 向child分发事件,并等待接收返回值            else {                   handled = child.dispatchTouchEvent(event);            }            event.setAction(oldAction);            return handled;        }        //计算要交付的指针数量        final int oldPointerIdBits = event.getPointerIdBits();        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;        //在特殊情况下,有可能产生没有指针的触摸事件,如果发生了,则删除此事件        if (newPointerIdBits == 0) {               return false;        }        //如果 新旧触摸 相同, 则不需要执行任何奇特的不可逆转换 (即 下面的这一步 :transformedEvent.transform(child.getInverseMatrix());)        //只需要小心地恢复我们所做的任何更改,我们就可以为这个分派重用motion事件。而不必复制一份        final MotionEvent transformedEvent;        if (newPointerIdBits == oldPointerIdBits) {               if (child == null || child.hasIdentityMatrix()) {                   if (child == null) {                       handled = super.dispatchTouchEvent(event);                } else {                       final float offsetX = mScrollX - child.mLeft;                    final float offsetY = mScrollY - child.mTop;                    event.offsetLocation(offsetX, offsetY);                    handled = child.dispatchTouchEvent(event);                    event.offsetLocation(-offsetX, -offsetY);                }                return handled;            }            transformedEvent = MotionEvent.obtain(event);        } else {               transformedEvent = event.split(newPointerIdBits);        }        // 执行必要的转换和分派        if (child == null) {               handled = super.dispatchTouchEvent(transformedEvent);        } else {               final float offsetX = mScrollX - child.mLeft;            final float offsetY = mScrollY - child.mTop;            transformedEvent.offsetLocation(offsetX, offsetY);            //如果没有转换,则执行转换            if (! child.hasIdentityMatrix()) {                   transformedEvent.transform(child.getInverseMatrix());            }            //事件分发给child            handled = child.dispatchTouchEvent(transformedEvent);        }        // Done.        transformedEvent.recycle();        return handled;    }

6. 如何将 可消费事件的View 记录给 TouchTarget对象(mFirstTouchTarget )

//将此View标记为触目目标(此事件集 的 下次事件 会直接找到此触目目标而无需遍历)newTouchTarget = addTouchTarget(child, idBitsToAssign);

完整代码解析

@Override    public boolean dispatchTouchEvent(MotionEvent ev) {           //检验输入事件的一致性        if (mInputEventConsistencyVerifier != null) {               mInputEventConsistencyVerifier.onTouchEvent(ev, 1);        }        // If the event targets the accessibility focused view and this is it, start        // normal event dispatch. Maybe a descendant is what will handle the click.        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {               ev.setTargetAccessibilityFocus(false);        }        //定义最终的返回值        boolean handled = false;        //过滤触摸事件, 确定触摸的view和window没有被遮挡        if (onFilterTouchEventForSecurity(ev)) {               final int action = ev.getAction();            //通过掩码方式重新生成该动作, 规避在代码执行该过程中 动作指针发生变化,确保当前处理的是一个触摸动作            final int actionMasked = action & MotionEvent.ACTION_MASK;            //如果是 
<按下的动作>
,则重置状态和标志 if (actionMasked == MotionEvent.ACTION_DOWN) { //通过mFirstTouchTarget ,为 记录的可消费触摸事件 的 view链 传递取消事件 ,并清空mFirstTouchTarget cancelAndClearTouchTargets(ev); //重置一些标记状态,比如 FLAG_DISALLOW_INTERCEPT 标记, 它会使之前设置的 "父布局是否拦截事件" 失效 resetTouchState(); } // 检查是否要拦截事件 final boolean intercepted; //正常情况下,触摸事件的各个状态都会执行此判断 ;因为大多数情况下,下面这两个条件 总有一个是成立的 if ( //如果是按下事件 actionMasked == MotionEvent.ACTION_DOWN || //不是按下事件,且记录了下一个可消费View(即 存在触摸目标) mFirstTouchTarget != null) { //查看子view是否允许父布局拦截 //此处如果是DOWN事件,则一定 允许拦截,因为 FLAG_DISALLOW_INTERCEPT 标记会在上面被重置 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //当父布局允许拦截时,查看是否需要拦截 if (!disallowIntercept) { //通过onInterceptTouchEvent查看是否需要拦截 intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } //当父布局不允许拦截时,则直接不拦截,不再关心onInterceptTouchEvent()方法的返回值 else { intercepted = false; } } //如果不是 DOWN 事件,且也没有 下一个触摸目标,则此时应该直接拦截 else { intercepted = true; } // If intercepted, start normal event dispatch. Also if there is already // a view that is handling the gesture, do normal event dispatch. if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } //记录是否是取消事件 (事件之前被取消了 || 事件本事是取消事件) final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // 查看是否需要在适当时将事件切分为多个 (多指操作) final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; //记录新的触摸目标 TouchTarget newTouchTarget = null; //标记已经 找到了新的 触摸目标 boolean alreadyDispatchedToNewTouchTarget = false; //如果事件没有被取消 && 没有被拦截 if (!canceled && !intercepted) { //事件是否存在可及的view , 如果存在则返回此View (此处与分析关联不大) View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; // DOWN事件 / 多指DOWN事件 / 悬浮针移动 // (可以理解为此处只是在处理Down事件 ,所以没有必要查看是否记录了触摸目标 mFirstTouchTarget) if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { //获取事件角标(当为down事件时,恒为0) final int actionIndex = ev.getActionIndex(); // always 0 for down //获取事件的指针id final int idBitsToAssign = split ? //如果是多指需要切分 1 << ev.getPointerId(actionIndex) //否则返回固定值 : TouchTarget.ALL_POINTER_IDS; //清除此 指针id 之前标识的 触摸目标,防止不同步问题 removePointersFromTouchTargets(idBitsToAssign); //记录子view的个数(也是为了防止多线程问题) final int childrenCount = mChildrenCount; // 如果存在子View (此处的newTouchTarget肯定为null) if (newTouchTarget == null && childrenCount != 0) { //获取触摸事件的坐标 final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); //创建一个包含特定排序的子视图的列表 (因为这在一个递归循环中,不应该直接创建对象,而是采用一个预留的数组,并在使用后清空它) final ArrayList
preorderedList = buildTouchDispatchChildList(); //标记是否具有自定义排序 final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); //记录children数组 final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { //安全的取出每一个子View final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); //如果存在视图 具有 可访问性焦点 ,我们应该首先处理它,并从子View中找到此视图 //如果不存在这种视图,则执行正常的事件分发工作 //此处与下方的子view的找寻工作是两套循环 if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } //如果子View不可接受点击 || 点击的点不在子View的范围内,跳过此次循环 if ( //canReceivePointerEvents(): 确保子View可以接收事件 !child.canReceivePointerEvents() //isTransformedTouchPointInView : 如果子视图在转换到它的坐标空间时包含指定的点,则返回true。 || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //如果子View 可接受点击 且 坐标空间包含点击的坐标,将其转换为 触摸目标对象 //由于此处mFirstTouchTarget因为是Down事件被清空,所以此处 newTouchTarget应该为null newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { //如果 newTouchTarget 不为null,则说明此 子View 已经开始接收触摸事件 //此处终止循环, 并在它正在处理的指针之外,给它一个新的指针 newTouchTarget.pointerIdBits |= idBitsToAssign; break; } //重置子View被设置取消的标志 ; 如果之前真的设置过,则返回true resetCancelNextUpFlag(child); if (//对此 子View 执行正常的事件分发 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) ) { //如果条件返回true,标明 存在child 消费事件 // 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标记为触目目标(此事件集 的 下次事件 会直接找到此触目目标而无需遍历) 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); } if (preorderedList != null) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // 如果mFirstTouchTarget 为null ,代表 此视图 不含有Child ,或者 Child没有消费事件, // 它在此处代替了 是否存在child消费事件的 布尔值 if (mFirstTouchTarget == null) { //如果mFirstTouchTarget 为null时,代表 此视图 不含有Child ,或者 Child没有消费事件,等同于一个图片 //此处的child传值很关键,它会使此布局直接调用了 super.dispatchTouchEvent(), handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } //如果mFirstTouchTarget不为null ,则说明存在 最终可消费事件的View链 并存储在了 mFirstTouchTarget 中 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; //迭代保存的 最终可消费事件的View链 while (target != null) { final TouchTarget next = target.next; //如果已经处理了并确定处理的是此视图,则跳过此次循环,直接返回true (在上方的Down事件中我们进行了处理) if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } //如果还没处理 (此处事件已经不是Down事件,或许是Move事件等), // 此时,我们应该直接通过记录的 触摸标记,直接找到下一级可消费事件的child else { //需要查看是否要取消事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //通过 触摸标记 中记录的,直接找到下一个 可消费的child进行分发 if (dispatchTransformedTouchEvent(ev, cancelChild, //记录的可消费的View和它的指针id target.child, target.pointerIdBits)) { //如果可消费,继续向上层返回true handled = true; } //当事件取消时,mFirstTouchTarget指向了next(普通情况下为null); //即当move事件中当父布局开始拦截事件时,也会进行此次处理 if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } // Update list of touch targets for pointer up or cancel, if needed. if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }

转载地址:http://iwamz.baihongyu.com/

你可能感兴趣的文章
MySQL 命令和内置函数
查看>>
mysql 四种存储引擎
查看>>
MySQL 在并发场景下的问题及解决思路
查看>>
MySQL 基础架构
查看>>
MySQL 基础模块的面试题总结
查看>>
MySQL 备份 Xtrabackup
查看>>
mYSQL 外键约束
查看>>
mysql 多个表关联查询查询时间长的问题
查看>>
mySQL 多个表求多个count
查看>>
mysql 多字段删除重复数据,保留最小id数据
查看>>
MySQL 多表联合查询:UNION 和 JOIN 分析
查看>>
MySQL 大数据量快速插入方法和语句优化
查看>>
mysql 如何给SQL添加索引
查看>>
mysql 字段区分大小写
查看>>
mysql 字段合并问题(group_concat)
查看>>
mysql 字段类型类型
查看>>
MySQL 字符串截取函数,字段截取,字符串截取
查看>>
MySQL 存储引擎
查看>>
mysql 存储过程 注入_mysql 视图 事务 存储过程 SQL注入
查看>>
MySQL 存储过程参数:in、out、inout
查看>>