【Android】事件分发机制源码解析

1. 分发顺序

Activity.dispatchTouchEvent()ViewGroup.dispatchTouchEvent()ViewGroup.onInterceptTouchEvent()View.dispatchTouchEvent()View.onTouchEvent()

2.源码分析

2.1 Activity中的分发流程

dispatchTouchEvent

首先事件进入Activity.dispatchTouchEvent()

事件链:指由ACTION_DOWN开始,途经≥0个ACTION_MOVE,最终结束于ACTION_UPACTION_CANCEL

有特殊情况,可能存在一次事件链中有两个ACTION_DOWN,这涉及到多指操作了,详情可以查看这篇文章:《每日一问 很多书籍上写:“事件分发只有一次 ACTION_DOWN,一次 ACTION_UP”严谨吗?》。大概是这样的:在ViewGroup中收到ACTION_POINTER_DOWN,即手指按下,但是该手指按下的View不是之前的View,因此会在分发过程中变成ACTION_DOWN,然后给到View。

下面开始正式的源码分析:

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 一条事件链的开始
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();// 该方法为空实现
    }
    
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

排除第一个if语句,直接看第二个。

getWindow()返回的是一个Window对象,而Window对象是一个抽象对象,其唯一实现是PhoneWindow,因此这里返回的就是一个PhoneWindow。那么继续去看看PhoneWindow.superDispatchTouchEvent()

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

mDecor是一个DecorView,那么继续跟进。

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

调用了父类的dispatchTouchEvent()。根据源码可以知道,DecorView是继承自FrameLayout的。那么这里也就相当于调用了FrameLayout.dispatchTouchEvent()。然而在FrameLayout中没有找到对应的方法,那么继续找FrameLayout的父类,也就是ViewGroup。此处留待后面分析ViewGroup的分发的时候再详看,目前先假设ViewGroup返回了false,即不消费该事件,那么该事件将由Activity进行消费。

onTouchEvent

现在回到最初的入口,看看后续代码,onTouchEvent()

