AndroidUI advanced--touch feedback and event distribution source code analysis

Android touch event distribution mechanism

Event distribution order

When the Android device is touched, the touch screen event response is sent down, but the processing is reversed. Activty will finally send the event to ViewGroup or View, and then pass it layer by layer if there is no interception, and understand this mechanism by combing the source code process.

For students who are not familiar with the mechanism, you can first read the dispatchTouchEvent part of View to understand it better.

Activity::dispatchTouchEvent

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

Briefly summarize dispatchTouchEventthis method.

  • Function: Pass the touch screen motion event down to the target view or the target itself.

  • Parameters: The motion event that needs to be dispatched

  • Return value: True if the event is consumed, otherwise False (no consumption does not mean delivery)

The touch event is first responded by the current Activity, execute the dispatchTouchEvent method, first determine whether the event is ACTION_DOWN, execute onUserInteraction, an empty method, used for custom implementation, usually used to debug the interaction of the device. This is also the earliest place to receive touch events during application development.

"Actually, the onUserInteraction method is mainly used to manage status bar notifications and cancel notifications when appropriate. There is another method related to this method, onUserLeaveHint. As part of the Activity lifecycle callback, this method will be Called when the Activity is placed in the background (such as the user clicks the Home button), this method will be called before the onPause method."

superDispatchTouchEvent这个方法是window的方法,window提供了空方法,由唯一子类PhoneWindow实现,而PhoneWindow则又调用了DecorView的superDispatchTouchEvent,这里真正的实现地方是DecorView的dispatchTouchEvent。而ViewGroup又可以将事件分发给View。最后才回到Activity的onTouchEvent,Activity对于事件的消费是最低级的。

最后执行onTouchEvent

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

    return false;
}
复制代码

判断Window是否要在touch后关闭,如果是就要结束Activity,并消费事件。否则不消费。

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
        return true;
    }
    return false;
}
复制代码

window这里主要是判断是否在window外,并且设定了关闭。

ViewGroup::dispatchTouchEvent

从Activity的dispatch方法知道了DecorView会将事件分发给ViewGroup执行其中对dispatchTouchEvent。所以逐步分析一下ViewGroup的dispatchTouchEvent方法。

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);
}
复制代码

方法的一开始自洽性检查、这里有一个焦点判断。如果事件的目标是一个可访问的焦点,那么就会查找具有可访问焦点的view,如果找到的子view不处理该事件,才会按照正常的流程派发给所有子view。可以理解为ViewGroup是子View的集合,需要判断处理的优先级,那么焦点一般是优先级最高,需要判断一下是否需要优先处理。设置为false就代表不特殊处理,正常派发事件。

 if (childWithAccessibilityFocus != null) {
     if (childWithAccessibilityFocus != child) {
         continue;
     }
     childWithAccessibilityFocus = null;
     i = childrenCount - 1;
}
复制代码

在Android Q里面还存在focus优先级判断,但是在Android R这里被删掉了,可能google删除了,也可能移到别的地方处理去了,这里不太清楚。

if (onFilterTouchEventForSecurity(ev)) {
    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.
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
复制代码

在判断Touch事件安全性以后,判断每次ACTION_DOWN,cancelAndClearTouchTargets初始化TouchTarget链表,保证没有子view正在被按下。

TouchTarget是一个子view对于触摸反馈顺序的链表,在多点触控下会比较复杂。

resetTouchState就和方法名一样。主要就是初始化一个Touch事件的周期,把flag都清除掉。这两个方法重置了触摸反馈。

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

// 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);
}
复制代码

接下来判断是否拦截,mFirstTouchTarget是链表头节点,如果不为null表示有子View可以消费事件。FLAG_DISALLOW_INTERCEPT这个flag表示子view不希望被拦截事件,子view可以通过实现requestDisallowInterceptTouchEvent这个方法来表示不希望被拦截。如果是在ACTION_DOWN的时候resetTouchState里将flag重置了,所以理解为当前不是ACTION_DOWN且子view表示过不希望被拦截,子view会强行绕过ViewGroup的分发顺序,这在一些滑动的View里可以考虑使用这个方法来优化滑动体验。

之后就是onInterceptTouchEvent,这是拦截的方法,和ontouchevent的响应顺序相反,先由父view来处理是否拦截,表示强行占用touchevent,不让子view用,比如滑动的时候可以让子view先响应,但是如果他滑动了就得交给父view来处理,在recyclerview重写了该方法,这个方法只会返回一次true,true后直接调用子view的onTouchEvent,子view的touchevent为false再去用viewgroup的touchevent,默认false,可重写该方法,但是建议非必要的情况下返回false。

如果没有可以消费事件的子view,默认也是拦截。拦截后将焦点处理设置为false。

