>  기사  >  Java  >  Android Touch 이벤트 분포에 대한 심층적인 이해

Android Touch 이벤트 분포에 대한 심층적인 이해

高洛峰
高洛峰원래의
2017-01-16 16:43:59988검색

이 글에서는 터치 이벤트의 분포에 대해 자세히 알아보겠습니다.
1. 터치 동작 및 이벤트 순서

(1) 터치 이벤트의 동작

총 터치 동작에는 ACTION_DOWN, ACTION_MOVE, ACTION_UP의 세 가지 유형이 있습니다. 사용자의 손가락이 화면을 터치하면 ACTION_DOWN 액션의 터치 이벤트가 발생하고, 사용자의 손가락이 화면을 바로 벗어나면 터치 후 계속해서 슬라이드하는 경우 ACTION_UP 액션의 터치 이벤트가 생성됩니다. 슬라이딩 거리가 시스템에서 미리 정의된 거리 상수에 도달하면 시스템에서 미리 정의된 거리 상수를 사용하여 터치 이벤트가 생성되어 화면에서 사용자의 손가락이 미끄러지는지 여부를 확인합니다. ACTION_MOVE 작업은 TouchSlop이라고 하며 ViewConfiguration을 통해 전달될 수 있습니다. get(getContext()).getScaledTouchSlop() 가져옵니다.

(2) 이벤트 시퀀스

사용자의 손가락이 화면을 터치하고 화면을 슬라이드한 다음 화면을 벗어나면 이 프로세스는 일련의 터치 이벤트를 생성합니다. ACTION_DOWN--> ; 여러 ACTION_MOVE -->ACTION_UP. 이 일련의 터치 이벤트는 이벤트 시퀀스입니다.

2. 터치 이벤트 배포

(1) 개요

터치 시간이 발생하면 시스템이 터치 이벤트를 View(TargetView) 처리하는 역할을 담당합니다. , TargetView에 터치 이벤트를 전달하는 프로세스는 터치 이벤트의 배포입니다.

터치 이벤트 배포 순서: Activity-->최상위 보기-->최상위 보기의 하위 보기--> .-->대상 보기

터치 이벤트 응답 순서: TargetView --> 최상위 보기 -->Activity

(2) toush 이벤트 배포.

a. 액티비티별 터치 이벤트 배포

사용자의 손가락이 화면을 터치하면 터치 이벤트를 캡슐화하는 MotionEvent가 먼저 생성됩니다. Activity의 dispatchTouchEvent 메서드는 터치 이벤트 배포를 담당합니다. 터치 이벤트를 배포하는 실제 작업은 현재 활동의 창에 의해 수행되며 창은 터치 이벤트를 DecorView(현재 사용자 인터페이스의 최상위 보기)에 전달합니다. Activity의 dispatchTouchEvent 메소드 코드는 다음과 같습니다.

public boolean dispatchTouchEvent(MotionEvent ev) {
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    onUserInteraction();
  }
  if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
  }
  return onTouchEvent(ev);
}

위의 코드를 보면 PhoneWindow의 superDispatchTouchEvent 메소드가 실제로 DecorView의 superDispatchTouchEvent 메소드를 통해 작업을 완료하는 것을 알 수 있습니다. 즉, 현재 Activity의 Window가 이 터치를 직접 전달합니다. 이벤트가 DecorView로 전달됩니다. 즉, 터치 이벤트는 Activity-->Window-->DecorView와 같이 배포되었습니다.

b. 최상위 뷰에 의한 터치 이벤트 배포

Activity 및 Window별로 배포한 후 이제 터치 이벤트가 DecorView의 dispatchTouchEvent 메서드로 전달되었습니다. DecorView는 본질적으로 ViewGroup(구체적으로는 FrameLayout)입니다. ViewGroup의 dispatchTouchEvent 메서드로 수행되는 작업은 다음과 같은 단계로 나눌 수 있습니다.

//Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
  //Throw away all previous state when starting a new touch gesture.
  //The framework may have dropped the up or cancel event for the previous gesture due to an app switch, ANR, or some other state change.
  cancelAndClearTouchTargets(ev);
  resetTouchState();
}

첫 번째 단계가 있습니다. 두 가지 주요 작업이 있습니다. 하나는 6행의 ResetTouchState 메소드에서 FLAG_DISALLOW_INTERCEPT 표시의 재설정을 완료하는 것이고, 다른 하나는 5행의 cancelAndClearTouchTargets 메소드가 현재 MotionEvent의 터치 대상을 지우는 것입니다. FLAG_DISALLOW_INTERCEPT 표시 및 터치 대상에 대해서는 아래에 관련 지침이 있습니다.

