View的触摸反馈、事件传递

738 阅读5分钟

View的触摸反馈

onTouchEvent

重写onTouchEvent(), 原本是长这样的,

@Override
public boolean onTouchEvent(MotionEvent event) {
  return super.onTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
  //这样view就消费了事件
  return true;
}

一下这样的话,点击事件才有效果,如上什么都不处理就仅仅返回true,那么在外面添加 onClickListener事件是没有反馈的,这就说明onTouchEvent的优先级高于 OnClickListener.

@Override
public boolean onTouchEvent(MotionEvent event) {
  if (event.getActionMasked() == MotionEvent.ACTION_UP){
    performClick();
  }
  return true;
}

当触摸到黄色块的onTouchEvent 处理了返回true的时候,在它底下的绿色快以及父View的onTouchEvent()都不会收到事件

OnTouchEvent是事件组,只有在 down事件返回true的时候,后续的move、up事件都会传过来。跟上面的直接返回true是一样的。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN){
            return true;
        }
        return super.onTouchEvent(event);
    }

event.getAction() 其实包含了两个信息,down事件以

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_UP){
            performClick();
        }
      	//包含两个信息,按下/抬起 , 第几个手指,处理需要逻辑。所以按下分两种MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN。两个信息被压到一个变量里面,类似于MeasureDesc存储
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
        }
      	//这里只是包含点击事件
        switch (event.getActionMasked()){
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_POINTER_UP:
        }
      	//第几个手指
      	switch (event.getActionIndex()){
        }
        return true;
    }
View的onTouchEvent

点击、长按,右键点击。

public boolean onTouchEvent(MotionEvent event) {
        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;
//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.
         	//事件被consume,但是disabled,自己不能点,底下的父view也不能被点击
            return clickable;
        }
        if (mTouchDelegate != null) {//一般用在增大点击范围。
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
		//clickable这个View可点击,比如TextView就没有用。
        //(viewFlags & TOOLTIP) == TOOLTIP SDK 28引入的点击,解释性的。
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                 case MotionEvent.ACTION_DOWN:
                	//InputDevice.SOURCE_TOUCHSCREEN  TOOLTIP 提示文字远一点的距离
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;
                    if (!clickable) {//给提示性文字用的。
                        checkForLongClick(0, x, y);
                        break;
                    }
                	//检测鼠标右键点击
                    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();
                        postDelayed(mPendingCheckForTap,
                             ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP){
                      //TOOlTIP 延迟消失
                        handleTooltipUp();
                    }
                    if (!clickable) {//这个View仅仅处理 TOOlTI,释放返回
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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);
                        }

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

                        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();
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                      //波纹效果
                      drawableHotspotChanged(x, y);
                    }
                    // Be lenient about moving outside of buttons
                	//移出去的宽容距离 mTouchSlop
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }
            return true;
        }
        return false;
    }
/**
     * @hide 任何级别的父View是否在滑动空间里。
     */
    public boolean isInScrollingContainer() {
        ViewParent p = getParent();
        while (p != null && p instanceof ViewGroup) {
            if (((ViewGroup) p).shouldDelayChildPressedState()) {
                return true;
            }
            p = p.getParent();
        }
        return false;
    }

//需要等待100ms的等待才触发按下。

 public boolean shouldDelayChildPressedState() {
        return true;
  }

ViewGroup的触摸反馈

ListView,ScrollView父view 拦截 onInterceptTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev){
  boolean consume = false;
  if(onInterceptTouchEvent(ev)){
    consume = onTouchEvent(ev);
  }else{
    consume = child.dispatchTouchEvent(ev);
  }
  return consume;
}

在OnInterceptTouchEvent中拦截,并记下拦截时候的坐标等事件。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int delta = ev.getY() ????;//纵向距离
        if (Math.abs(delta) > SLOP) {
            //最后一次触发自己的onInterceptTouchEvent, 第一次去触发自己的
            return true;
        } else {
            //首先返回false,当满足条件后再返回 true。
            return false;
        }
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

触摸反馈的流程

View的dispatchTouchEvent, 外挂的mOnTouchListener 优先级比onTouchEvent(event)更高。

public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        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);
        }
        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();
        }

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

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

ViewGroup.dispatchTouchEvent()

  • 如果是用户初次按下(Action_down),清空TouchTargets 和 DISALLOW_INTERCEPT标记(requestDisallow)

  • 拦截处理

  • 如果不拦截并且不是CANCEL事件,并且是DOWN或者POINTER_DOWN, 尝试把pointer(手指)通过 TouchTarget分配给子View;并且如果分配给了新的子View, 调用child.dispatchTouchEvent()把事件传给子View

  • 看有没有TouchTarget(有哪些子View要消费事件。)

    ​ ** 如果没有,调用自己的super.dispatchTouchEvent()

    ​ **如果有, 调用child.dispatchTouchEvent()把事件传给对应的子View(如果有的话)

  • 如果是POINTER_UP, 从TouchTargets中清除POINTER信息;如果是UP或Cancel,重置状态。

dispatchTransformedTouchEvent:确定要分配给哪个子View

addTouchTarget:分配给哪个子View