学习android一段时间了,觉得事件分发是一个比较难的部分,自定义控件需要这方面的知识,因此花了一段时间研究了一下,在此记录下自己学习的过程,供以后学习使用。

View的时间分发过程dispatchTouchEvent—> onTouch –-> onTouchEvent

    /**     * 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) {        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onTouchEvent(event, 0);        }        if (onFilterTouchEventForSecurity(event)) {            //noinspection SimplifiableIfStatement            ListenerInfo li = mListenerInfo;            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                return true;            }            if (onTouchEvent(event)) {                return true;            }        }        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);        }        return false;    }

dispatchTouchEvent的返回值由是否设置触摸回调的返回值或者onTouchEvent的返回值决定。

一个触摸事件发生后,首先如果设置了触摸事件的侦听,并且返回了true,表示该事件已经被消费了,dispatchTouchEvent方法会直接返回true。如果回调返回了false。那么会执行onTouchEvent方法,dispatchTouchEvent的返回值由onTouchEvent的返回值决定。默认情况下,只要可以点击并且是enable的,都会返回true。下面是onTouchEvent方法

 /**     * Implement this method to handle touch screen motion events.     *     * @param event The motion event.     * @return True if the event was handled, false otherwise.     */    public boolean onTouchEvent(MotionEvent event) {        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));        }        if (mTouchDelegate != null) {            if (mTouchDelegate.onTouchEvent(event)) {                return true;            }        }        if (((viewFlags & CLICKABLE) == CLICKABLE ||                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {            switch (event.getAction()) {                case MotionEvent.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);                       }                        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();                                }                                if (!post(mPerformClick)) {                                    performClick();                                }                            }                        }                        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();                    }                    break;                case MotionEvent.ACTION_DOWN:                    mHasPerformedLongPress = 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();                        }                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                    } else {                        // Not inside a scrolling container, so show the feedback right away                        setPressed(true);                        checkForLongClick(0);                    }                    break;                case MotionEvent.ACTION_CANCEL:                    setPressed(false);                    removeTapCallback();                    removeLongPressCallback();                    break;                case MotionEvent.ACTION_MOVE:                    final int x = (int) event.getX();                    final int y = (int) event.getY();                    // Be lenient about moving outside of buttons                    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 里面执行的,点击事件是在ACTION_UP里面执行的。长按事件的执行逻辑如下:

    class CheckForLongPress implements Runnable {        private int mOriginalWindowAttachCount;        public void run() {            if (isPressed() && (mParent != null)                    && mOriginalWindowAttachCount == mWindowAttachCount) {                if (performLongClick()) {                    mHasPerformedLongPress = true;                }            }        }public boolean performLongClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);        boolean handled = false;        ListenerInfo li = mListenerInfo;        if (li != null && li.mOnLongClickListener != null) {            handled = li.mOnLongClickListener.onLongClick(View.this);        }        if (!handled) {            handled = showContextMenu();        }        if (handled) {            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);        }        return handled;    }

如果执行了长按事件,并且长按的回调返回了true,那么mHasPerformedLongPress= true就不会再去执行performClick();

因此想要长按事件和单击事件同时发生,就要在长按的回调函数返回false。

单击事件和长按事件其实都是封装在触摸事件里面的。

ViewGroup的事件分发过程dispatchTouchEvent---> onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {      return false;  }

默认情况下onInterceptTouchEvent返回false,即不拦截。

