学习笔记——深入理解Android Touch事件分发之View

ViewGroupdispatchTouchEvent()方法中,有时会调用super.dispatchTouchEvent(),而ViewGroup的父类就是View

ViewdispatchTouchEvent()方法说起

/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;

    // 调试用
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

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

    // 判断该View是否被其它View遮盖住。
    if (onFilterTouchEventForSecurity(event)) {

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            // 1.设置了OnTouchListener
            // 2.ENABLED状态
            // 3.onTouch()方法返回true
            result = true;
        }

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

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

上面的代码中提到了li.mOnTouchListener,那么mOnTouchListener是什么呢?

/** 
  * Register a callback to be invoked when a touch event is sent to this view. 
  * @param l the touch listener to attach to this view 
  */  
 public void setOnTouchListener(OnTouchListener l) {  
     mOnTouchListener = l;  
 }

mOnTouchListener其实就是我们在Activity中设置的OnTouchListener

如果设置了OnTouchListenerViewENABLED状态并且onTouch()方法返回true,则ViewOnTouchEvent()方法就不会被执行。

ViewonTouchEvent()方法

/**
 * Implement this method to handle touch screen motion events.
 * <p>
 * If this method is used to detect click actions, it is recommended that
 * the actions be performed by implementing and calling
 * {@link #performClick()}. This will ensure consistent system behavior,
 * including:
 * <ul>
 * <li>obeying click sound preferences
 * <li>dispatching OnClickListener calls
 * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
 * accessibility features are enabled
 * </ul>
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }

    // 关于TouchDelegate,文档中是这样说的
    // The delegate to handle touch events that are physically in this view 
    // but should be handled by another view. 
    // 就是说如果两个View, View 2在View 1中,View 1比较大,如果我们想点击
    // View1的时候,让View 2去响应点击事件,这时候就需要使用TouchDelegate来设置。
    // 如果希望View的Touch范围扩大,可以尝试使用TouchDelegate。
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {// 这个View可点击

        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                // PFLAG_PREPRESSED 表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动。
                // PFLAG_PRESSED 表示不是在一个可滚动的容器中,已经可以确定按下这一操作。
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;

                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // mPrivateFlags为PFLAG_PREPRESSED或PFLAG_PRESSED时执行下面的代码。
                    // 处理点击或长按事件。

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


                    if (!mHasPerformedLongPress) {

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

                            // PerformClick就是个Runnable,里面执行performClick()方法。
                            // post()未执行成功,在直接立即执行performClick()方法。
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }

                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }

                    // `ACTION_DOWN`事件发生后,
                    // 如果在ViewConfiguration.getTapTimeout()时间内触发`ACTION_UP`事件,
                    // mPrivateFlag = PFLAG_PREPRESSED,即prepressed == true
                    if (prepressed) {

                        // 取消按下状态,UnsetPressedState也是个Runnable,里面执行setPressed(false)。
                        postDelayed(mUnsetPressedState,
                                ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        // If the post failed, unpress right now
                        mUnsetPressedState.run();
                    }

                    // 如果mPendingCheckForTap != null,做移除操作。
                    removeTapCallback();
                }
                break;

            case MotionEvent.ACTION_DOWN:
                // 长按事件还未发生
                mHasPerformedLongPress = false;

                // performButtonActionOnTouchDown()处理鼠标右键菜单,有些View显示右键菜单就直接弹菜单。
                // 一般设备用不到鼠标,所以返回false。
                if (performButtonActionOnTouchDown(event)) {
                    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();

                    // 发送一个延时的操作,用于判断用户到底是点击还是滚动。
                    // 其实就是在tapTimeout中如果用户没有滚动,那就是点击了。
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);

                    // 检查是否是长按,就是过一段时间后如果还在按住,那就是长按了。
                    // 长按的时间是ViewConfiguration.getLongPressTimeout(),也就是500毫秒
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                // 取消按下状态,移除点击检查,移除长按检查。
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);

                // 检查是否移动到View外面了。
                if (!pointInView(x, y, mTouchSlop)) {// Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }

        return true;
    }

    return false;
}

ACTION_DOWN事件的处理

postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());发送一个延迟时间为ViewConfiguration.getTapTimeout()的消息,到达延时时间后会执行CheckForTap()里面的run()方法。

  • ViewConfiguration.getTapTimeout()为115ms。
  • mPendingCheckForTap是一个CheckForTap()
private final class CheckForTap implements Runnable {  
      public void run() {  
          // 取消mPrivateFlags的PREPRESSED,然后设置PRESSED标识
          mPrivateFlags &= ~PREPRESSED;  
          mPrivateFlags |= PRESSED;  

          refreshDrawableState();// 刷新背景
          if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
              // 如果设置了长点击,再发送一个长按检测
              postCheckForLongClick(ViewConfiguration.getTapTimeout());  
          }  
      }  
  } 

如果115ms后,没有触发ACTION_UP事件,将会执行上面的run()方法。

private void postCheckForLongClick(int delayOffset) {  
       mHasPerformedLongPress = false;  

       if (mPendingCheckForLongPress == null) {  
           mPendingCheckForLongPress = new CheckForLongPress();  
       }  
       mPendingCheckForLongPress.rememberWindowAttachCount(); 

       // 然后发出一个检测长按的延迟任务,
       // 延时为:ViewConfiguration.getLongPressTimeout() - delayOffset(500ms -115ms)
       postDelayed(mPendingCheckForLongPress,  
               ViewConfiguration.getLongPressTimeout() - delayOffset);  
   }  

从用户ACTION_DOWN触发开始算起,如果500ms内没有触发ACTION_UP事件,则认为是长按事件。

class CheckForLongPress implements Runnable {  

        private int mOriginalWindowAttachCount;  

        public void run() {  
            if (isPressed() && (mParent != null)  
                    && mOriginalWindowAttachCount == mWindowAttachCount) { 

                // performLongClick()方法返回true,才会将mHasPerformedLongPress设为true,
                // 即认为发生了长按点击事件。
                if (performLongClick()) {  
                    mHasPerformedLongPress = true;  
                }  
            }  
        }
}

ACTION_UP事件的处理

ACTION_UP事件中,调用了performClick()方法.。

/**
 * Call this view's OnClickListener, if it is defined.  Performs all normal
 * actions associated with clicking: reporting accessibility event, playing
 * a sound, etc.
 *
 * @return True there was an assigned OnClickListener that was called, false
 *         otherwise is returned.
 */