if (!canceled && !intercepted) {
    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
            ? findChildWithAccessibilityFocus() : null;

    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        final int actionIndex = ev.getActionIndex(); // always 0 for down
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                : TouchTarget.ALL_POINTER_IDS;

        // Clean up earlier touch targets for this pointer id in case they
        // have become out of sync.
        removePointersFromTouchTargets(idBitsToAssign);

        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            final float x =
                    isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
            final float y =
                    isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
            // Find a child that can receive the event.
            // Scan children from front to back.
            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
            final boolean customOrder = preorderedList == null
                    && isChildrenDrawingOrderEnabled();
            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);
                if (!child.canReceivePointerEvents()
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    continue;
                }

                newTouchTarget = getTouchTarget(child);
                if (newTouchTarget != null) {
                    // Child is already receiving touch within its bounds.
                    // Give it the new pointer in addition to the ones it is handling.
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                    break;
                }
复制代码

判断事件没有被取消也没有被ViewGroup拦截的话。

得到可以响应的view数组根据遍历规则(可以理解为在xml里面从大往小,从下往上)进行遍历,

protected boolean canReceivePointerEvents() {
    return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
复制代码

子view需要visible(就是xml里那个visible)且不在进行动画,且在view范围内。这里可以理解为view动画移位后,触摸事件的响应还在原来的位置。

然后通过touchtarget链表判断多点触控下的view处理。newTouchTarget就是表示不只是一个子view。

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

                // 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;
        }
    }
}
复制代码

这段代码寻找即将分发消费的touchtarget

dispatchTransformedTouchEvent这个方法是ViewGroup到View分发的方法,判断了子View不为空则调用子View的dispatchTouchEvent,在这里第一次调用是将down事件分发给子view,第二次调用是在touchtarget为空的情况下,分发机制给自己,第三次分发给子view消费并且返回子view是否消费事件。然后处理事件被消费了的标识alreadyDispatchedToNewTouchTarget。

如果没有找到可以消费事件的子view,就采取最近消费事件的子view来消耗事件。

// 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;
    }
}
复制代码

如果没有子类消费或是被拦截,就自己消费。

后面根据alreadyDispatchedToNewTouchTarget判断事件如果被分发了,handle = true。

    // 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;
复制代码

最后的处理,事件被取消就更新链表,事件没被消费的情况下,自洽性检查。返回handle。

ViewGroup本身并没有重写onTouchEvent,具备事件分发、事件拦截,但是没有做事件处理。有些ViewGroup的子类会去重写onTouchEvent,比如RecyclerView。

根据上面的分析,如果ViewGroup有子View的话,一般流程最终会走到子View的dispatchTouchEvent。

View::dispatchTouchEvent(MotionEvent event):

// 如果此event作为第一个可访问的焦点被处理
if (event.isTargetAccessibilityFocus()) {
    // 我们没有焦点,或者没有虚拟后代拥有焦点,因此不处理事件。
    if (!isAccessibilityFocusedViewOrHost()) {
        return false;
    }
    // 我们有焦点并得到了事件,然后使用常规事件调度。
    event.setTargetAccessibilityFocus(false);
}
复制代码

首先进行isTargetAccessibilityFocus,判断事件是否t作为第一个可访问的焦点被处理

public  boolean isTargetAccessibilityFocus() {
    final int flags = getFlags();
    return (flags & FLAG_TARGET_ACCESSIBILITY_FOCUS) != 0;
}
复制代码

getFlags调用native方法返回flags,再进行位与操作,目的是判断是否等同于FLAG_TARGET_ACCESSIBILITY_FOCUS,先去判断是不是第一个可访问的焦点,如果是就去判断有没有焦点,如果有焦点就去按常规处理,把flag改掉。这个在ViewGroup里作为子view优先级判断。

也就是说,target View 获取不到焦点(我们将focusable = false) 将直接跳过此次事件处理,他还是能获取到触摸事件,只是跳过处理

焦点的情况主要是EditText或者电视等设备。

设置默认返回result false

if (mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
复制代码

自洽检查,类似于ActionDown和Up是否一一匹配

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

一个Action是32位的,高位表示指针的index,低位表示事件,这里获取事件的第八位,可以理解为掩码,用一个较小的int数来表示事件。这里是表示在滑动的时候,又重新接收到action_down事件,所以用一种无副作用的方式停止嵌套滚动

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;
    }
}
复制代码

onFilterTouchEventForSecurity首先判断此次事件窗口是否被遮挡,被遮挡则返回false

判断是否添加了OnTouchListener,View要处理Touch事件,就需要添加,并且判断是否enable(默认true)且在onTouch方法里返回true,例如

button.setOnTouchListener(new OnTouchListener() {
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    Log.i(TAG, "onTouch");
    return true;
  }
});
复制代码

