Android事件分发原理

Android 事件分发

阅读完之后,你可以学到以下知识

  • 事件分发原理
  • 解决view之间交互冲突

1、事件组成以及传递顺序

1.1、触摸事件的组成
  • 1个down
  • n个move
  • 1个up
  • 0|1 个cancel
1.2、传递顺序

Activity —> PhoneWindow —> DecorView —> ViewGroup —>… —> View

使用的是责任链设计模式,又上层往下层传递,如果有下层组建消费任务则结束,如果没有消费则交给上层自己处理

1.3、涉及的核心方法
  1. dispatchTouchEvent 分发事件
  2. onInterceptTouchEvent 拦截事件
  3. li.mOnTouchListener.onTouch 由开发者来处理事件
  4. onTouchEvent 处理用户产生的事件
  5. requestDisallowInterceptTouchEvent 控制父view拦截功能

其中1,2,3,4方法会返回一个值

true : 代表事件被处理,不会继续传递

false : 事件继续往下传递

2、View的事件分发

dispatchTouchEvent -> li.mOnTouchListener.onTouch -> onTouchEvent -> li.mOnClickListener.onClick

2.1、dispatchTouchEvent

分发触摸事件

public boolean dispatchTouchEvent(MotionEvent event) {
 		//...
  if (onFilterTouchEventForSecurity(event)) {
   	//...
    //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;
    }
  }
	//...
  return result;
}

第7行到第9行4个条件决定了result的值,result控制onTouchEvent方法调用

  1. li != null(mListenerInfo不为空)

    用户设置了事件(比如点击事件) - 条件为true

  2. li.mOnTouchListener != null (view设置了OnTouchListener)

    设置了触摸事件 - 条件为true

  3. (mViewFlags & ENABLED_MASK) == ENABLED

    控件不可用(默认可用)- 条件为true

  4. li.mOnTouchListener.onTouch(this, event)

    设置触摸事件onTouch方法返回true - 条件为true

2.2、mOnTouchListener.onTouch

触摸事件

public interface OnTouchListener {
  /**
         * Called when a touch event is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         *
         * @param v The view the touch event has been dispatched to.
         * @param event The MotionEvent object containing full information about
         *        the event.
         * @return True if the listener has consumed the event, false otherwise.
         */
  boolean onTouch(View v, MotionEvent event);
}
2.3、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;

  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;
    }
  }
  if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
      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, 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_DOWN:
        break;
      case MotionEvent.ACTION_CANCEL:
        break;
      case MotionEvent.ACTION_MOVE:
        break;
    }
    return true;
  }
  return false;
}

54,55行 case MotionEvent.ACTION_UP 经过一些处理到达performClickInternal方法

private boolean performClickInternal() {
  // Must notify autofill manager before performing the click actions to avoid scenarios where
  // the app has a click listener that changes the state of views the autofill service might
  // be interested on.
  notifyAutofillManagerOnClick();
  return performClick();
}

在看performClick

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

第9行执行了li.mOnClickListener.onClick通知上层控件被点击了

补下OnClickListener源码

public interface OnClickListener {
  /**
         * Called when a view has been clicked.
         *
         * @param v The view that was clicked.
         */
  void onClick(View v);
}

场景1:button注册点击事件,触摸事件

action -> 点击按钮 ,代码执行流程如下

1、上层li.mOnTouchListener.onTouch返回为true:

​ dispatchTouchEvent -> li.mOnTouchListener.onTouch

2、上层li.mOnTouchListener.onTouch返回为false:

​ dispatchTouchEvent -> li.mOnTouchListener.onTouch -> li.mOnClickListener.onClick

总结

  1. 点击事件是在onTouchEvent中触发的
  2. onTouch触摸事件被消费,则不会产生li.mOnClickListener.onClick 事件
  3. 发现按钮点击事件不响应看dispatchTouchEvent 在看 onTouch

3、ViewGroup事件分发

dispatchTouchEvent->onInterceptTouchEvent-> li.mOnTouchListener.onTouch -> onTouchEvent

li.mOnTouchListener.onTouch,onTouchEvent跟View的事件传递是一直的

下面主要分析一下

  • dispatchTouchEvent
  • onInterceptTouchEvent
3.1、dispatchTouchEvent

ViewGroup重写了view的dispatchTouchEvent方法

主要做了一下几件事情

  1. 拦截事件分发
  2. 事件传递给子view
public boolean dispatchTouchEvent(MotionEvent ev) {
    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;
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            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--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    final View target = mMotionTarget;
    if (target == null) {
        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);
    }
    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)) {
        }
        mMotionTarget = null;
        return true;
    }
    if (isUpOrCancel) {
        mMotionTarget = null;
    }
    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;
    }
    return target.dispatchTouchEvent(ev);
}