두 번째 단계의 주요 작업은 현재 ViewGroup이 이 터치 이벤트를 가로채는지 여부를 결정하는 것입니다. 주요 코드는 다음과 같습니다.

//Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWM || mFirstTouchTarget != null) {
  final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
  if (!disallowIntercept) {
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action); //restore action in case it was changed
  } else {
    intercepted = false;
  }
} else {
  //There are no touch targets and this action is not an initial down so this view group continues to intercept touches.
  intercept =true;
}

위 코드에서 터치가 발생했을 때 알 수 있습니다. 이벤트가 ViewGroup에 전달되면 먼저 이 터치 이벤트의 동작이 ACTION_DOWN인지 확인합니다. 이 이벤트가 ACTION_DOWN이거나 mFirstTouchTarget이 null이 아닌 경우 FLAG_DISALLOW_INTERCEPT 플래그를 기반으로 이 터치 이벤트를 가로챌지 여부를 결정합니다. 그렇다면 mFirstTouchTarget은 무엇입니까? ViewGroup의 하위 View에서 터치 이벤트가 성공적으로 처리되면 mFirstTouchTarget은 위에서 발생한 터치 대상인 터치 이벤트를 성공적으로 처리한 View에 할당됩니다.

위 코드의 프로세스를 요약하면 하위 뷰가 ViewGroup의 가로채기를 방해하지 않는 경우(위 코드의 disallowIntercept는 false), 현재 이벤트가 ACTION_DOWN이거나 mFirstTouchTarget이 비어 있지 않은 경우입니다. , ViewGroup이 호출됩니다. onInterceptTouchEvent 메소드는 이 이벤트를 궁극적으로 가로챌지 여부를 결정합니다. 그렇지 않으면(TargetView가 없고 이 이벤트가 ACTION_DOWN이 아님) 현재 ViewGroup이 이 이벤트를 가로챌 것입니다. ViewGroup이 터치 이벤트를 가로채면 mFirstTouchTarget에 값이 할당되지 않습니다. 따라서 ACTION_MOVE 또는 ACTION_UP이 ViewGroup에 전달되면 mTouchTarget은 null이 되므로 위 코드의 3번째 줄 조건은 false가 됩니다. 차단할 것입니다. 여기서 도출할 수 있는 결론은 ViewGroup이 이벤트를 가로채면 기본적으로 가로채기 여부를 묻지 않고 동일한 이벤트 시퀀스의 나머지 이벤트가 가로채어진다는 것입니다(즉, onInterceptTouchEvent는 다시 호출되지 않습니다).

    这里存在一种特殊情形,就是子View通过requestDisallowInterceptTouchEvent方法设置父容器的FLAG_DISALLOW_INTERCEPT为true,这个标记指示是否不允许父容器拦截,为true表示不允许。这样做能够禁止父容器拦截除ACTION_DOWN以外的所有touch事件。之所以不能够拦截ACTION_DOWN事件,是因为每当ACTION_DOWN事件到来时,都会重置FLAG_DISALLOW_INTERCEPT这个标记位为默认值(false),所以每当开始一个新touch事件序列(即到来一个ACTION_DOWN动作),都会通过调用onInterceptTouchEven询问ViewGroup是否拦截此事件。当ACTION_DOWN事件到来时,重置标记位的工作是在上面的第一阶段完成的。  

    接下来,会进入第三阶段的工作:

final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
  // 不是ACTION_CANCEL并且不拦截
  if (actionMasked == MotionEvent.ACTION_DOWN) {
     // 若当前事件为ACTION_DOWN则去寻找这次事件新出现的touch target
     final int actionIndex = ev.getActionIndex(); // always 0 for down
 
     ...
 
     final int childrenCount = mChildrenCount;
     if (newTouchTarget == null && childrenCount != 0) {
       // 根据触摸的坐标寻找能够接收这个事件的touch target
       final float x = ev.getX(actionIndex);
       final float y = ev.getY(actionIndex);
 
       final View[] children = mChildren;
       // 遍历所有子View
       for (int i = childrenCount - 1; i >= 0; i--) {
         final int childIndex = i;
         final View child = children[childIndex];
         // 寻找可接收这个事件并且touch事件坐标在其区域内的子View
         if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
           continue;
         }
 
         newTouchTarget = getTouchTarget(child); // 找到了符合条件的子View,赋值给newTouchTarget
         if (newTouchTarget != null) {
           //Child is already receiving touch within its bounds.
           //Give it the new pointer in addition to ones it is handling.
           newTouchTarget.pointerIdBits |= idBitsToAssign;
           break;
         }
         resetCancelNextUpFlag(child);
         // 把ACTION_DOWN事件传递给子组件进行处理
         if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
           //Child wants to receive touch within its bounds.
           mLastTouchDownTime = ev.getDownTime();
           if (preorderedList != null) {
             //childIndex points into presorted list, find original index
             for (int j=0;j<childrenCount;j++) {
               if (children[childIndex]==mChildren[j]) {
                 mLastTouchDownIndex=j;
                 break;
               }
             }
           } else {
             mLastTouchDownIndex = childIndex;
           }
           mLastTouchDownX = ev.getX();
           mLastTouchDownY = ev.getY();
           //把mFirstTouchTarget赋值为newTouchTarget,此子View成为新的touch事件的起点
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
           alreadyDispatchedToNewTouchTarget = true;
           break;
         }           
       }
     }
  }
}

    当ViewGroup不拦截本次事件,则touch事件会分发给它的子View进行处理,相关代码从第21行开始:遍历所有ViewGroup的子View,寻找能够处理此touch事件的子View,若一个子View不在播放动画并且touch事件坐标位于其区域内,则该子View能够处理此touch事件,并且会把该子View赋值给newTouchTarget。

    若当前遍历到的子View能够处理此touch事件,就会进入第38行的dispatchTransformedTouchEvent方法,该方法实际上调用了子View的dispatchTouchEvent方法。dispatchTransformedTouchEvent方法中相关的代码如下:

if (child == null) {
  handled = super.dispatchTouchEvent(event);
} else {
  handled = child.dispatchTouchEvent(event);
}

    若dispatchTransformedTouchEvent方法传入的child参数不为null,则会调用child(即处理touch事件的子View)的dispatchTouchEvent方法。若该子View的dispatchTouchEvent方法返回true,则dispatchTransformedTouchEvent方法也会返回true,则表示成功找到了一个处理该事件的touch target,会在第55行把newTouchTarget赋值给mFirstTouchTarget(这一赋值过程是在addTouchTarget方法内部完成的),并跳出对子View遍历的循环。若子View的dispatchTouchEvent方法返回false,ViewGroup就会把事件分发给下一个子View。

    若遍历了所有子View后,touch事件都没被处理(该ViewGroup没有子View或是所有子View的dispatchTouchEvent返回false),ViewGroup会自己处理touch事件,相关代码如下:

if (mFirstTouchTarget == null) {
  handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}

    由以上代码可知,ViewGroup自己处理touch事件时,会调用dispatchTransformedTouchEvent方法,传入的child参数为null。根据上文的分析,传入的chid为null时,会调用super.dispatchTouchEvent方法,即调用View类的dispatchTouchEvent方法。 

c. View对touch事件的处理

    View的dispatchTouchEvent方法的主要代码如下:

public boolean dispatchTouchEvent(MotionEvent event) {
  boolean result = false;
  . . .
   
  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;
}

    由上述代码可知,View对touch事件的处理过程如下:由于View不包含子元素,所以它只能自己处理事件。它首先会判断是否设置了OnTouchListener,若设置了,会调用onTouch方法,若onTouch方法返回true(表示该touch事件已经被消耗),则不会再调用onTouchEvent方法;若onTouch方法返回false或没有设置OnTouchListener,则会调用onTouchEvent方法,onTouchEvent对touch事件进行具体处理的相关代码如下:

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) {
        . . .
        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) {
              mPerformClck = new PeformClick();
            }
            if (!post(mPerformClick)) {
              performClick();
            }
          }
        }
        . . .
      }
      break;
  }
  . . .
  return true;
}

    由以上代码可知,只要View的CLICKABLE属性和LONG_CLICKABLE属性有一个为true(View的CLICKABLE属性和具体View有关,LONG_CLICKABLE属性默认为false,setOnClikListener和setOnLongClickListener会分别自动将以上俩属性设为true),那么这个View就会消耗这个touch事件,即使这个View处于DISABLED状态。若当前事件是ACTION_UP,还会调用performClick方法,该View若设置了OnClickListener,则performClick方法会在其内部调用onClick方法。performClick方法代码如下:

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

以上是我学习Android中触摸事件分发后的简单总结,很多地方叙述的还不够清晰准确

更多Android Touch事件分发深入了解相关文章请关注PHP中文网!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.