/**  * {@inheritDoc}  */  @Override  public boolean dispatchTouchEvent(MotionEvent ev) {      if (!onFilterTouchEventForSecurity(ev)) {          return false;      }        final int action = ev.getAction();      final float xf = ev.getX();      final float yf = ev.getY();      final float scrolledXFloat = xf + mScrollX;      final float scrolledYFloat = yf + mScrollY;      final Rect frame = mTempRect;      // 是否禁用拦截,如果为true表示不能拦截事件;反之,则为可以拦截事件      boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;      // ACTION_DOWN事件,即按下事件      if (action == MotionEvent.ACTION_DOWN) {          if (mMotionTarget != null) {              // this is weird, we got a pen down, but we thought it was              // already down!              // XXX: We should probably send an ACTION_UP to the current              // target.              mMotionTarget = null;          }          // If we're disallowing intercept or if we're allowing and we didn't          // intercept。如果不允许事件拦截或者不拦截该事件,那么执行下面的操作          if (disallowIntercept || !onInterceptTouchEvent(ev))         // 1、是否禁用拦截、是否拦截事件的判断              // reset this event's action (just to protect ourselves)              ev.setAction(MotionEvent.ACTION_DOWN);              // We know we want to dispatch the event down, find a child              // who can handle it, start with the front-most child.              final int scrolledXInt = (int) scrolledXFloat;              final int scrolledYInt = (int) scrolledYFloat;              final View[] children = mChildren;              final int count = mChildrenCount;                for (int i = count - 1; i >= 0; i--)        // 2、迭代所有子view,查找触摸事件在哪个子view的坐标范围内                  final View child = children[i];                  // 该child是可见的                  if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE                          || child.getAnimation() != null) {                      // 3、获取child的坐标范围                      child.getHitRect(frame);                                     // 4、判断发生该事件坐标是否在该child坐标范围内                      if (frame.contains(scrolledXInt, scrolledYInt))                              // offset the event to the view's coordinate system                          final float xc = scrolledXFloat - child.mLeft;                          final float yc = scrolledYFloat - child.mTop;                          ev.setLocation(xc, yc);                          child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;                          // 5、child处理该事件,如果返回true,那么mMotionTarget为该child。正常情况下,                          // dispatchTouchEvent(ev)的返回值即onTouchEcent的返回值。因此onTouchEcent如果返回为true,                          // 那么mMotionTarget为触摸事件所在位置的child。                         if (child.dispatchTouchEvent(ev)) //默认的实现下View.dispatchTouchEvent(ev)返回值一定为true                            // Event handled, we have a target now.                              mMotionTarget = child;                              return true; //表示子view已经能将触摸时间消费掉                        }                                     }                  }              }          }      }// end if        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||              (action == MotionEvent.ACTION_CANCEL);        if (isUpOrCancel) {          // Note, we've already copied the previous state to our local          // variable, so this takes effect on the next event          mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;      }        // The event wasn't an ACTION_DOWN, dispatch it to our target if      // we have one.      final View target = mMotionTarget;      // 6、如果mMotionTarget为空,那么执行super.dispatchTouchEvent(ev),      // 即View.dispatchTouchEvent(ev),就是该View Group自己处理该touch事件,只是又走了一遍View的分发过程而已. (指没有找到view,也可能是下面两种情况) // 1,拦截事件 或者2.在不拦截事件target view的onTouchEvent返回false的情况都会执行到这一步.  这种情况下//执行super.dispatchTouchEvent(ev);也就是当成view来分发事件,过程同 view的时间分发过程一致     if (target == null) {          // We don't have a target, this means we're handling the          // event as a regular view.          ev.setLocation(xf, yf);          if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {              ev.setAction(MotionEvent.ACTION_CANCEL);              mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;          }          return super.dispatchTouchEvent(ev); // 调用super.dispatchTouchEvent(ev); 表示子view没能将触摸时间消费掉,就会将触摸事件传递给父view    }        // if have a target, see if we're allowed to and want to intercept its      // events  // 7、如果没有禁用事件拦截,并且onInterceptTouchEvent(ev)返回为true,即进行事件拦截.  //-----似乎只有target!=null也就是子view处理down还返回true,然后拦截事件发生了才会执行下面的if//也就是对move 和 up 事件的拦截会执行到这里。//由于在down时child.dispatchTouchEvent(ev)返回了true,所以target有了值。下面的代码是让子view执行ACTION_CANCEL事件    if (!disallowIntercept && onInterceptTouchEvent(ev)) {          final float xc = scrolledXFloat - (float) target.mLeft;          final float yc = scrolledYFloat - (float) target.mTop;          mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;          ev.setAction(MotionEvent.ACTION_CANCEL);          ev.setLocation(xc, yc);          //           if (!target.dispatchTouchEvent(ev)) {//拦截move事件,会让子view分发cancel事件              // target didn't handle ACTION_CANCEL. not much we can do              // but they should have.          }          // clear the target          mMotionTarget = null;          // Don't dispatch this event to our own view, because we already          // saw it when intercepting; we just want to give the following          // event to the normal onTouchEvent().          return true;      }        if (isUpOrCancel) {          mMotionTarget = null;      }        // finally offset the event to the target's coordinate system and      // dispatch the event.      final float xc = scrolledXFloat - (float) target.mLeft;      final float yc = scrolledYFloat - (float) target.mTop;      ev.setLocation(xc, yc);        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {          ev.setAction(MotionEvent.ACTION_CANCEL);          target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;          mMotionTarget = null;      }  // 事件不拦截,且target view在ACTION_DOWN时返回true,那么后续事件由target来处理事件  // 执行到这里的条件是子view 在ACTION_DOWN时返回true,这样target不为null,并且//还不会执行target == null 的判断才会执行到这里    return target.dispatchTouchEvent(ev);  }

拦截的使用方法

@Override     public booleanonInterceptTouchEvent(MotionEvent ev)     {         int action =ev.getAction();         switch (action)         {         case MotionEvent.ACTION_DOWN:             return true ;          caseMotionEvent.ACTION_MOVE:             return true ;          caseMotionEvent.ACTION_UP:             return true ;          }                  return false;     } 
  

1.如果你在DOWNretrun true ,则DOWN,MOVE,UP子View都不会捕获事件;onInterceptTouchEvent(ev) return true的时候,mMotionTarget 为null ;

2.如果你在MOVEreturn true , 则子View在MOVE和UP都不会捕获事件。onInterceptTouchEvent(ev) return true的时候,此时target还不为null,会执行target.dispatchTouchEvent(ev)来分发cancel事件,接着会把mMotionTarget置为null ;

3.拦截down事件,会执行到target==null,然后调用super.dispatchTouchEvent(ev);即父view来处理。

4.拦截move事件,子view会处理cancel事件,父view不会处理该触摸事件,并且让mMotionTarget = null;结果return true。认为这一个动作已完成,不会再回传到父控件的OnTouchEvent中处理(通过源代码发现确实是这样子)。但是实际上触摸事件是一连串事件,下一个move事件发生后,会判断target ==null,执行return super.dispatchTouchEvent(ev);也就是让拦截的ViewGroup来处理后续的MOVE、UP事件(我是这么理解的^_^)。
总结一下就是:触摸事件对DOWN事件不进行拦截,因此DOWN事件可以被子View正常的处理。但是在MOVE时对事件进行了拦截,那么子View就无法接收到MOVE以及后面的事件了,它会收到一个CANCEL事件,后续的事件将会被拦截的ViewGroup的onTouchEvent进行处理。

5.requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截

如果ViewGroup的onInterceptTouchEvent(ev)当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;那还有补救的措施。requestDisallowInterceptTouchEvent(true)便可以使子view接收到,因为会跳过if(!disallowIntercept && onInterceptTouchEvent(ev)),执行returntarget.dispatchTouchEvent(ev);通过源码很容易解释。

6.但是如果是在ACTION_DOWN时返回true来拦截的,那么子view无论怎么做都不可能捕获任何事件,因为此时target == null,肯定会执行return super.dispatchTouchEvent(ev);

7.onTouchEvent收到ACTION_DOWN,是否一定能收到ACTION_MOVE,ACTION_UP?收到ACTION_MOVE,能否说明它已经收到过ACTION_DOWN?

第一个如果MOVE和UP被父View拦截就收不到了。第二个可能是ViewGroup拦截了子View的MOVE事件,虽然DOWN事件传递了下去被子View消费,但是由于拦截了MOVE接下来会在ViewGroup中处理MOVE事件。

最后,一个重要的知识点:一般的View默认都是不可点击的。例如:View、ViewGroup、ImageView。但所有的View默认都是enable的除非设置为disable。这个时候就要小心:他们在处理默认的onTouchEvent的时候返回的是false!(disable并且clickable或者longclickable会返回true表明已经被消费)返回false意味着对它们的touch事件会向上抛。

参考的文章:http://blog.csdn.net/lmj623565791/article/details/39102591

http://blog.csdn.net/lmj623565791/article/details/38960443

http://blog.csdn.net/xiaanming/article/details/21696315

更多相关文章

  1. android init进程解析init.rc过程
  2. 在Android中将子View的坐标转换为父View的坐标
  3. Android 事件触发机制的深入学习
  4. Android ViewGroup 触摸屏事件派发机制和源码分析
  5. Android触摸事件的传递(四-1)--输入系统-InputReader
  6. Android framework源码之JNI实现过程分析
  7. Android开机启动过程

随机推荐

  1. Android学习之 使用依赖注入函数库Robogu
  2. android install
  3. Android 中插件的编写方法
  4. android开发之布局常用属性
  5. 安卓中RelativeLayout布局
  6. Android异步处理一:使用Thread+Handler实
  7. Android中EditText的inputType属性值
  8. Android与JavaScript相互调用(Android和h
  9. 相对布局中一些常用属性
  10. Android 相对布局 RelativeLayout 属性 (