public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);// 执行onClick()方法。
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

如果设置了OnClickListener(),则执行onClick()方法。

同时也看看UnsetPressedState()的实现,

private final class UnsetPressedState implements Runnable {
        public void run() {
            setPressed(false);
        }
}

public void setPressed(boolean pressed) {
        if (pressed) {
            mPrivateFlags |= PRESSED;
        } else {
            mPrivateFlags &= ~PRESSED;
        }
        refreshDrawableState();
        dispatchSetPressed(pressed);
}

mPrivateFlags中的PRESSED取消,然后刷新背景,分发SetPressed()

onTouchEvent()方法脉络整理

从源码角度将MotionEvent的各种情况分析了一遍,需要从整体角度整理该方法

  • ACTION_DOWN事件

    • 设置mPrivateFlagsPREPRESSED,设置mHasPerformedLongPress = false ,然后发出一个延迟为115ms的CheckForTap(实际为Runnable())。

    • 如果115ms内没有触发ACTION_UP,则将mPrivateFlags置为PRESSED,清除PREPRESSED标识,同时发出一个延时为500ms-115ms的CheckForLongPress(),检测长按。

    • 如果500ms内没有触发ACTION_UP,则会调用performLongClick()方法。此时如果LongClickListener != null,则会执行LongClickListener.onClick(),同时如果LongClickListener.onClick()返回值为true,则mHasPerformedLongPress = true;否则mHasPerformedLongPress == false

  • ACTION_MOVE事件

    • 115ms内移出ViewremoveTapCallback()

    • 115ms后移除View,则将mPrivateFlags中的PRESSED去除,同时removeLongPressCallback()

  • ACTION_UP事件

    • 115ms内,此时mPrivateFlags == PREPRESSED,移除长按检测removeLongPressCallback(),执行点击事件performClick(),延迟执行UnsetPressedState()

    • 115ms - 500ms内,此时mPrivateFlags == PRESSED,移除长按检测removeLongPressCallback(),执行点击事件performClick()

    • 如果是500ms以后,那么有两种情况:

      • 设置了onLongClickListener(),且onLongClickListener.onClick()返回true,则点击事件OnClick()方法无法触发。

      • 没有设置onLongClickListener()或者onLongClickListener.onClick()返回false,则点击事件OnClick()方法依然可以触发。

    • 执行UnsetPressedState()

    • 如果mPendingCheckForTap != null,做移除操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值