Android觸摸回饋

  • 2020 年 3 月 27 日
  • 筆記

事件分發

  • 當點擊事件發生時,事件最先傳遞給Activity,Activity會首先將事件將被所屬的Window進行處理,即調用 superDispatchTouchEvent() 方法。通過觀察superDispatchTouchEvent()方法的調用鏈,我們可以發現事件的傳遞順序:
  1. PhoneWinodw.superDispatchTouchEvent()
  2. DecorView.dispatchTouchEvent(event)
  3. ViewGroup.dispatchTouchEvent(event)
  • 事件一層層傳遞到了ViewGroup里。
  • 每到一個子view,看他的onInterceptTouchEvent 方法是否攔截,ontouch是否消費方法,如果沒有繼續向下dispatchTouchEvent分發事件,都不處理向上傳,當回到頂級,若頂層(activity)也不對此事件進行處理,此事件相當於消失了(無效果)。
  • View沒有onInterceptTouchEvent()方法,有dispatchTouchEvent,一但有點擊事件傳遞給它,它的ouTouchEvent()方法就會被調用。
  • ouTouchEvent是否消費事件取決於 ACTION_DOWN 事件 或 POINT_DOWN 事件是否返回 為true

遞歸 ViewGroup(View).dispatchTouchEvent()

  • ViewGroup.onInterceptTouchEvent()
  • child.dispatchTouchEvent()
  • super.dispatchTouchEvent()
    • View.onTouchEvent()

一個場景

有一個 ViewGroup, 然後手指頭接觸 Button ,手指頭滑開了,滑開又鬆手的過程,整個事件發生了什麼?經歷了什麼?

一開始ViewGroup 會接受到整個事件序列的第一個事件: ACTION_DOWN,ViewGroup#dispatchTouchEvent 收到ACTION_DOWN 後,   開始詢問 ViewGroup#onInterceptTouchEvent 是否需要攔截,   默認情況下 ViewGroup#onInterceptTouchEvent 返回false 不攔截,開始向下傳遞ACTION_DOWN 事件,   Buttton#dispatchTouchEvent 收到ACTION_DOWN 詢問onTouchEvent 是否處理,   Button 默認處理,此後的所有事件序列都直接跨過 ViewGroup#onInterceptTouchEvent 的判斷直接傳遞給Button,   但 ViewGroup#dispatchTouchEvent 會收到所有事件。隨著手指的滑動Button 的坐標發生了改變,當手指抬起時觸發 Button#onClick 事件。(移動出自己的範圍,就消失了)

事件衝突

不同向嵌套

  1. 外部處理,重寫父view的onInterceptTouchEvent ,MotionEvent的事件全部返回false,不攔截;
  2. 內部處理。重寫子view的dispatchTouchEvent,通過requestDisallowInterceptTouchEvent方法(這個方法可以在子元素中干預父元素的事件分發過程),請求父控制項不攔截自己的事件,true是不攔截,false是攔截。

同向嵌套 父 View 會徹底卡住子 View 原因:搶奪條件一致,但 父 View 的 onInterceptTouchEvent() 早於子View 的 dispatchTouchEvent() 本質上是策略問題:嵌套狀態下用戶手指滑動,他是想滑誰? 解決⽅方案: 實現策略—父 View、子 View 誰來消費事件可以實時協商

  1. 換成 NestedScrollView:可以滑動
  2. 實現 NestedScrollingChild3 介面來實現自定義的嵌套滑動邏輯

自定義單 View 的觸摸回饋

View.onTouchEvent()

  • 當用戶按下(ACTION_DOWN):
    • 如果不在滑動控制項中,切換至按下狀態,並註冊長按計時器
    • 如果在滑動控制項中,切換至預按下狀態,並註冊按下計時器
  • 當進入按下狀態並移動(ACTION_MOVE):
    • 重繪 Ripple Effect
    • 如果移動出自己的範圍,自我標記本次事件失效,忽略後續事件
  • 當用戶抬起(ACTION_UP):
    • 如果是按下狀態並且未觸髮長按,切換至抬起狀態並觸發點擊事件,並清除⼀切狀態
    • 如果已經觸髮長按,切換至抬起狀態並清除一切狀態
  • 當事件意外結束(ACTION_CANCEL):
    • 切換至抬起狀態,並清除一切狀態

