View触摸事件相关分析

事件的类型,主要有六种:

  • MotionEvent.ACTION_DOWN:第一个手指刚接触屏幕
  • MotionEvent.ACTION_POINTER_DOWN:多点触摸第二个手指接触屏幕
  • MotionEvent.ACTION_MOVE:手指在屏幕上滑动,如果是多点触摸则无法明确区分具体是哪一个手指在滑动
  • MotionEvent.ACTION_POINTER_UP:多点触摸有某个手指离开屏幕,但不是最后一个手指
  • MotionEvent.ACTION_UP:最后一个手指离开屏幕
  • MotionEvent.CANCEL:非人为因素取消

事件序列

正常情况下,事件序列会由一个DOWN开始、一个UP结尾和0-n个中间事件,比如MOVE、POINTER_DOWN、POINTER_UP组成。

事件分发

事件分发是从Activity --> ViewGroup --> View。

Activity基本不做事,但是可以增加事件序列最开头的响应,和无人消费事件的最末尾响应。

ViewGroup主要职责:

  • 在DOWN事件清空上次操作事件的状态位和重置mFirstTouchTarget即事件响应的View链表集合
  • 在DOWN事件遍历所有子View询问它们是否要消耗事件,如果有就记录到mFirstTouchTarget集合
  • 可以重写onInterceptTouchEvent方法决定是否对事件序列进行拦截,该方法默认是不拦截的
  • 分发所有事件,比如MOVE、UP等
  • 如果有子View响应了事件,即mFirstTouchTarget有记录,那么事件序列的后续事件直接交给mFirstTouchTarget集合进行处理,不用再遍历子View,所以mFirstTouchTarget的设计理念就是性能优化,去掉了DOWN后续事件的遍历操作。
  • 如果没有子View响应事件,那么交由自己的onTouchEvent进行处理,如果自己也不消费事件,那么往上回传,最后可能会传回给Activity;否则,自己消费所有的后续事件序列。

View的职责:

  • 判断当前View是ENABLE或DISENABLE
  • 判断当前View是可点击或不可点击状态,在可点击状态下,View是默认消费事件的。
  • 在DOWN事件设置长按监听,如果超过500毫秒没有UP事件响应,那么长按事件回调。如果这个回调结果代表消费事件,那么不会响应单击事件;否则还可以响应单击事件。
  • 在UP事件响应单击事件。会首先响应listener的onTouch(),该方法结果如果代表消费事件,那么onTouchEvent()不会回调;否则可以继续回调onTouchEvent();再看这个方法的结果决定是否回调View.onClickListener的回调。

具体可以看看下面的源码分析

事件分发–Activity

// Activity 这里是事件分发的入口
public boolean dispatchTouchEvent(MotionEvent ev) {
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    // 可以重写这个方法实现事件最开始的处理,但是不能拦截事件
    onUserInteraction();
  }
  // 交给下层View去处理,注意window就是PhoneWindow,这里最后会进入ViewGroup的方法,即dispatchTouchEvent(),就是下面会分析的步骤
  if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
  }
  // 如果所有的View都不处理事件,那么交给Activity自己处理,虽然实现只是简单的丢弃
  return onTouchEvent(ev);
}

事件分发–ViewGroup

// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
  // 记住,事件序列是从DOWN开始
  if (actionMasked == MotionEvent.ACTION_DOWN) {
    // 所以,这里需要清空之前的事件序列,包括把mFirstTouchTarget和FLAG_DISALLOW_INTERCEPT重置
    // mFirstTouchTarget是一个链表,代表要响应这次事件序列的View集合
    // FLAG_DISALLOW_INTERCEPT,可以禁止或允许ViewGroup拦截除了DOWN之外的事件,getParent().requestDisallowInterceptTouchEvent()
    cancelAndClearTouchTargets(ev);
    resetTouchState();
  }
  
  // 检查是否需要拦截事件 
  // 如果当前事件是DOWN或mFirstTouchTarget不等于null才执行
  if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    // 因为DOWN事件会重置FLAG_DISALLOW_INTERCEPT,所以无法拦截DOWN事件
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
      // ViewGroup默认不拦截任何事件,下面的方法默认返回fasle
      intercepted = onInterceptTouchEvent(ev);
      ev.setAction(action); 
    } else {
      intercepted = false;
    }
  }
  
  // 如果是DOWN事件且不拦截,看是否交给子View去处理DOWN事件
  if(!intercepted && actionMasked == MotionEvent.ACTION_DOWN) {
    // 倒叙遍历子View,看是否需要响应事件
    for (int i = childrenCount - 1; i >= 0; i--) {
      // 当前View是可见的且没有执行动画且在点击范围内才能响应事件,否则continue跳过
      if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
        continue;
      }
      if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // 方法返回true,表示当前View已经处理了事件,保存mFirstTouchTarget
        addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
      }
    }
  }
  
  //  分发事件,包括DOWN事件之外的所有事件
  if (mFirstTouchTarget == null) {
    // 因为没有子View处理事件,所以需要自己处理事件,最后会调用onTouchEvent(),这里的null就是对应下面函数中的child
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
  } else {
    // 有子View处理事件,那么遍历所有需要处理事件的子View,让他们去处理事件
    while (target != null) {
      final TouchTarget next = target.next;
      if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
        // DOWN事件,已经处理了
        handled = true;
      } else {
        // 除DOWN之外的事件,子View接着处理
        if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
          handled = true;
        }
      }
      target = next;
    }
  }
  
  // 后面就是处理UP和POINTER_UP事件,主要是重置相关和移除touch target
  // ... 
}

