View的事件體系

cryAllen發表於2017-01-22

View雖然不屬於四大元件,但它的作用堪比四大元件,甚至比Receiver和Provider的重要性都大,在Android開發中,Activity承擔這視覺化的功能,同時Android系統提供了很多基礎控制元件,常見的有Button、Textview、CheckBox等。

View基礎知識

什麼是View

View是一種介面層的控制元件的一種抽象,它代表了一個控制元件。除了View,還有ViewGroup,ViewGroup內部包含了許多個控制元件,即一組View,在ViewGroup也繼承了View,這就意味著View本身就可以是單個控制元件也可以是由多個控制元件組成的一組控制元件,通過這種關係形成了View樹的結構。

View結構圖

View的位置引數

View的位置主要由它的四個頂點來決定,分別對應於View的四個屬性:top、left、right、bottom,其中top是左上角縱座標,left是左上角橫座標,right是右下角橫座標,bottom是右下角的縱座標。需要注意的是,這些座標都是相對於View的父容器來說的,因為它是一種相對座標。

View座標軸

在Android中,X軸和Y軸的正方向分別為右和下。那麼就可以得出以下關係:

width = right - left
height = bottom - top

MotionEvent和TouchSlop

在手指接觸螢幕後會產生一系列的事件,典型的事件型別有如下幾種:

  • ACTION_DOWN——手指剛接觸螢幕
  • ACTION_MOVE——手指在螢幕上移動
  • ACTION_UP——手指從螢幕上鬆開的一瞬間

正常情況下,一次手指觸控螢幕的行為會觸發一系列點選事件,如下:

  • 點選螢幕後離開鬆開,事件序列為DOWN->UP
  • 點選螢幕滑動一會再鬆開,事件序列為DOWN->MOVE->.......->MOVE->UP

同時我們可以通過MotionEvent物件我們可以得到點選事件發生的X座標和Y座標。為此,系統提供了兩組方法:getX/getY和getRawX/getRawY,區別很簡單,前者返回的是相對於當前View左上角的X和Y座標,而後者返回的相對於手機螢幕左上角的X和Y座標。

TouchSlop是系統所能識別出的被認為是滑動的最小距離,換句話說,當手指在螢幕上滑動時,如果兩次滑動之間的距離小於這個常量,那麼系統就不認為你是在進行滑動操作,因為滑動的距離太短了,系統就不認為它是滑動,這是一個常量,跟裝置有關。可以通過以下方式獲取:

ViewConfiguration.get(getContext()).getScaledTouchSlop()

View的滑動

在Android裝置中,滑動幾乎是應用的標配,不管是下拉重新整理還是SlidingMenu,它們的基礎都是滑動。因此,掌握滑動的方法是實現絢麗的自定義控制元件的基礎,有三種方式實現View的滑動:

  • scrollTo/scrollBy方法來實現
  • 使用動畫
  • 改變佈局引數

scrollTo/scrollBy方法來實現

首先,我們需要獲取View裡的兩個屬性mScrollX和mScrollY,在滑動過程中,mScrollX的值總是等於View的左邊緣和View內容左邊緣在水平方向的距離,而mScrollY的值總等於View上邊緣和View內容上邊緣在豎直方向的距離。View邊緣是指View的位置,由四個頂點組成,而View內容邊緣是指View中的內容的邊緣,scrollTo/scrollBy只能改變View內容的位置而不能改變View在佈局中的位置。

如果從左往右滑動,那麼mScrollX為負值,反之為正值,如果從上往下滑動,那麼mScrollY為負值,反之為正值。

View滑動

使用動畫

使用動畫我們能夠讓一個View進行平移,而平移就是一個滑動。比如:

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

但是動畫移動會有個問題,那就是View動畫並不能真正改變View的位置,這會帶來一個很嚴重的問題,就是移動到新位置了,卻發現無法觸發事件,而單擊原來的位置卻能觸發。那麼從Android3.0開始,使用屬性動畫可以解決上面問題。

改變佈局引數

改變佈局引數,即改變LayoutParams,這個比較好理解,比如我們想把一個Button向右平移100px,我們只需要將這個Button的LayoutParams裡的marginLeft引數的值增加100px即可。

MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams;
params.width += 100;
parms.leftMargin += 100;
mButton1.requestLayout();

三種滑動方式對比:

  • scrollTo/scrollBy:操作簡單,適合對View內容的滑動
  • 動畫:操作簡單,主要使用於沒有互動的View和實現複雜的動畫效果
  • 改變佈局引數:操作稍微複雜,適用於有互動的View

彈性滑動

