首頁 Java java教程 Android Touch事件分發深入了解

Android Touch事件分發深入了解

Jan 16, 2017 pm 04:43 PM

本文帶著大家深入學習觸摸事件的分發,具體內容如下
1. 觸摸動作及事件序列

(1)觸摸事件的動作

    觸摸動作一共有三種:ACTION_DOWN、ACTION_MOVE、ACTION_UP。當使用者手指接觸螢幕時,就會產生一個動作為ACTION_DOWN的觸控事件,此時​​若使用者的手指立即離開螢幕,會產生一個動作為ACTION_UP的觸控事件;若使用者手指接觸螢幕後持續滑動,當滑動距離超過了系統中預先定義的距離常數,則產生一個動作為ACTION_MOVE的觸摸事件,系統中預先定義的用來判斷用戶手指在屏幕上的滑動是否是一個ACTION_MOVE動作的這個距離常數叫做TouchSlop,可通過ViewConfiguration. get(getContext()).getScaledTouchSlop()取得。

(2)事件序列

    當使用者的手指接觸螢幕,在螢幕上滑動,又離開螢幕,這個過​​程會產生一連串觸控事件:ACTION_DOWN-->若干個ACTION_MOVE-->ACTION_UP。這一系列觸摸事件即為一個事件序列。

2. 觸摸事件的分發

(1)概述

    當產生了一個觸摸時間後,系統要負責把這個觸摸事件給一個View(TargetView)來處理,touch事件傳遞到TargetView的過程即為touch事件的分發。

    觸摸事件的分發順序:Activity-->頂級View-->頂級View的子View-->. . .-->Target View

    觸摸事件的回應順序:TargetView --> TargetView的父容器- -> . . . -->頂級View -->Activity

(2)toush事件分發的具體過程

  a. Activity對touch事件的分發

    當用戶手指接觸屏幕時,便產生了一個touch事件,封裝了touch事件的MotionEvent最先傳遞給目前Activity,Activity的dispatchTouchEvent方法負責touch事件的分發。分發touch事件的實際工作由目前Activity的Window完成,而Window會將touch事件傳遞給DecorView(目前使用者介面頂級View)。 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。也就是說,目前touch事件已經經過如下的分發:Activity-->Window-->DecorView。

b. 頂級View對touch事件的分發

    經過Activity與Window的分發,現在touch事件已經被傳遞到了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的touch target。關於FLAG_DISALLOW_INTERCEPT標記和touch target,在下文會有相關說明。

    第二階段的主要工作是決定當前ViewGroup是否攔截本次的touch事件,主要代碼如下:

//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;
}
登入後複製

    由以上代碼我們可以知道,當一個touch事件被傳遞到ViewGroup時,會先判斷這個touch事件的動作是否為ACTION_DOWN,如果這個事件是ACTION_DOWN或mFirstTouchTarget不為null,就會根據FLAG_DISALLOW_INTERCEPT標記決定是否攔截這個touch事件。那麼mFirstTouchTarget是什麼呢?當touch事件被ViewGroup的子View成功處理時,mFirstTouchTarget就會被賦值為成功處理touch事件的View,也就是上面提高的touch target。

    總結上述程式碼的流程:在子View不干預ViewGroup的攔截的情況下(上述程式碼中的disallowIntercept為false),若目前事件為ACTION_DOWN或mFirstTouchTarget不為空,則會呼叫ViewGroup的onInterceptTouchEvent方法來決定最終是否攔截此事件;否則(沒有TargetView且此事件不是ACTION_DOWN),當前ViewGroup就會攔截下此事件。 一旦ViewGroup攔截了某次touch事件,那麼mFirstTouchTarget就不會被賦值,因此當再有ACTION_MOVE或是ACTION_UP傳遞到該ViewGroup時,mTouchTarget就為null,所以上述程式碼第3行的條件就為false,ViewGroup會攔截下來。由此可得的結論是:一旦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

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型? Java的類負載機制如何起作用,包括不同的類載荷及其委託模型? Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存? 如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存? Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射? 如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射? Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案? 如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案? Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)? 如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)? Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

See all articles