这就是为什么在这里返回true会拦截事件分发的原因,因为这里会在dispatchTouchEvent的result设为true

当然,在这里还看不出来为什么true就拦截了。

然后再进一步执行view的onTouchEvent,这里的逻辑与操作时为了短路避免在上面已经true的情况下进行不必要的运算

if (actionMasked == MotionEvent.ACTION_UP ||
        actionMasked == MotionEvent.ACTION_CANCEL ||
        (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
    stopNestedScroll();
}
复制代码

在事件的结束或者不想继续事件了,停止嵌套滚动。

事件分发机制的顺序就是dispatchTouchEvent → onTouch → onTouchEvent。

这里可以看出当onTouch消费了,那么onClick也就不会执行了,而onClick是在onTouchEvent里。

所以说如果在onTouch里面就返回了true,事件也就被拦截了,不会执行onTouchEvent

再来看一下View的onTouchEvent

其实View的onTouchEvent主要就是要在没有被touch事件消费掉的情况下,区分用户到底是在怎么操作屏幕,是滚动,是点击,是长按,是误触等等(如果一个view只是想触发touch,不是click等的话,上面返回true)。onTouchEvent对于应用开发来说,相当于是一组view的触摸事件的响应接口。

final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    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;
    }
}
复制代码

初始化坐标、viewFlags、action、clickable,clickable的判断表示如果一个View不可用,但是只要它可以点击或长按,都返回true。判断是否交给事件分发代理处理。

然后进入switch,也就是事件的处理。在这个switch外面就是return true;这也表明了一旦进入switch事件分发一定会在这里消费掉

ACTION_DOWN

首先介绍一下两个flag,PFLAG_PRESSED标识事件按下,而PREPRESSED用于标识在ACTION_DOWN后短时间内(getTapTimeout事件)无法确定是哪一种点击事件(长按、触摸等判断)。这两个flag都是二进制int,PREPRESSED是0000 0010 0000 0000 0000 0000 0000 0000,也就是可以通过第七位的1用位或|=和位与非$=~来控制flag的第七位来判断当前状态。

case MotionEvent.ACTION_DOWN:
    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
    }
    mHasPerformedLongPress = false;

    if (!clickable) {
        checkForLongClick(
                ViewConfiguration.getLongPressTimeout(),
                x,
                y,
                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
        break;
    }

    if (performButtonActionOnTouchDown(event)) {
        break;
    }
复制代码

首先View需要判断clickable,不可点击的view可以响应Touch,但是在ActionDown就会被拦截,例如ImageView

判断performButtonActionOnTouchDown表示的是类似鼠标右键的事件,不深入。

// 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();
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
    // Not inside a scrolling container, so show the feedback right away
    setPressed(true, x, y);
    checkForLongClick(
            ViewConfiguration.getLongPressTimeout(),
            x,
            y,
            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
复制代码

isInScrollingContainer遍历判断当前view是否是可滑动容器内,用于处理滑动事件

如果在滚动容器内,mPendingCheckForTap是一个runnable对象,判断在taptimeout时间内,用户的触摸坐标是否变化,变了就是滑动,这是一个delay消息,在延迟执行的tapTimeout中,如果坐标没变,则确认为按下,并且进入判断是否长按。这个runnable会把PFLAG_PREPRESSED取消标记,因为这个时候已经可以确认tap行为。

如果不是在滚动容器,则直接判断长按。

Action_down可以确认的是当前事件是否为长按、滑动。

这里要注意的是这个delay消息是一个延迟,在自定义View的时候非滑动组件要把这个延迟设置为false。

checkForLongClick

再来看一下长按事件,这里经常看到一个方法checkForLongClick

private void checkForLongClick(long delay, float x, float y, int classification) {
    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();
        mPendingCheckForLongPress.setClassification(classification);
        postDelayed(mPendingCheckForLongPress, delay);
    }
}
复制代码

这里有四个参数,延迟、位置信息,和分类,这个分类在这里只有两种,长按和类似3d touch的deep press,不去管deep press。

首先确认ViewFlag是可长按或TOOLTIP(xml可配置的长按提示功能)。

mHasPerformedLongPress这个标识代表了长按是否已经被调用,设为false,表示还没有,如果已经被调用了,那么就不会识别长按而是tap了。然后就是一个名为CheckForLongPress的Runnable

private final class CheckForLongPress implements Runnable {
    private int mOriginalWindowAttachCount;
    private float mX;
    private float mY;
    private boolean mOriginalPressedState;
    /**
     * The classification of the long click being checked: one of the
     * FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__* constants.
     */
    private int mClassification;

    @UnsupportedAppUsage
    private CheckForLongPress() {
    }

    @Override
    public void run() {
        if ((mOriginalPressedState == isPressed()) && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            recordGestureClassification(mClassification);
            if (performLongClick(mX, mY)) {
                mHasPerformedLongPress = true;
            }
        }
    }

    public void setAnchor(float x, float y) {
        mX = x;
        mY = y;
    }

    public void rememberWindowAttachCount() {
        mOriginalWindowAttachCount = mWindowAttachCount;
    }

    public void rememberPressedState() {
        mOriginalPressedState = isPressed();
    }

    public void setClassification(int classification) {
        mClassification = classification;
    }
}
复制代码

发送延迟消息就是执行该run方法,这里检查了WIndowAttachCount也就是view的attach次数,用于判断长按过程中是否有Activity的生命周期变化,view的实效来判断长按是否失效。然后就是执行performLongClick,并将mHasPerformedLongPress = true。

public boolean performLongClick(float x, float y) {
    mLongClickX = x;
    mLongClickY = y;
    final boolean handled = performLongClick();
    mLongClickX = Float.NaN;
    mLongClickY = Float.NaN;
    return handled;
}
复制代码
public boolean performLongClick() {
    return performLongClickInternal(mLongClickX, mLongClickY);
}
复制代码

在这里调用了view的onLongClickListener。

private boolean performLongClickInternal(float x, float y) {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

    boolean handled = false;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnLongClickListener != null) {
        handled = li.mOnLongClickListener.onLongClick(View.this);
    }
    if (!handled) {
        final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
        handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
    }
    if ((mViewFlags & TOOLTIP) == TOOLTIP) {
        if (!handled) {
            handled = showLongClickTooltip((int) x, (int) y);
        }
    }
    if (handled) {
        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    }
    return handled;
}
复制代码