知道了View的滑動,我們還需要知道如何實現View的彈性滑動,比較生硬的滑過去,這種方式的使用者體驗實在太差了,因此我們要實現漸近式滑動。如何實現彈性滑動呢,有個共同思想:將一次大的滑動分成若干次小的滑動,並在一個時間段內完成,常見的彈性滑動具體實現方式有很多,比如通過Scroller、Handler#postDelayed,以及Thread#Sleep等等。

View的事件分發機制

點選事件的傳遞規則

點選事件的分發過程由三個很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。

  • dispatchTouchEvent(MotionEvent ev),用來進行事件的分發。
  • onInterceptTouchEvent(MotionEvent event),用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列中,此方法不會被再次呼叫,返回結果表示是否攔截當前事件。
  • onTouchEvent(MotionEvent event),在dispatchTouchEvent方法呼叫,用來處理當前點選事件,返回結果表示是否消耗當前事件。如果不消耗,那麼在同一個事件序列中,當前View無法再次接收到事件。
public boolean dispatchTouchEvent(MotionEvent ev){
  boolean consume = false;
  if(onInterceptTouchEvent(ev)){
    consume = onTouchEvent(ev);
  }
  else{
    consume = child.dispatchTouchEvent(ev);
  }
  return consume;
}

簡單來說,對於一個根ViewGroup來說,點選事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent會被呼叫,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接著事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被呼叫,如果這個ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會被呼叫,如此反覆直到事件被最終處理。

還有一個當View需要處理事件時,如果它設定了OnTouchListener,那麼OnTouchListener中的onTouch方法會被回撥,OnTouchListener優先於onTouchEvent。在onTouchEvent中,如果設定了OnClickListener,那麼它的OnClick方法會被呼叫,可以看出我們平時常用的OnClickListener優先順序最低,onTouch>onClick.

View不處理流程:

View不處理流程

View處理流程

View處理流程

一些總結:

  • 同一個事件序列是指從手指接觸螢幕的那一刻起,到手指離開螢幕的那一刻結束。一般是以down事件開始,中間含有數量不定的move事件,最終以up事件結束。
  • 正常情況下,一個事件序列只能被一個View攔截且消耗。
  • 某個View一旦決定攔截,那麼這個事件序列就只能由它來處理,那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交給它的父元素去處理,即父元素的onTouchEvent會被呼叫。
  • 如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點選事件就會消失,此時父元素的onTouchEvent並不會被呼叫,最終會交給Activity處理。
  • ViewGroup預設不攔截任何事件。
  • View中沒有onInterceptTouchEvent方法。
  • View的onTouchEvent預設都會被消耗,除非它是不可點選的。
  • 事件傳遞過程是由外向內的,即事件先是傳遞給父元素,然後再由父元素分發給子View。

View的滑動衝突

常見的滑動衝突場景

滑動衝突三種場景

場景1:主要是講ViewPager和Fragment配合使用組成的頁面滑動效果,會產生的問題。

場景2:在開發中,內外兩層同時能上下滑動或者內外兩層同時能左右滑動。

場景3:是場景1和場景2兩種情況的巢狀。

如何處理

根據滑動是水平滑動還是豎直滑動來判斷到底是由誰來攔截事件。幾種處理方式:

  • 外部攔截法。點選事情都是先經過父容器的攔截處理,如果父容器需要次事件就攔截,如果不需要此事件就不攔截,這樣就可以解決滑動衝突的問題。

  • 內部攔截法。父容器不攔截任何事件,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗點,否則就交由父容器進行處理。

閱讀擴充套件

源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
1,Android系統簡介
2,ProGuard程式碼混淆
3,講講Handler+Looper+MessageQueue關係
4,Android圖片載入庫理解
5,談談Android執行時許可權理解
6,EventBus初理解
7,Android 常見工具類
8,對於Fragment的一些理解
9,Android 四大元件之 " Activity "
10,Android 四大元件之" Service "
11,Android 四大元件之“ BroadcastReceiver "
12,Android 四大元件之" ContentProvider "
13,講講 Android 事件攔截機制
14,Android 動畫的理解
15,Android 生命週期和啟動模式
16,Android IPC 機制
17,View 的事件體系
18,View 的工作原理
19,理解 Window 和 WindowManager
20,Activity 啟動過程分析
21,Service 啟動過程分析
22,Android 效能優化
23,Android 訊息機制
24,Android Bitmap相關
25,Android 執行緒和執行緒池
26,Android 中的 Drawable 和動畫
27,RecylerView 中的裝飾者模式
28,Android 觸控事件機制
29,Android 事件機制應用
30,Cordova 框架的一些理解
31,有關 Android 外掛化思考
32,開發人員必備技能——單元測試

相關文章