View的事件體系

View雖然不屬于四大組件,但它的作用堪比四大組件,甚至比Receiver和Provider的重要性都大,在Android開發(fā)中,Activity承擔這可視化的功能,同時Android系統(tǒng)提供了很多基礎(chǔ)控件,常見的有Button、Textview、CheckBox等。

View基礎(chǔ)知識

什么是View

View是一種界面層的控件的一種抽象,它代表了一個控件。除了View,還有ViewGroup,ViewGroup內(nèi)部包含了許多個控件,即一組View,在ViewGroup也繼承了View,這就意味著View本身就可以是單個控件也可以是由多個控件組成的一組控件,通過這種關(guān)系形成了View樹的結(jié)構(gòu)。

View結(jié)構(gòu)圖

View的位置參數(shù)

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

View坐標軸

在Android中,X軸和Y軸的正方向分別為右和下。那么就可以得出以下關(guān)系:

width = right - left
height = bottom - top

MotionEvent和TouchSlop

在手指接觸屏幕后會產(chǎn)生一系列的事件,典型的事件類型有如下幾種:

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

正常情況下,一次手指觸摸屏幕的行為會觸發(fā)一系列點擊事件,如下:

  • 點擊屏幕后離開松開,事件序列為DOWN->UP
  • 點擊屏幕滑動一會再松開,事件序列為DOWN->MOVE->.......->MOVE->UP

同時我們可以通過MotionEvent對象我們可以得到點擊事件發(fā)生的X坐標和Y坐標。為此,系統(tǒng)提供了兩組方法:getX/getY和getRawX/getRawY,區(qū)別很簡單,前者返回的是相對于當前View左上角的X和Y坐標,而后者返回的相對于手機屏幕左上角的X和Y坐標。

TouchSlop是系統(tǒng)所能識別出的被認為是滑動的最小距離,換句話說,當手指在屏幕上滑動時,如果兩次滑動之間的距離小于這個常量,那么系統(tǒng)就不認為你是在進行滑動操作,因為滑動的距離太短了,系統(tǒng)就不認為它是滑動,這是一個常量,跟設(shè)備有關(guān)??梢酝ㄟ^以下方式獲取:

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

View的滑動

在Android設(shè)備中,滑動幾乎是應(yīng)用的標配,不管是下拉刷新還是SlidingMenu,它們的基礎(chǔ)都是滑動。因此,掌握滑動的方法是實現(xiàn)絢麗的自定義控件的基礎(chǔ),有三種方式實現(xiàn)View的滑動:

  • scrollTo/scrollBy方法來實現(xiàn)
  • 使用動畫
  • 改變布局參數(shù)

scrollTo/scrollBy方法來實現(xiàn)

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

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

View滑動

使用動畫

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

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

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

改變布局參數(shù)

改變布局參數(shù),即改變LayoutParams,這個比較好理解,比如我們想把一個Button向右平移100px,我們只需要將這個Button的LayoutParams里的marginLeft參數(shù)的值增加100px即可。

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

三種滑動方式對比:

  • scrollTo/scrollBy:操作簡單,適合對View內(nèi)容的滑動
  • 動畫:操作簡單,主要使用于沒有交互的View和實現(xiàn)復(fù)雜的動畫效果
  • 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的View

彈性滑動

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

View的事件分發(fā)機制

點擊事件的傳遞規(guī)則

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

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

簡單來說,對于一個根ViewGroup來說,點擊事件產(chǎn)生后,首先會傳遞給它,這時它的dispatchTouchEvent會被調(diào)用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接著事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調(diào)用,如果這個ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當前事件,這時當前事件就會繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會被調(diào)用,如此反復(fù)直到事件被最終處理。

還有一個當View需要處理事件時,如果它設(shè)置了OnTouchListener,那么OnTouchListener中的onTouch方法會被回調(diào),OnTouchListener優(yōu)先于onTouchEvent。在onTouchEvent中,如果設(shè)置了OnClickListener,那么它的OnClick方法會被調(diào)用,可以看出我們平時常用的OnClickListener優(yōu)先級最低,onTouch>onClick.

View不處理流程:

View不處理流程

View處理流程:

View處理流程

一些總結(jié):

  • 同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結(jié)束。一般是以down事件開始,中間含有數(shù)量不定的move事件,最終以up事件結(jié)束。
  • 正常情況下,一個事件序列只能被一個View攔截且消耗。
  • 某個View一旦決定攔截,那么這個事件序列就只能由它來處理,那么同一事件序列中的其他事件都不會再交給它來處理,并且事件將重新交給它的父元素去處理,即父元素的onTouchEvent會被調(diào)用。
  • 如果View不消耗除ACTION_DOWN以外的其他事件,那么這個點擊事件就會消失,此時父元素的onTouchEvent并不會被調(diào)用,最終會交給Activity處理。
  • ViewGroup默認不攔截任何事件。
  • View中沒有onInterceptTouchEvent方法。
  • View的onTouchEvent默認都會被消耗,除非它是不可點擊的。
  • 事件傳遞過程是由外向內(nèi)的,即事件先是傳遞給父元素,然后再由父元素分發(fā)給子View。

View的滑動沖突

常見的滑動沖突場景

滑動沖突三種場景

場景1:主要是講ViewPager和Fragment配合使用組成的頁面滑動效果,會產(chǎn)生的問題。
場景2:在開發(fā)中,內(nèi)外兩層同時能上下滑動或者內(nèi)外兩層同時能左右滑動。
場景3:是場景1和場景2兩種情況的嵌套。

如何處理

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容