第13行 有两个条件disallowIntercept || !onInterceptTouchEvent(ev)

  • disallowIntercept

    是否禁用掉事件拦截的功能,默认是false。可通过requestDisallowInterceptTouchEvent修改

  • onInterceptTouchEvent

    拦截控件的触摸功能

源码执行流程

  1. disallowIntercept 为false 尝试事件拦截,进入onInterceptTouchEvent

  2. disallowIntercept 为true 不需要尝试事件拦截,不执行onInterceptTouchEvent

3.2、onInterceptTouchEvent

onInterceptTouchEvent源码

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}
  • onInterceptTouchEvent返回true代表拦截触摸事件

    此后事件将交给当前控件来处理,调用super.dispatchTouchEvent() 也就是View.dispatchTouchEvent()

  • onInterceptTouchEvent返回false代表不拦截触摸事件

    匹配子view,匹配到之后传递给子view,调用child.dispatchTouchEvent(ev)

最后走的是View的事件传递:

dispatchTouchEvent -> li.mOnTouchListener.onTouch -> onTouchEvent -> li.mOnClickListener.onClick

4、事件冲突解决方法

一个布局中有多个可滑动的控件,就会存在滑动冲突的问题

举个栗子:

首页页面组成:Banner、功能菜单、列表item

  • 主view为ListView
  • Banner为ViewPager
  • ListView里面header是一个ViewPager

交互

  • ListView交互是上下滑动
  • ViewPager交互是左右滑动

用户想滑动ViewPager,ListView也滑动了 导致体验很不好

这个案例中ListView是父View,ViewPager是子View

事件传递顺序

ListView -> ViewPager

因为是滑动冲突,我们只要关注滑动事件move即可

下面说下两种解决方法

  1. 内部拦截法
  2. 外部拦截法
4.1、内部拦截法

左右滑动是触发ViewPager

内部指的是ViewPager

dispatchTouchEvent方法 中进行处理冲突

private float mInitialTouchX;
private float mInitialTouchY;

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
  switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      mInitialTouchY = event.getY();
      mInitialTouchX = event.getX();
      break;
    case MotionEvent.ACTION_MOVE:
      float moveX = event.getX() - mInitialTouchX;
      float moveY = event.getY() - mInitialTouchY;
      if (Math.abs(moveX) - Math.abs(moveY) < 0) {
         //核心逻辑 
        getParent().requestDisallowInterceptTouchEvent(true);
      }
      mInitialTouchY = event.getY();
      mInitialTouchX = event.getX();
      break;
    case MotionEvent.ACTION_UP:
      getParent().requestDisallowInterceptTouchEvent(false);
      break;
    default:
      break;
  }
  return super.dispatchTouchEvent(event);
}

核心逻辑:x轴移动距离大于y轴则代表用户操作的是上下滑动。

第16行 getParent().requestDisallowInterceptTouchEvent(true); 禁用父view(ListView)拦截,将事件传递给ViewPager来处理

这样处理之后move事件,一旦符合view的滑动规则,子view告诉父view后续产生的的move事件都交给自己来处理,所以此时只有一个view(ViewPager)能拿到move事件

4.2、外部拦截法

上下滑动是触发ListView

这里外部指的就是ListView

也是在dispatchTouchEvent方法处理

private float mInitialTouchX;
private float mInitialTouchY;

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
  switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      mInitialTouchY = event.getY();
      mInitialTouchX = event.getX();
      break;
    case MotionEvent.ACTION_MOVE:
      float moveX = event.getX() - mInitialTouchX;
      float moveY = event.getY() - mInitialTouchY;
      mInitialTouchY = event.getY();
      mInitialTouchX = event.getX();
      if (Math.abs(moveX) - Math.abs(moveY) > 0) {
         //核心逻辑 
        return true;
      }
      break;
    case MotionEvent.ACTION_UP:
      getParent().requestDisallowInterceptTouchEvent(false);
      break;
    default:
      break;
  }
  return super.dispatchTouchEvent(event);
}

核心逻辑: y轴移动距离大于x轴则代表用户操作的是上下滑动

第18行 return true; 事件拦截后,交给自己处理。事件将不会分发到ViewPager

使用哪种方案合适

  1. 看view冲突是子view还是父view
  2. 滑动意图是希望父view滑动,则采用外部拦截法
  3. 滑动意图是希望子view滑动,则采用内部拦截法

文章有什么不对的地方,请大家斧正

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值