IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    自定义View系列教程06–详解View的Touch事件处理

    summer发表于 2016-06-05 23:56:32
    love 0

    自定义View系列教程01–常用工具介绍
    自定义View系列教程02–onMeasure源码详尽分析
    自定义View系列教程03–onLayout源码详尽分析
    自定义View系列教程04–Draw源码分析及其实践
    自定义View系列教程05–示例分析
    自定义View系列教程06–详解View的Touch事件处理

    PS:如果觉得文章太长,那就直接看视频吧


    在之前的几篇文章中结合Andorid源码还有示例分析完了自定义View的三个阶段:measure,layout,draw。 在自定义View的过程中我们还经常需要处理View的Touch事件,这就涉及到了大伙常说的Touch事件的分发。其实,这一部分还是有些复杂的,而且有的地方不是很好理解,尤其是对于刚上路的新司机来说经常理不清楚,欲求不满,欲罢不能——想搞懂却又觉得难,想放弃又觉得舍不得。

    好吧,我也经历过这些痛楚,感同身受。

    所以,我们就从相对而言比较简单的View的Touch事件处理入手开始这部分知识的学习和总结。

    滴滴,开车了,车门即将关闭。上车请刷卡,没卡的乘客请投币。


    如果一个View(比如Button)接收到Touch,那么该Touch事件首先会传入到它的dispatchTouchEvent( )方法,所以我们从这里开始学习View对Touch事件的处理。

    /**
    * 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 (event.isTargetAccessibilityFocus()) {

    if (!isAccessibilityFocusedViewOrHost()) {
    return false;
    }

    event.setTargetAccessibilityFocus(false);
    }

    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
    stopNestedScroll();
    }

    if (onFilterTouchEventForSecurity(event)) {
    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);
    }

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

    return result;
    }

    嗯哼,这段源码不长,除了注释就剩下不到100行了。该方法的输入参数为event它表示Touch事件,这个很好理解;那么它的返回值有是什么含义呢?该boolean值表示的是Touch事件是否被消费。

    在此,对该部分源码的核心部分和主要逻辑做一个梳理

    第一步:
    调用TouchListener中的onTouch()处理Touch事件,请参见代码第31-32行

    该if判断中一共包含了4个条件,必须同时满足时才表示Touch事件被消费

    1. li != null
      ListenerInfo是View中的一个静态类,包含了几个Listener,比如TouchListener,FocusChangeListener,LayoutChangeListeners,ScrollChangeListener等等。一般情况下它均不为null,所以我们不用过多关注它。
    2. li.mOnTouchListener != null
      mOnTouchListener是由View设置的,比如mButton.setOnTouchListener()。所以如果View设置了Touch监听那么,那么mOnTouchListener不空;反之,mOnTouchListener为null
    3. (mViewFlags & ENABLED_MASK) == ENABLED
      当前View可用(ENABLED)。通常可调用view.setEnabled( )设置View是否可用
    4. li.mOnTouchListener.onTouch(this, event)
      这一点其实是在li.mOnTouchListener != null的基础上继续判断。判断TouchListener的onTouch( )方法是否消耗了Touch事件。返回值为true表示消费掉该事件,false表示未消费。

    在这四个条件中,我们通常最关心的就是最后一个:TouchListener的onTouch()方法。假如这四个条件中的任意一个不满足,那么result仍为false;则进入下一步

    第二步:
    调用View自身的onTouchEvent()处理Touch事件,请参见代码第36-38行

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

    嗯哼,看到了吧:如果在上一步中Touch事件被消费result为true,就不会执行这三行代码了。该处调用了onTouchEvent()若该方法返回值false那么dispatchTouchEvent()的返回值也为false;反之,若该方法返回值为true,那么dispatchTouchEvent()的返回值亦为true。
    既然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();

    if ((viewFlags & ENABLED_MASK) == DISABLED) {

    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
    }

    return (((viewFlags & CLICKABLE) == CLICKABLE
    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
    return true;
    }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
    switch (action) {
    case MotionEvent.ACTION_UP:
    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    boolean focusTaken = false;
    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
    focusTaken = requestFocus();
    }

    if (prepressed) {
    setPressed(true, x, y);
    }

    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {

    removeLongPressCallback();

    if (!focusTaken) {

    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)) {
    mUnsetPressedState.run();
    }

    removeTapCallback();
    }
    mIgnoreNextUpEvent = false;
    break;

    case MotionEvent.ACTION_DOWN:
    mHasPerformedLongPress = false;

    if (performButtonActionOnTouchDown(event)) {
    break;
    }

    boolean isInScrollingContainer = isInScrollingContainer();

    if (isInScrollingContainer) {
    mPrivateFlags |= PFLAG_PREPRESSED;
    if (mPendingCheckForTap == null) {
    mPendingCheckForTap = new CheckForTap();
    }
    mPendingCheckForTap.x = event.getX();
    mPendingCheckForTap.y = event.getY();
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    } else {
    setPressed(true, x, y);
    checkForLongClick(0);
    }
    break;

    case MotionEvent.ACTION_CANCEL:
    setPressed(false);
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    break;

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

    if (!pointInView(x, y, mTouchSlop)) {
    removeTapCallback();
    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {

    removeLongPressCallback();

    setPressed(false);
    }
    }
    break;
    }

    return true;
    }

    return false;
    }

    这段代码稍微复杂一些,在此分析几个核心点。

    1. 当View为disable时对于Touch的处理,请参见代码第7-16行。
      若一个View是disable的,如果它是CLICKABLE或者LONG_CLICKABLE或CONTEXT_CLICKABLE的就返回true,表示消耗掉了Touch事件。
      但是请注意,该view所对应的ClickListener.onClick( )不会有任何的响应。即官方文档的描述:

      A disabled view that is clickable still consumes the touch events, it just doesn’t respond to them.

      若View虽然是disable的,但只要满足这三个条件中的一个,它就会消费掉Touch事件但不再回调view的onClick( )方法

    2. 处理ACTION_DOWN,ACTION_MOVE,ACTION_UP事件等,请参见代码第24-116行。
      在此请注意在对于ACTION_UP的处理时调用了performClick(),请参见代码第50行。

      public boolean performClick() {
      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);
      return result;
      }

      在该方法中调用了view的mOnClickListener.onClick( ),请参见代码第6行。
      嗯哼,看到了吧:我们平常见得很多的Click事件是在View的onTouchEvent( )中处理ACTION_UP时调用的。

    3. 返回onTouchEvent()方法的输出结果,请参见代码第118-121行。
      在该处请尤其注意:
      如果View是enable的,只要该View满足CLICKABLE和LONG_CLICKABLE以及CONTEXT_CLICKABLE这三者的任意一个(请参见代码第24-26行)不论当前的action是什么,该onTouchEvent()返回的均是true(请参见代码第118行);而且会在ACTION_UP时处理click事件。
      同理,如果这三个条件都不满足,该onTouchEvent()返回的是false。
      也请注意一个细节:
      View的clickable属性视不同的子View有所差异
      比如:Button的clickable默认为true,但是TextView的clickable属性默认为false。
      View的longClickable属性默认为false。
      当然,我们可以通过代码修改这些默认的属性。
      比如:setClickable()和setLongClickListener()可以改变View的CLICKABLE和LONG_CLICKABLE属性。
      除此以外,通过设置监听器也可改变某些属性。
      比如:setOnClickListener()会将View的CLICKABLE设置为true;setOnLongClickListener()会将View的LONG_CLICKABLE设置为true。

    第三步:
    返回Touch事件是否被消费,请参见代码第52行

    以上就为View对于Touch事件的主要步骤。
    在此我画了一个简单的流程图,现结合该图和刚才的源码分析对View的Touch事件处理流程做一个总结。

    这里写图片描述

    1. View处理Touch事件的总体流程
      dispatchTouchEvent()—>onTouch()—>onTouchEvent()—>onClick()
      Touch事件最先传入dispatchTouchEvent()中;如果该View存在TouchListener那么会调用该监听器中的onTouch()。在此之后如果Touch事件未被消费,则会执行到View的onTouchEvent()方法,在该方法中处理ACTION_UP事件时若该View存在ClickListener则会调用该监听器中的onClick()
    2. onTouch()与onTouchEvent()以及click三者的区别和联系
      2.1 onTouch()与onTouchEvent()都是处理触摸事件的API
      2.2 onTouch()属于TouchListener接口中的方法,是View暴露给用户的接口便于处理触摸事件,而onTouchEvent()是Android系统自身对于Touch处理的实现
      2.3 先调用onTouch()后调用onTouchEvent()。而且只有当onTouch()未消费Touch事件才有可能调用到onTouchEvent()。即onTouch()的优先级比onTouchEvent()的优先级更高。
      2.4 在onTouchEvent()中处理ACTION_UP时会利用ClickListener执行Click事件。所以Touch的处理是优先于Click的
      2.5 简单地说三者执行顺序为:onTouch()–>onTouchEvent()–>onClick()
    3. View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有,请勿混淆

    关于View对Touch事件的处理就分析到此。

    滴滴,到站了,下车的乘客们请往后门走。

    PS:若觉得文章太长,那就直接看视频吧。


    who is the next one? ——> 详解ViewGroup的Touch事件分发



沪ICP备19023445号-2号
友情链接