Android?View的事件體系教程詳解
一、什么是View?什么是ViewGroup?
View是Android中所有控件的基類,不管是Button、ListView還是RelativeLayout,它們的基類都是View。View是一種界面層的控件的一種抽象,代表了一個控件。
而什么是ViewGroup,從字面上看,ViewGroup應(yīng)該指的是一個控件組,即ViewGroup中可以包含許多控件。而ViewGroup繼承自View,所以View本身就可以是單個控件也可以由多個控件組成的一組控件。這樣就構(gòu)成了View樹。
二、View的位置
View的位置由它的四個頂點確定,top(左上角縱坐標(biāo))、left(左上角橫坐標(biāo))、bottom(右下角縱坐標(biāo))、right(右下角橫坐標(biāo)),這幾個參數(shù)都是相對父級容器而言的。
在Android中,X軸和Y軸的正方向分別為向右和向下。
根據(jù)四個頂點及AndroidView的坐標(biāo)系,我們可以很容易得到View的寬高和坐標(biāo)的關(guān)系:
width=right-left
height=bottom-top
那么如何得到這四個頂點呢?
left=getLeft();
right=getRight();
top=getTop();
bottom=getBottom();
從Android3.0開始,View增加了x,y,translationX和translationY。其中x和y是view左上角的坐標(biāo)(相對坐標(biāo)系),而translationX和translationY是View左上方相對父容器的偏移量。
x=left+translationX
y=top+translationY
需要注意的是View在平移過程中,top和left表示的是原始左上角的位置信息,其值不會改變,此時改變的是x、y、translationX和translationY
三、View的觸摸事件
1.MotionEvent
在手指接觸屏幕后所產(chǎn)生的一系列事件中,典型的事件有:
ACTION_DOWN——手指剛接觸屏幕
ACTION_MOVE——手指在屏幕上移動
ACTION_DOWN——手指從屏幕上松開
一般我們可以將一次手指接觸屏幕的行為分為兩種情況:
點擊屏幕后松開,事件序列為DOWN->UP
點擊屏幕滑動一段時間后松開,事件序列為DOWN->MOVE->…->MOVE->UP
2.TouchSlop
TouchSlop即系統(tǒng)能識別滑動的最小距離,這是一個與設(shè)備有關(guān)的系統(tǒng)常量。不難得知其意思,當(dāng)手指在屏幕上滑動小于這個距離時,系統(tǒng)不認(rèn)為你在進(jìn)行滑動操作。
通過ViewConfiguration.get(getContext()).getScaledTouchSlop()方法來獲取這個系統(tǒng)常量。
3.VelocityTracker
速度追蹤,用于追蹤手指在滑動過程中的速度,包括水平和豎直方向上的速度。
具體用法:
在View的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度。
VelocityTracker velocityTracker =VelocityTracker.obtain(); velocityTracker.addMovement(event); //接著當(dāng)我們我們想知道當(dāng)前的滑動速度時 //獲取速度前先計算速度, 參數(shù)? 時間間隔 單位ms velocityTracker.computeCurrentVelocity(1000); //獲取速度 int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity=(int)velocityTracker.getYVelocity();
需要注意的是,這邊的計算得到的速度與時間間隔有關(guān),其計算公式如下:
速度=(終點位置-起點位置)/時間間隔
計算速度時得到是就是一定時間間隔內(nèi)手指在水平或豎直方向上滑動的像素數(shù),如
100像素/1000ms,這里的速度值即為100。
當(dāng)然在不需要使用它時,需要調(diào)用clear方法來重置并回收內(nèi)存。
velocityTracker.clear();
velocityTracker.recycle();
4.GestureDetector
手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。如果只是監(jiān)聽滑動相關(guān)的建議在onToucheEvent實現(xiàn),如果需要監(jiān)聽雙擊,使用GestureDetector。
5.Scroller
彈性滑動對象,用來實現(xiàn)View的彈性滑動,View的scrollTo/scrollBy是瞬間完成的,使用Scroller配合View的computeScroll方法配合使用達(dá)到彈性滑動的效果
其典型代碼是通用的.
/**
* 平滑滾動
* @param dx 橫向位移
* @param dy
*/
private void smoothScrollBy(int dx, int dy) {
//水平滑動
mScroller.startScroll(getScrollX(),0,dx,0,500);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}四、View的滑動
實現(xiàn)View滑動的幾種方式:
| View的滑動方式 | 特點 | 適用場景 |
| 使用ScrollTo/ScrollBy | 只能改變View的內(nèi)容,不能改變View本身的位置 | 適合對view內(nèi)容的滑動 |
| 通過動畫實現(xiàn)View的平移效果 | 只對影像進(jìn)行操作,不能改變View的位置參數(shù) | 適用于沒有交互的View和實現(xiàn)復(fù)雜的動畫效果。 |
| 使用屬性動畫實現(xiàn)View的平移效果 | 改變View的位置參數(shù),可響應(yīng)觸摸等事件 | 適用于有交互的View,適配到Android3.0 |
| 改變View的LayoutParams,使得View重新布局實現(xiàn)滑動 | 改變View的位置參數(shù),可響應(yīng)觸摸等事件 | 適用于有交互的View,使用稍復(fù)雜 |
前面提到了彈性滑動對象Scroll,其實實現(xiàn)彈性滑動的方法不止這一種,它們的共同思想就是將一次大的滑動分成若干次小的滑動并要求在一定時間內(nèi)完成。實現(xiàn)彈性滑動的具體實現(xiàn)方式有:
- 通過Scroll實現(xiàn)
- 通過動畫
- 使用延時策略
1)使用Scroll
使用Scroll實現(xiàn)彈性滑動需要配合View的computeScroll方法實現(xiàn),簡單來講就是實現(xiàn)多次重繪,每一次重繪有一定的時間間隔,通過這個時間間隔Scroller可以得到View的當(dāng)前滑動位置,然后通過ScrollTo方法實現(xiàn)滑動。
具體實現(xiàn)方法是在自己實現(xiàn)的平滑滑動方法中調(diào)用invalidate方法,它會導(dǎo)致View重繪,又因為在View的draw方法中又會去調(diào)用computeScroll方法,而在computeScroll方法中,我們實現(xiàn)了scrollTo方法來實現(xiàn)滑動,接著調(diào)用postInvalidate來進(jìn)行第二次重繪,此時又會調(diào)用View中的draw方法,,繼而調(diào)用computeScroll方法,如此反復(fù),直到整個滑動過程完成。
2)通過動畫
動畫本身就是一種漸漸地過程,可以很好地實現(xiàn)彈性滑動。
3)使用延時策略
使用延時策略完成滑動,核心思想就是通過發(fā)送一系列的延時消息從而達(dá)到一種漸進(jìn)的效果。具體的實現(xiàn)可以采用Handler或View的postDelayed方法,也可以采用sleep休眠。對于postDelayed方法們可以通過它來延時發(fā)送一個消息,然后在消息中進(jìn)行View的滾動。如果接連不斷發(fā)的發(fā)送這種消息,則可以達(dá)到彈性滑動對象。
而對于sleep方法,通過在while循環(huán)中不斷滑動View和sleep即可實現(xiàn)。
五、View的事件分發(fā)機制
分發(fā)對象:MotionEvent,所謂的事件分發(fā)其實就是對MotionEvent事件的分發(fā)過程,即需要將這個事件傳遞到一個具體的View上進(jìn)行處理。而完成這一過程需要三個重要方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent ev)
用來進(jìn)行事件的分發(fā),返回結(jié)果受當(dāng)前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法內(nèi)部調(diào)用,用于判斷是否攔截某個時間,如果當(dāng)前View攔截了某個事件,那么在同一個事件序列當(dāng)中,此方法不會被再次調(diào)用。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用于處理點擊事件,返回結(jié)果表示是否消耗事件,如果不消耗,則在同一個事件序列中,當(dāng)前View無法再次收到事件。
建立在上述方法的基礎(chǔ)上,我們簡單分析一下事件分發(fā)的過程。