View.dispatchTouchEvent()

View中 setOnTouchListener的onTouch,onTouchEvent,onClick的執行順序 View的dispatchTouchEvent源碼

public boolean dispatchTouchEvent(MotionEvent event) {          if (!onFilterTouchEventForSecurity(event)) {              return false;          }            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                  mOnTouchListener.onTouch(this, event)) {              return true;          }          return onTouchEvent(event);      }  }

當以下三個條件任意一個不成立時

  1. mOnTouchListener不為null
  2. view是enable的狀態
  3. mOnTouchListener.onTouch(this, event)返回true 函數會執行到onTouchEvent。在這裡我們可以看到,首先執行的是mOnTouchListener.onTouch的方法,然後是onTouchEvent方法繼續追溯源碼,到onTouchEvent()觀察,發現在處理ACTION_UP事件里有這麼一段程式碼
if (!post(mPerformClick)) {        performClick();    }

此時可知,onClick方法也在最後得到了執行所以三者的順序是:

  1. setOnTouchListener() 的onTouch
  2. onTouchEvent()
  3. onClick()

view的事件分發:View為啥會有dispatchTouchEvent方法? View可以註冊很多事件監聽器,事件的調度順序是onTouchListener> onTouchEvent>onLongClickListener> onClickListener

自定義 ViewGroup 的觸摸回饋

  • 除了重寫 onTouchEvent() ,還需要重寫 onInterceptTouchEvent()
  • onInterceptTouchEvent() 不用在第一時間返回 true,而是在任意事件,需要攔截的時候返回 true 就行
    //偽程式碼      view.dispatchTouchEvent();        public boolean dispatchTouchEvent(MotionEvent event) {          return ontouchEvent();      }        ViewGroup.dispatchTouchEvent();        public boolean dispatchTouchEvent(MotionEvent event) {          boolean result;          if (interceptTouchEvent()) {              result = ontouchEvent();          } else {              result = 子 view的 dispatchTouchEvent ();          }          return result;      }

ViewGroup.dispatchTouchEvent()

  • 如果是用戶初次按下(ACTION_DOWN),清空 TouchTargets 和DISALLOW_INTERCEPT 標記
  • 攔截處理
  • 如果不攔截並且不是 CANCEL 事件,並且是 DOWN 或者 POINTER_DOWN,嘗試把 pointer(手指)通過 TouchTarget 分配給子 View;並且如果分配給了新的子 View,調用 child.dispatchTouchEvent() 把事件傳給子 View
  • 看有沒有 TouchTarget
    • 如果沒有,調用⾃己的 super.dispatchTouchEvent()
    • 如果有,調用 child.dispatchTouchEvent() 把事件傳給對應的子 View(如果有的話)
  • 如果是 POINTER_UP,從 TouchTargets 中清除 POINTER 資訊;如果是 UP 或CANCEL,重置狀態

TouchTarget 作用:記錄每個子 View 是被哪些 pointer(手指)按下的 結構:單向鏈表

一般自定義onTouchEvent方法流程

  1. 在down的時候去記錄坐標點 getX/getY獲取相對於當前View左上角的坐標,getRawX/getRawY獲取相對於螢幕左上角的坐標。比如接觸到按鈕時,x,y是相對於該按鈕左上點的相對位置。而rawx,rawy始終是相對於螢幕的位置。
  2. move的時候計算偏移量,並用scrollTo()或scrollBy()方法移動view。這倆個方法都是快速滑動,是瞬間移動的。注意:滾動的並不是viewgroup內容本身,而是它的矩形邊框。
  3. 在up的時候,判斷應顯示的頁面位置,並計算距離、滑動頁面。