private boolean dispatchTransformedTouchEvent(View child){
  if (child == null) {
    // 没有子View了,需要自己处理,最后会调用onTouchEvent(),下面还会细说这个方法,因为还可以增加listener
    handled = super.dispatchTouchEvent(event);
  } else {
    // 交给子View去处理事件,完成了事件从ViewGroup到View的事件分发
    handled = child.dispatchTouchEvent(event);
  }
  return handled;
}

事件分发–View

// View
// 这个方法总结一下:如果有listener,那么先由listener处理事件并给出一个结果,当结果为true,那么表示已经消耗了事件,onTouchEvent()不会执行,否则onTouchEvent()会执行;如果没有listener,直接执行onTouchEvent()
public boolean dispatchTouchEvent(MotionEvent event) {
  ListenerInfo li = mListenerInfo;
  // 当前View是ENABLED状态
  if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
  }
  if (!result && onTouchEvent(event)) {
    result = true;
  }
}

public boolean onTouchEvent(MotionEvent event) {
  // 判断View是否clickable,如果设置了CLICKABLE或LONG_CLICKABLE,代表会消耗事件,但是不响应它们。
  // 然后分别处理DOWN、MOVE、UP、CANCEL事件
  if (clickable) {
    switch (event.getActionMask()) {
      case DOWN:
      	// 是否执行了长按事件
      	mHasPerformedLongPress = false;
      	if (isInScrollingContainer) {
        	// 在滑动容器内部,设置预按下状态
        	mPrivateFlags = PFLAG_PREPRESSED;
        	// 100毫米后,增加长按事件监听,500毫秒
      		checkForLongClick();
      	} else {
        	// 非滑动容器内部,设置按下状态
        	mPrivateFlags = PFLAG_PRESSED;
        	// 增加长按事件监听,500毫秒
      		checkForLongClick();
      	}
        break;
      case UP:
      	// 包含PFLAG_PRESSED或PFLAG_PREPRESSED事件才继续,DOWN事件已经设置了
      	boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
      	// 如果响应了长按事件,且把mHasPerformedLongPress设置为true,则不会响应点击事件
      	if (!mHasPerformedLongPress) {
        	// 没有超过500毫秒,不需要处理长按事件,先移除长按事件监听
        	removeLongPressCallback();
        	// 再响应点击事件
        	performClickInternal();
      	}
      	break;
    }
    // View在Clickable的时候默认返回true,代表会消耗事件
    return true;
  } else {
    return false;
  }
  
}

// 增加长按事件监听
private void checkForLongClick(){
  // 简化代码如下,如果消耗了长按事件,那么设置标记位,该标记位主要用于UP事件决定是否响应点击事件
  if (performLongClick(mX, mY)) {
    mHasPerformedLongPress = true;
  }
}

// 调用用户设置的长按事件并返回结果代表是否消耗了该事件
private boolean performLongClickInternal(float x, float y) {
  if (li != null && li.mOnLongClickListener != null) {
    handled = li.mOnLongClickListener.onLongClick(View.this);
  }
  return handled;
}

多点触摸

每个手指都会对应一个pointerIndex和pointerId,二者的区别在于index会随着手指的按下或离开而发生动态变化,而id不会,它唯一标记了当前的手指。比如有三个手指,一开始index和id分别都为1、2、3,这个时候第二个手指离开屏幕,这时候两个手指的index为1、2,而id为1、3。

ACTION_MOVE是无法确定哪只手指在移动,比如现在屏幕上有三个手指,无论你移动一个手指还是全部手指,都会触发MOVE事件。

触摸事件序列是针对View,而不是某个pointer,所以某个pointer或手指的事件这种说法是错误的。

多点触摸的三种类型:

  • 接力型,一个时刻只有一个Pointer在起作用,比如RecyclerView,具体实现的关键在于记录最新的Pointer,根据这个Pointer的状态去更新画面。
  • 配合型,所有Pointer配合起作用,比如ScaleGestureDetector,具体实现的关键在于需要遍历所有的Pointer,根据某种计算规则(作者规定)去得出结果,然后去更新画面。
  • 各自为战,自己玩自己的。比如支持多画笔的画板应用,具体实现的关键在于在DOWN和POINTER_DOWN记录每个Pointer的id,然后在MOVE的时候根据id去追逐更新。

系统提供的事件相关的工具类

  • GestureDetector:普通触摸事件的区分,比如单击、长按、双击、快速滑动等等
  • VelocityTracker:帮助追踪触摸事件的速度
  • OverScroller:用于自动计算滑动的偏移,多用于辅助实现滑动和快速滑动动画
  • ScaleGestureDetector:用于检测缩放动作,比如双指放大或缩小图片
  • OnDragListner:API11增加,用于拖拽操作,可以附加拖拽数据,原理是创造出一个新的图像在屏幕的最上层,跟着用户的手指移动。通过mViewRootImpl拿到Surface,在Surface的Canvas上draw一个新的图像,具体的绘制看DragShadowBuilder代码相关,我们可以重写onDrawShadow方法绘制任何内容。
  • ViewDragHelper:v4的support包增加,也用于拖拽操作,不可以附加拖拽数据,原理是直接修改被拖拽的子View上下左右的属性。
发布了8 篇原创文章 · 获赞 1 · 访问量 2866

猜你喜欢

转载自blog.csdn.net/lneartao/article/details/105023971