AndroidUI进阶--触摸反馈和事件分发源码解析

Posted bug樱樱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AndroidUI进阶--触摸反馈和事件分发源码解析相关的知识,希望对你有一定的参考价值。

android触摸事件分发机制

事件分发顺序

当Android设备被触摸的时候,触摸屏事件响应是向下发,但处理是反过来的。Activty会将事件最终发给ViewGroup或者View,然后没有拦截的话层层传递,通过梳理源码流程理解这一机制。

对于机制不太熟悉的同学,可以先看View的dispatchTouchEvent部分会比较好理解。

Activity::dispatchTouchEvent

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

先简单概括一下dispatchTouchEvent这个方法。

  • 功能:将触摸屏运动事件向下传递到目标view或者目标本身。

  • 参数:需要被分发的运动事件

  • 返回值: 如果事件被消费则True,否则False(不消费不代表就是传递)

触摸事件先由当前Activity响应,执行dispatchTouchEvent方法,首先判断事件是否为ACTION_DOWN,执行onUserInteraction,空方法,用于自定义实现,通常用来debug设备的交互情况。这里也是在应用开发过程中,最早能接收到触摸事件的地方。

“实际上,onUserInteraction方法主要是用于管理状态栏通知,以及在恰当的时候取消通知。与该方法相关的还有另一个方法,onUserLeaveHint。该方法作为Activity生命周期回调的一部分,会在用户将Activity放到后台时调用(如用户点击Home键),该方法会在onPause方法之前调用。”

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型图(来源于网络)

文末

要想成为架构师,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取【保证100%免费】↓↓↓

以上是关于AndroidUI进阶--触摸反馈和事件分发源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Android6.0触摸事件分发机制解读

从源码角度分析Android 事件分发机制以及常见滑动冲突解决方案

从源码角度分析Android 事件分发机制以及常见滑动冲突解决方案

View事件分发机制(源码分析篇)

Android 事件分发机制

Android触摸事件分发