第一行是辅助功能,用于一些特殊需求,可以不管。这个handled是返回值,表示事件是否被消费。在这里就调用了onLongClick方法

如果消费了,会提供震动反馈HapticFeedbackConstants。

ACTION_MOVE

case MotionEvent.ACTION_MOVE:
    if (clickable) {
        drawableHotspotChanged(x, y);
    }

    final int motionClassification = event.getClassification();
    final boolean ambiguousGesture =
            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
    int touchSlop = mTouchSlop;
    if (ambiguousGesture && hasPendingLongPressCallback()) {
        if (!pointInView(x, y, touchSlop)) {
            // The default action here is to cancel long press. But instead, we
            // just extend the timeout here, in case the classification
            // stays ambiguous.
            removeLongPressCallback();
            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                    * mAmbiguousGestureMultiplier);
            // Subtract the time already spent
            delay -= event.getEventTime() - event.getDownTime();
            checkForLongClick(
                    delay,
                    x,
                    y,
                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
        }
        touchSlop *= mAmbiguousGestureMultiplier;
    }

    // Be lenient about moving outside of buttons
    if (!pointInView(x, y, touchSlop)) {
        // Outside button
        // Remove any future long press/tap checks
        removeTapCallback();
        removeLongPressCallback();
        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    }
复制代码

这里主要执行移动,判断手势操作,判断移动边际。

view的边界范围touchSlop,用于一些手指有部分在view外的情况下判断是否算是该view的时间,扩大这个值可以增加边界,这里判断是否在范围外。

final boolean deepPress =
        motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
    // process the long click action immediately
    removeLongPressCallback();
    checkForLongClick(
            0 /* send immediately */,
            x,
            y,
            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}

break;
复制代码

这是用于判断压感的,类似3dtouch。

ACTION_UP:

boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    // take focus if we don't have it already and we should in
    // touch mode.
    boolean focusTaken = false;
    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
        focusTaken = requestFocus();
    }

    if (prepressed) {
        // The button is being released before we actually
        // showed it as pressed.  Make it show the pressed
        // state now (before scheduling the click) to ensure
        // the user sees it.
        setPressed(true, x, y);
    }
复制代码

首先获取焦点、然后要判断是否button在还没有来得及响应的时候就被释放了,那也要继续完成点击事件。

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)) {
            performClickInternal();
        }
    }
}
复制代码

removeLongPressCallback删除长按检测计时器。

为了保证view的时序,使用线程发布消息,让界面可以先更新

public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();

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

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}
复制代码

performClickInternal最终会走到performClick,先后两次notifyAutofillManagerOnClick确保view的时序,然后就是onClick,result = true。后面就是状态判断removeTapCallback

ACTION_CANCEL

触控事件被系统取消,类似于移动事件被父view拦截。

case MotionEvent.ACTION_CANCEL:
    if (clickable) {
        setPressed(false);
    }
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    break;
复制代码

cancel就是把原来的一些状态记录都去除,是一个重置的操作。

总结

经典U型图(来源于网络)

image-20201218192441754.png

Guess you like

Origin juejin.im/post/7079398040762597384