/**
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

根据源码的注释中可以看到,如果没有重写该方法,默认是返回false的,即该事件没有被消费。但是为了刨根问底,还是看看mWindow.shouldCloseOnTouch()方法。

首先在PhoneWindow中没有找到该方法,猜测是在抽象类Window中。

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}

通过同时满足以下几个条件来返回true:

  1. mCloseOnTouchOutside为true

  2. decorView不为null

  3. 当前事件为ACTION_OUTSIDE或当前事件为ACTION_DOWN但是超出边界

然而通过源码发现,mCloseOnTouchOutside默认下是为false的,意味着在默认情况下条件无法同时满足,因此一直返回false。

至此,在Activity中,一个事件的传递结束了。

总结

Activity首先将事件通过Window传递给DecorView,然后DecorView通过默认的ViewGroup.dispatchTouchEvent()进行事件分发并返回结果。默认情况下,如果没有任何ViewGroupView消费事件,那么该事件最后会去到Activity.onTouchEvent(),方法中没有对事件进行任何操作,相当于忽略了这个事件。

下面继续看看,什么时候ViewGroup.dispatchTouchEvent()返回true什么时候返回false。

2.2 ViewGroup中的分发流程

dispatchTouchEvent

在上面的分析中提到,事件第一次到ViewGroup是调用到了dispatchTouchEvent()这个方法。

整个方法比较长,这里慢慢来分析。

@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;
    // onFilterTouchEventForSecurity 当视图或window被隐藏、遮挡时返回false
    if (onFilterTouchEventForSecurity(ev)) {
        ...暂时省略部分代码
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

首先整体的看这个方法,在关键代码(我省略的那部分)的前后,都进行了一些验证性的动作,先不关注。首先涉及到事件分发的第一个关键方法,onFilterTouchEventForSecurity(),这个方法主要是判断当前View和当前window是否被遮挡或隐藏,如果是的话则返回false。也就意味着,如果view被隐藏、遮挡并且触发事件所在的window也遮挡、隐藏,那么事件就不会继续进行传递了,直接返回了false。

而根据前面的分析可以知道,返回false之后,事件会传递到Activity.onTouchEvent()中,而在其中并没有对事件进行消费处理,因此事件链就这样结束了。

现在,按照正常流程,分析下前面省略的关键代码。

final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// Handle an initial down.
// 翻译:处理初次按下
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    // 翻译:开始新的触摸手势时,放弃所有先前的状态。
    // 由于应用程序切换,ANR或某些其他状态更改,框架可能已放弃
    // 上一个手势的抬起或取消事件。

    // 主要是清空mFirstTouchTarget,清空前会向之前的接收事件的view发送一个cancel事件
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

如果是ACTION_DOWN的话,就先清空之前的接收事件的目标以及触摸状态等。调用的两个方法,主要看cancelAndClearTouchTargets()

private void cancelAndClearTouchTargets(MotionEvent event) {
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        if (event == null) {
            final long now = SystemClock.uptimeMillis();
            event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
            syntheticEvent = true;
        }

        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            resetCancelNextUpFlag(target.child);
            // 向之前的接收事件的view发送一个cancel事件
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
        clearTouchTargets();//将mFirstTouchTarget设置为null

        if (syntheticEvent) {
            event.recycle();
        }
    }
}

先进行了一个判断,由于ACTION_DOWN是一个事件链的开始,因此这里的mFirstTouchTarget是上一条事件链的目标View。注意看,如果传入的事件为空的话,则会构建一个ACTION_CANCEL事件。然后遍历目标View去分发该事件,另外需要注意的是分发事件的方法dispatchTransformedTouchEvent(),第二个参数这里传的是true。继续跟进,查看是如何分发事件的。

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

        // child为null时,调用view.dispatchTouchEvent
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ...省略了部分代码
}

这里我省略掉后面分发正常事件的代码。这里可以看到,cancel也就是方法的第二个参数,在上面传过来的时候是传的true,因此这里会进入这个if语句。

在这里,再次将事件设置成ACTION_CANCEL,然后判断是否有子View,如果有则向子View传递事件,否则将当前ViewGroup当成一个View来处理事件。super.dispatchTouchEvent(event)即调用View.dispatchTouchEvent(event),因为ViewGroup是继承自View的,也就相当于让当前ViewGroup来自己处理这个事件。这个后续再进行分析。

现在回到ViewGroup.dispatchTouchEvent(),知道当遇到ACTION_DOWN时,ViewGroup会向上一条事件链的目标发送ACTION_CANCEL事件,然后将mFirstTouchTarget置为null。

继续向下看。

// Check for interception.
// 检查是否需要拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {// 事件链开始或之前有消费事件的View
    // 这里是判断是否允许拦截事件,可以用requestDisallowInterceptTouchEvent()
    // 来进行设置,默认情况下disallowIntercept=false
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        // 这里先判断是否需要将事件拦截下来自己处理,默认返回false
        // 这里也就是事件分发顺序中dispatchTouchEvent→onInterceptTouchEvent的体现
        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;
}

这一段主要是判断是否要拦截当前事件,也是在这里,将事件传递到了ViewGroup.onInterceptTouchEvent()中。注意一下,调用拦截方法是有条件的。

onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        // 这里的判断是,鼠标按住左键拖动滚动条,所以要拦截事件
        return true;
    }
    return false;
}

默认情况下,是返回false的,也即是不拦截事件。上面返回true的特殊情况不考虑,毕竟很少出现用鼠标的情况。

OK,现在继续回到ViewGroup.dispatchTouchEvent()中查看后面的代码。

// Check for cancelation.
// 检查是否取消
final boolean canceled = resetCancelNextUpFlag(this)
        || actionMasked == MotionEvent.ACTION_CANCEL;

// Update list of touch targets for pointer down, if needed.
// 翻译:如有必要,更新触摸目标列表以使指针向下。
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;// 新的接收事件的目标
boolean alreadyDispatchedToNewTouchTarget = false;// 标记事件是否已经分发

// 下面这个if只给down事件进入的
if (!canceled && !intercepted) {
    // 这里是找到当前已经获得焦点的View,下面会用到
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;

    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        
        ...忽略部分关键代码

    }
}

首先进行了是否取消的检查,然后开始了第一个比较关键的代码块了,由于代码过长,所以这里先省略掉了。省略的那部分关键代码,进行了两个if的判断,首先假设第一个if通过,即该事件即不取消也不拦截。那么进入第二个代码判断,满足一下条件之一即可进入:

  1. ACTION_DOWN事件
  2. 允许多指操作且事件为ACTION_POINTER_DOWN(手指按下)
  3. ACTION_HOVER_MOVE事件,不属于常见情况。

下面开始分析第一个关键代码块了。这里假设进入的条件是满足了ACTION_DOWN

...省略部分代码
for (int i = childrenCount - 1; i >= 0; i--) {
    // 倒序的方式遍历,优先获取到新加入的View
    // 这里是可以改变获取view的顺序的,详细的话可以查看getAndVerifyPreorderedIndex
    // 和getAndVerifyPreorderedView
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    // 如果前面找到了当前获取焦点的View,那么将事件优先传递给它
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;// 将循环重置,防止由于优先级的问题错过了前面的view		
    }

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        // 该子View不能接受event事件:visibility!=visiable或animation=null
        // 或者事件没有落在子view的范围内
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    // 查找子view是否在之前的触摸目标内,由于这里假设给down
    // 事件进入,因此这里获取肯定没有的,为null
    // 如果是多指的话,那么这里可能不为null
    newTouchTarget = getTouchTarget(child);

    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // 翻译:子View已经在其范围内接触了。
        // Give it the new pointer in addition to the ones it is handling.
        // 翻译:除了要处理的手指外,还要为其提供新的手指。
        
        // 能进入到这里,表示是有新的手指按下了
        // 那么下面的分发代码就不走了
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);

    // 调用dispatchTransformedTouchEvent把事件分发给子View
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // true表示子View消费了事件
        ...省略部分代码
            
        // 这里为mFirstTouchTarget赋值了
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
}
...省略部分代码

使用一个倒序的for循环遍历子View,调用canViewReceivePointerEvents()判断子View是否能有接收事件的能力,调用isTransformedTouchPointInView()判断事件是否落在子View中。

private static boolean canViewReceivePointerEvents(@NonNull View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}

通过这个方法可以知道,如果子View不是VISIBLE且没有设置动画的情况下,是不能接收事件的。

找到目标子View之后,调用dispatchTransformedTouchEvent()进行事件分发,这里需要注意,第二个参数为false,并且child是不为null的。

现在回到之前分析过的dispatchTransformedTouchEvent()方法,这次将会执行后面的正常分发逻辑。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    
    ...省略ACTION_CANCEL代码
    
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {// 不是新手指按下的事件
        if (child == null || child.hasIdentityMatrix()) {
            // hasIdentityMatrix用于判断view是否进行过矩阵变换
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                // 对事件的坐标进行偏移,因为这是相对坐标
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);

                // 事件分发给子View的dispatchTouchEvent
                // 体现了事件传递中的ViewGroup.dispatchTouchEvent→View.dispatchTouchEvent
                handled = child.dispatchTouchEvent(event);

                // 分发完成后将之前的偏移量移除
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    ...省略了部分代码

首先判断是否是多指事件,这里先假设为不是,也即newPointerIdBits == oldPointerIdBits条件成立。然后判断child是否为null或调用child.hasIdentityMatrix()判断是否进行过矩阵变换。这里需要关注的是child.hasIdentityMatrix()当使用过补间动画平移进行变化时,这个方法会返回false。那么这里的话依然假设没有进行过矩阵变换。

前面说到,这次传来的child是不为null的,那么就可以正常的进行事件分发了。直接调用了child.dispatchTouchEvent(),完成向子View的事件传递,并返回结果:子View是否消费事件。

下面假设是多指事件或者child进行过矩阵变化,看看后面的代码针对这种情况是如何进行事件分发的。

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

    handled = child.dispatchTouchEvent(transformedEvent);
}

可以很明显的看到,这里事件调用了transform()方法,并且传入的矩阵是child.getInverseMatrix()。根据方法名称可以猜到,这个方法返回的矩阵是执行变化前的,也就意味着获取到的是child本身的矩阵。这也就导致了事件的触发区域依然在之前的位置了。

现在回到前面,假设这里子View消费了事件,也即dispatchTransformedTouchEvent()返回了true,然后会调用addTouchTarget()mFirstTouchTarget赋值,然后构建一个TouchTarget对象,其nextmFirstTouchTargetchild为目标View,然后将该TouchTarget返回并赋值给newTouchTarget

到这里,关于ACTION_DOWN如何找到目标View就完成了。通过这里的分析,可以知道,一条事件链只有在ACTION_DOWNACTION_PONITER_DOWN的时候才会去找目标View,后续的事件将直接传递到目标View。

下面看看如果没有找到目标View或不是ACTION_DOWN事件会如何进行处理。

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    // 翻译:没有触摸目标,因此请将其视为普通视图。
    
    // 没有触摸目标,事件将会由默认的View.dispatch去处理
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    ...省略找到目标view后分发事件的代码
}

如果没有找到目标View,则开始分发事件,注意dispatchTransformedTouchEvent()的第三个参数,这里传的是null。回想前面看过的源码,可以知道,在方法经常数出现这么一个if语句:

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    ...省略部分代码
    handled = child.dispatchTouchEvent(event);
}
return handled;

因此可以知道,如果child即方法的第三个参数为null的情况下,是会将该ViewGroup当成普通的View去处理事件,即调用View.dispatchTouchEvent()

下面看看如何分发其他非ACTION_DOWN事件。

TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
    final TouchTarget next = target.next;
    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
        // down事件,并且被消费,那么就会进入到这里
        // 而且target.next是Null,所以在下次循环的时候就会退出
        handled = true;
    } else {
        // 如果拦截事件的话,这里为true
        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                || intercepted;
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)) {
            // 将事件分发到子view中
            handled = true;
        }
        if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();// 这里将target.child清空了,即没有目标view了
            target = next;
            continue;
        }
    }
    predecessor = target;
    target = next;
}

这里的话,使用循环去向target.child分发事件,不难理解。但是关键需要关注这一段代码:

if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();// 这里将target.child清空了,即没有目标view了
            target = next;
            continue;
        }

这段代码实现的是:当该ViewGroup拦截事件的话,那么会将之前的目标View给清空,也就导致后续分发事件的时候,target.child为null。并且mFirstTouchTarget最后也会变成null,因为next在最后的目标View肯定是null的。

总结

首先可以确定的是,具体进行事件分发的方法是dispatchTransformedTouchEvent(),而该方法的第三个参数就是目标View。如果为null的话,就会把事件交给当前ViewGroup去处理,调用当前ViewGroup的super.dispatchTouchEvent()

通过前面的源码分析可以得出以下结论:

  1. 如果当前ViewGroup拦截了事件链中的任何一次事件,那么该事件链后续的事件都会被拦截下来,即使onInterceptTouchEvent()返回false;并且后续的事件也不会再走onInterceptTouchEvent()
  2. 能接收事件的View要么是可见的(Visibility=Visible)要么设置了动画(getAnimation()!=null)。
  3. 对于执行过矩阵变换的View,会获取最初的矩阵去当做目标位置进行事件分发。这是为什么补间动画执行后原来的位置才能响应事件的原因。

至此,ViewGroup中的分发流程就走完了。其实可以看到,整个事件分发流程的主要就是在ViewGroup,因为View是不具备分发功能的,毕竟它下面没东西了。

2.3 View中的分发流程

在View中,关键方法是onTouchEvent(),如果没有重写该方法的话,默认下所有控件对事件的处理都是在这里执行的。如果说ViewGroup.dispatchTouchEvent()是事件分发流程中的主要步骤,那么View.onTouchEvent()就是事件处理的主要步骤,也是事件分发流程中最后的步骤。

尽管View不具备再将事件传递下去的资格,然而它依然有dispatchTouchEvent()方法。在方法中主要是确定事件该交由谁处理,是自己的onTouchEvent()处理还是给onTouchListener处理?

dispatchTouchEvent

和ViewGroup一样,上来首先判断当前事件是否要求传递给已经获取焦点的View。

if (event.isTargetAccessibilityFocus()) {
    // We don't have focus or no virtual descendant has it, do not handle the event.
    // 没有焦点,无法处理该事件
    if (!isAccessibilityFocusedViewOrHost()) {
        return false;
    }
    // We have focus and got the event, then use normal event dispatch.
    event.setTargetAccessibilityFocus(false);
}

如果当前事件要求传递给获取到焦点的View,但是当前View没有获取到焦点,那么直接返回false,无法处理事件。否则就进入正常的流程。

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopNestedScroll();
}

属于优化手感的代码,如果是ACTION_DOWN事件的话,则停止滚动。

下面是方法的关键代码,首先进行了一个判断当前View是否可以接收事件的判断,如果可以则进行分发。

if (onFilterTouchEventForSecurity(event)) {// 判断当前view和当前window是否能接收事件
    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)) {
        // 这里通知到onTouchListener,如果返回true则返回true
        result = true;
    }

    // 如果前面的listener返回了true的话,那么onTouchEvent也就不执行了
    if (!result && onTouchEvent(event)) {
        result = true;
    }
}

这里的话主要是将事件分发给了3个地方处理,首先给到handleScrollBarDragging(),根据名字可以知道这个是处理滚动条事件的,该方法只处理来自鼠标的事件,因此一般情况下都返回false。

然后将事件交给onTouchListener处理,并记录处理结果。

最后再判断,前面两者是否已经消费了事件,如果是的话则不再将事件分发给onTouchEvent(),否则再将事件交给onTouchEvent()去处理。

通过这里我们可以知道,如果我们在onTouchListener中返回了true,即消费了事件的话,那么该事件将不再进入View.onTouchEvent()。也就意味着无法处理点击、长按等事件处理了。

View.dispatchTouchEvent()中的关键逻辑就这么多,后面的代码不涉及到事件分发,因此就不继续分析了。下面去看看View.onTouchEvent()中,对事件的默认处理是怎么样的。

onTouchEvent

// 当前view是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {// 当前view不可用
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        // 抬起事件,且当前页面处于按压状态,则释放按压状态
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    // 翻译:可单击的禁用视图仍然消耗触摸事件,只是不响应它们。
    return clickable;
}

// 如果代理对象消费了事件,那么事件将不再进行默认处理
if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}
...省略默认处理的代码
    return false;

首先判断当前View是否允许点击,该变量在后面需要用到。

然后判断View是否是处于不可用状态,如果不可用的话则直接将是否可点击作为是否消费事件返回。也就是那段注释所说的:可单击的禁用视图仍然消耗触摸事件,只是不响应它们。

最后将事件交给代理去处理,如果代理处理了事件,那么事件分发到此结束。否则就继续往下,使用默认的实现对事件进行处理。

这里可以看到,如果需要干预事件的处理的话,可以对View设置一个代理,然后在代理中进行自己的处理逻辑。是否覆盖默认逻辑则取决于在代理中是否返回了true。

下面看看,View中默认处理事件的逻辑是怎么样的。

首先要注意的是,要进入默认处理的逻辑需要满足以下条件之一:

  1. 当前View可点击
  2. View.flag为TOOLTIP(长按或悬浮时会出现工具条提示)

一般来说满足第一个即可。然后会根据不同的动作进行处理。下面将按照ACTION_DOWNACTION_MOVEACTION_UPACTION_CANCEL来进行分析。

ACTION_DOWN
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
// 注意这里将这个变量设为false了,在ACTION_UP会用到这个变量
mHasPerformedLongPress = false;

if (!clickable) {
    // 不允许点击,那么这里就去触发长按事件
    // 前面知道,这里进入的条件只有两个,既然不满足可点击
    // 那么这里肯定就是长按的时候会出现工具条提示
    checkForLongClick(0, x, y);
    break;
}

if (performButtonActionOnTouchDown(event)) {
    // 一般情况下都返回false
    break;
}

// !!!注意,下面的代码只有在可点击的时候才会去执行!!!

// Walk up the hierarchy to determine if we're inside a scrolling container.
// 翻译:遍历层次结构以确定我们是否在滚动容器内。
boolean isInScrollingContainer = isInScrollingContainer();

// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
// 翻译:对于滚动容器内部的视图,如果滚动,则将按下的反馈延迟一小段时间。
if (isInScrollingContainer) {
    mPrivateFlags |= PFLAG_PREPRESSED;
    if (mPendingCheckForTap == null) {
        mPendingCheckForTap = new CheckForTap();
    }
    mPendingCheckForTap.x = event.getX();
    mPendingCheckForTap.y = event.getY();
    // 延迟100ms后再触发反馈,即else的代码
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
    // Not inside a scrolling container, so show the feedback right away
    // 立刻显示反馈
    setPressed(true, x, y);
    checkForLongClick(0, x, y);
}

ACTION_DOWN中,主要是涉及到了长按的逻辑。checkForLongClick()CheckForTap都是和长按有关的。它们都是通过向当前View的Handler发送一个Runnable去执行长按的。

private void checkForLongClick(int delayOffset, float x, float y) {
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        mHasPerformedLongPress = false;

        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        mPendingCheckForLongPress.setAnchor(x, y);
        mPendingCheckForLongPress.rememberWindowAttachCount();
        mPendingCheckForLongPress.rememberPressedState();
        // post一个Runnable,延迟500ms,然后Runnable会触发长按performLongClick()
        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
}

    private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick(mX, mY)) {// 触发长按事件
                    // 注意,这里将变量设为了true,即表示触发了长按事件
                    mHasPerformedLongPress = true;
                }
            }
        }
    }

首先发送一个延迟Runnable,在Runnable中去触发长按,即performLongClick()。这个延迟时间就是View判断当前事件是否是长按的一个阈值,500毫秒。

这种通过Handler的延时机制来执行延时操作的方法在Android源码中很多地方都有用到。

ACTION_MOVE
if (clickable) {
    // 将当前触摸位置传递给background、foreground
    drawableHotspotChanged(x, y);
}

// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {// 划着划着滑出视图范围了
    // Outside button
    // Remove any future long press/tap checks
    // 移除之前添加的的长按、按压等runnable
    // 这里不移除click的原因是,click只有在ACTION_UP的时候才会添加
    removeTapCallback();
    removeLongPressCallback();
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);// 取消按压状态
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}

基本都在注释里面了,大意是当滑出当前View的范围后,之前在ACTION_DOWN中添加了长按、按下等Runnable都会被从队列中移除,并且即使后来又滑入了当前View的范围,因为没有走ACTION_DOWN,所以也不会触发长按了。

ACTION_UP

由于代码过长,因此分段进行分析。

if ((viewFlags & TOOLTIP) == TOOLTIP) {
    handleTooltipUp();// 关闭工具提示
}
if (!clickable) {
    // 移除可能在down的时候发送的Runnable
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    break;
}

再次提醒,前面说到的代码都是在允许点击或有ToolTip的情况下才会执行的。

首先会判断是否有ToolTip,如果有的话则去post一个ToolTip的Runnable。关于ToolTip,这是个在SDK≥26后出现的一个功能。调用setToolTipText()可以设置提示文本,当长按的时候会弹出一个小窗口显示文本信息。那么这个功能和长按监听器如何处理冲突呢?这里不具体分析,结论是:当onLongClickListener()返回false的时候,将会去显示ToolTip,返回true则不显示。

注意上面if(!clickable)中使用了break,所以下面的代码都是在可点击的情况下才执行的!

boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    boolean focusTaken = false;
    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
        focusTaken = requestFocus();// 获取焦点
    }

   	...省略部分代码	
    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
		// 这是个点击事件,移除长按runnable
        removeLongPressCallback();

        if (!focusTaken) {
            if (mPerformClick == null) {
                mPerformClick = new PerformClick();
            }
            if (!post(mPerformClick)) {// post失败的情况下,直接触发click
                performClickInternal();
            }
        }
    }

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

    // 移除按压,检查长按的runnable
    removeTapCallback();
}
mIgnoreNextUpEvent = false;

整个代码主要执行了两个动作,移除非点击相关的Runnable,以及Post一个点击Runnable。

需要满足以下几个条件才会当成点击事件处理:

  1. mHasPerformedLongPress为false,该变量在触发长按,即performLongClick()后为true。
  2. mIgnoreNextUpEvent为false,该变量仅在非Touch事件才有可能为true。
  3. 当前View能够获取到焦点。

这里有个问题,那就是为什么不直接调用performClick()去触发点击,而是要使用Handler去post呢?根据源码注释,官方是这样解释的:

This lets other visual state of the view update before click action start.

谷歌翻译:这样,在单击操作开始之前,View的其他视觉状态就会更新。

ACTION_CANCEL
// 执行一些清除操作
if (clickable) {
    setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

移除所有可能添加的Runnable,重置状态。

总结

  1. onTouchListener中返回了true的话,事件将不再传递到onTouchEvent(),即事件被消费了。
  2. 可单击的禁用View依然会消费事件,但是不会触发onClickListener()
  3. 如果设置了代理,且在代理的onTouchEvent()返回true的话,事件将不再进行默认的处理。
  4. 如果按下之后,进行滑动,并且滑出了当前View的范围,那么将不再触发长按事件,即使后面又滑入当前View的范围。
发布了25 篇原创文章 · 获赞 7 · 访问量 1万+
App 阅读领勋章
微信扫码 下载APP
阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览