由上述流程圖不難發(fā)現(xiàn),在MotionEvent被一個View所攔截時,其內(nèi)部的事件分發(fā)的過程中,onTouchListener的優(yōu)先級高于onTouchEvent,而常用的onClickListener的優(yōu)先級是最低的。即在onTouch->onTouchEvent->onClick。
幾個重要的結(jié)論:
- 在整個View樹中的事件分發(fā)中,如果一個View一旦開始處理事件,但它不消耗ACTION_DOWN事件(onTOuchEvent返回false),那么同一個事件序列中的其他事件也不會交給它來處理,而是將事件重新交給它的父元素進(jìn)行處理,即父元素的onTouchEvent會被調(diào)用。
- 而如果一個View消耗了ACTION_DOWN,但沒有消耗事件序列中的其他事件,那么這個點擊事件會消失,并且此時父元素的onTouchEvent也不會被調(diào)用,當(dāng)前View可以持續(xù)受到后續(xù)的事件,最終這些消失的點擊事件會傳遞給Activity處理。
- ViewGroup默認(rèn)不攔截任何事件
- View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,就好調(diào)用它的onTouchEvent方法。
- View的onTouchEvent默認(rèn)都是會消耗事件的,除非它是不可點擊的(clickable和longClickable為false)
- 事件傳遞過程是由外向內(nèi)的,即事件總是傳給父元素,然后再由父元素分發(fā)給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素中的分發(fā)過程(除ACTION_DOWN)
六、View的滑動沖突問題
View的滑動沖突常見可以簡單分為三種:
1.外部滑動和內(nèi)部滑動方向不一致
2.外部滑動方向和內(nèi)部滑動方向一致
3.上面兩張情況的嵌套
對于第一種情況,一個很好的例子就是ViewPager和Fragment嵌套使用組成的頁面滑動效果,而在Fragment內(nèi)部又會嵌套一個ListView。大家都知道ViewPager的滑動方向是水平的,而ListView的滑動方向是豎直的,這種情形和第一種情況是相符的。當(dāng)然ViewPager在內(nèi)部處理了這種滑動沖突,因此采用ViewPager不用考慮這個問題。而如果我們采用的是ScrollView,則必須手動處理這種滑動沖突了。
對于第二種情況,即內(nèi)外兩層都是需要上下滑動或者左右滑動的??梢耘e一個常見的例子,即ViewPager和NavigationDrawer。這兩者都是水平方向的滑動。當(dāng)然在實際使用中,會發(fā)現(xiàn)并沒有滑動沖突,還是上一個原因,ViewPager內(nèi)部處理了這種滑動沖突。
第三種情況即前面兩個例子的融合。
滑動沖突的處理規(guī)則
如何解決滑動沖突,這就需要用到前面講到的事件分發(fā)機制了,其核心思想就是根據(jù)實際事件的特點(down的位置,水平滑動距離,豎直滑動距離等)來判斷由哪個View來攔截事件。對于第一種情況可以簡單地判斷是水平滑動還是豎直滑動來判斷由哪個View來攔截事件。(可以根據(jù)水平和豎直方向上的距離差或速度差來進(jìn)行判斷),而對于第二種情況,可根據(jù)down的位置來加以區(qū)分。
滑動沖突的解決方法
- 外部攔截法 —— 即點擊事件先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,不需要就不攔截,需要重寫父容器的onInterceptTouchEvent方法;在onInterceptTouchEvent方法中,首先ACTION_DOWN這個事件,父容器必須返回false,即不攔截ACTION_DOWN事件,因為一旦父容器攔截了ACTION_DOWN,那么后續(xù)的ACTION_MOVE/ACTION_UP都會直接交給父容器處理;其次是ACTION_MOVE,根據(jù)需求來決定是否要攔截;最后ACTION_UP事件,這里必須要返回false,在這里沒有多大意義。
- 內(nèi)部攔截法 —— 所有事件都傳遞給子元素,如果子元素需要就消耗掉,不需要就交給父元素處理,需要子元素配合requestDisallowInterceptTouchEvent方法才能正常工作;父元素需要默認(rèn)攔截除ACTION_DOWN以外的事件,這樣子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續(xù)攔截需要的事件。(ACTION_DOWN事件不受requestDisallowInterceptTouchEvent方法影響,所以一旦父元素攔截ACTION_DOWN事件,那么所有元素都無法傳遞到子元素去)
兩種攔截方法的范式(偽代碼形式):
外部攔截法:只需要重寫父容器的onInterceptTouchEvent方法
private int mLastXIntercepet=0;
private int mLastYIntercepet=0;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted =false;
int x=(int) ev.getX();
int y=(int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted=false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要當(dāng)前點擊事件){
intercepted=true;
}else{
intercepted=false;
}
break;
case MotionEvent.ACTION_UP:
intercepted=false;
break;
default:
break;
}
mLastXIntercepet=x;
mLastYIntercepet=y;
return intercepted;
}內(nèi)部攔截法:需要重寫子元素的dispatchTouchEvent方法和父容器的onInterceptTouchEvent方法。
子元素的dispatchTouchEvent方法
//分別記錄上次滑動的坐標(biāo)
private int mLastX=0;
private int mLastY=0;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x= (int) ev.getX();
int y= (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX=x-mLastX;
int deltaY=y-mLastY;
if(父容器需要此類點擊事件){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX=x;
mLastY=y;
return super.dispatchTouchEvent(ev);
}父容器的onInterceptTouchEvent:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x= (int) ev.getX();
int y= (int) ev.getY();
int action=ev.getAction();
if(action==MotionEvent.ACTION_DOWN)
return false;
else
return true;
}以上就是Android View的事件體系教程詳解的詳細(xì)內(nèi)容,更多關(guān)于Android View事件體系的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android判斷手機是否是小米MIUI系統(tǒng)的方法
這篇文章主要介紹了Android判斷手機是否是小米MIUI系統(tǒng)的方法的相關(guān)資料,需要的朋友可以參考下2016-02-02
Android網(wǎng)絡(luò)訪問之Retrofit使用教程
Retrofit?是一個?RESTful?的?HTTP?網(wǎng)絡(luò)請求框架的封裝,網(wǎng)絡(luò)請求的工作本質(zhì)上是?OkHttp?完成,而?Retrofit?僅負(fù)責(zé)?網(wǎng)絡(luò)請求接口的封裝2022-12-12
listview與SQLite結(jié)合實現(xiàn)記事本功能
這篇文章主要為大家詳細(xì)介紹了listview與SQLite結(jié)合實現(xiàn)記事本功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12
androidx下的fragment的lazy懶加載問題詳解
這篇文章主要介紹了androidx下的fragment的lazy懶加載問題詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04

