Android?View的事件體系教程詳解
一、什么是View?什么是ViewGroup?
View是Android中所有控件的基類,不管是Button、ListView還是RelativeLayout,它們的基類都是View。View是一種界面層的控件的一種抽象,代表了一個(gè)控件。
而什么是ViewGroup,從字面上看,ViewGroup應(yīng)該指的是一個(gè)控件組,即ViewGroup中可以包含許多控件。而ViewGroup繼承自View,所以View本身就可以是單個(gè)控件也可以由多個(gè)控件組成的一組控件。這樣就構(gòu)成了View樹。
二、View的位置
View的位置由它的四個(gè)頂點(diǎn)確定,top(左上角縱坐標(biāo))、left(左上角橫坐標(biāo))、bottom(右下角縱坐標(biāo))、right(右下角橫坐標(biāo)),這幾個(gè)參數(shù)都是相對(duì)父級(jí)容器而言的。
在Android中,X軸和Y軸的正方向分別為向右和向下。
根據(jù)四個(gè)頂點(diǎn)及AndroidView的坐標(biāo)系,我們可以很容易得到View的寬高和坐標(biāo)的關(guān)系:
width=right-left
height=bottom-top
那么如何得到這四個(gè)頂點(diǎn)呢?
left=getLeft();
right=getRight();
top=getTop();
bottom=getBottom();
從Android3.0開始,View增加了x,y,translationX和translationY。其中x和y是view左上角的坐標(biāo)(相對(duì)坐標(biāo)系),而translationX和translationY是View左上方相對(duì)父容器的偏移量。
x=left+translationX
y=top+translationY
需要注意的是View在平移過程中,top和left表示的是原始左上角的位置信息,其值不會(huì)改變,此時(shí)改變的是x、y、translationX和translationY
三、View的觸摸事件
1.MotionEvent
在手指接觸屏幕后所產(chǎn)生的一系列事件中,典型的事件有:
ACTION_DOWN——手指剛接觸屏幕
ACTION_MOVE——手指在屏幕上移動(dòng)
ACTION_DOWN——手指從屏幕上松開
一般我們可以將一次手指接觸屏幕的行為分為兩種情況:
點(diǎn)擊屏幕后松開,事件序列為DOWN->UP
點(diǎn)擊屏幕滑動(dòng)一段時(shí)間后松開,事件序列為DOWN->MOVE->…->MOVE->UP
2.TouchSlop
TouchSlop即系統(tǒng)能識(shí)別滑動(dòng)的最小距離,這是一個(gè)與設(shè)備有關(guān)的系統(tǒng)常量。不難得知其意思,當(dāng)手指在屏幕上滑動(dòng)小于這個(gè)距離時(shí),系統(tǒng)不認(rèn)為你在進(jìn)行滑動(dòng)操作。
通過ViewConfiguration.get(getContext()).getScaledTouchSlop()方法來獲取這個(gè)系統(tǒng)常量。
3.VelocityTracker
速度追蹤,用于追蹤手指在滑動(dòng)過程中的速度,包括水平和豎直方向上的速度。
具體用法:
在View的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度。
VelocityTracker velocityTracker =VelocityTracker.obtain(); velocityTracker.addMovement(event); //接著當(dāng)我們我們想知道當(dāng)前的滑動(dòng)速度時(shí) //獲取速度前先計(jì)算速度, 參數(shù)? 時(shí)間間隔 單位ms velocityTracker.computeCurrentVelocity(1000); //獲取速度 int xVelocity = (int)velocityTracker.getXVelocity(); int yVelocity=(int)velocityTracker.getYVelocity();
需要注意的是,這邊的計(jì)算得到的速度與時(shí)間間隔有關(guān),其計(jì)算公式如下:
速度=(終點(diǎn)位置-起點(diǎn)位置)/時(shí)間間隔
計(jì)算速度時(shí)得到是就是一定時(shí)間間隔內(nèi)手指在水平或豎直方向上滑動(dòng)的像素?cái)?shù),如
100像素/1000ms,這里的速度值即為100。
當(dāng)然在不需要使用它時(shí),需要調(diào)用clear方法來重置并回收內(nèi)存。
velocityTracker.clear();
velocityTracker.recycle();
4.GestureDetector
手勢(shì)檢測(cè),用于輔助檢測(cè)用戶的單擊、滑動(dòng)、長(zhǎng)按、雙擊等行為。如果只是監(jiān)聽滑動(dòng)相關(guān)的建議在onToucheEvent實(shí)現(xiàn),如果需要監(jiān)聽雙擊,使用GestureDetector。
5.Scroller
彈性滑動(dòng)對(duì)象,用來實(shí)現(xiàn)View的彈性滑動(dòng),View的scrollTo/scrollBy是瞬間完成的,使用Scroller配合View的computeScroll方法配合使用達(dá)到彈性滑動(dòng)的效果
其典型代碼是通用的.
/** * 平滑滾動(dòng) * @param dx 橫向位移 * @param dy */ private void smoothScrollBy(int dx, int dy) { //水平滑動(dòng) mScroller.startScroll(getScrollX(),0,dx,0,500); invalidate(); } @Override public void computeScroll() { if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }
四、View的滑動(dòng)
實(shí)現(xiàn)View滑動(dòng)的幾種方式:
View的滑動(dòng)方式 | 特點(diǎn) | 適用場(chǎng)景 |
使用ScrollTo/ScrollBy | 只能改變View的內(nèi)容,不能改變View本身的位置 | 適合對(duì)view內(nèi)容的滑動(dòng) |
通過動(dòng)畫實(shí)現(xiàn)View的平移效果 | 只對(duì)影像進(jìn)行操作,不能改變View的位置參數(shù) | 適用于沒有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果。 |
使用屬性動(dòng)畫實(shí)現(xiàn)View的平移效果 | 改變View的位置參數(shù),可響應(yīng)觸摸等事件 | 適用于有交互的View,適配到Android3.0 |
改變View的LayoutParams,使得View重新布局實(shí)現(xiàn)滑動(dòng) | 改變View的位置參數(shù),可響應(yīng)觸摸等事件 | 適用于有交互的View,使用稍復(fù)雜 |
前面提到了彈性滑動(dòng)對(duì)象Scroll,其實(shí)實(shí)現(xiàn)彈性滑動(dòng)的方法不止這一種,它們的共同思想就是將一次大的滑動(dòng)分成若干次小的滑動(dòng)并要求在一定時(shí)間內(nèi)完成。實(shí)現(xiàn)彈性滑動(dòng)的具體實(shí)現(xiàn)方式有:
- 通過Scroll實(shí)現(xiàn)
- 通過動(dòng)畫
- 使用延時(shí)策略
1)使用Scroll
使用Scroll實(shí)現(xiàn)彈性滑動(dòng)需要配合View的computeScroll方法實(shí)現(xiàn),簡(jiǎn)單來講就是實(shí)現(xiàn)多次重繪,每一次重繪有一定的時(shí)間間隔,通過這個(gè)時(shí)間間隔Scroller可以得到View的當(dāng)前滑動(dòng)位置,然后通過ScrollTo方法實(shí)現(xiàn)滑動(dòng)。
具體實(shí)現(xiàn)方法是在自己實(shí)現(xiàn)的平滑滑動(dòng)方法中調(diào)用invalidate方法,它會(huì)導(dǎo)致View重繪,又因?yàn)樵赩iew的draw方法中又會(huì)去調(diào)用computeScroll方法,而在computeScroll方法中,我們實(shí)現(xiàn)了scrollTo方法來實(shí)現(xiàn)滑動(dòng),接著調(diào)用postInvalidate來進(jìn)行第二次重繪,此時(shí)又會(huì)調(diào)用View中的draw方法,,繼而調(diào)用computeScroll方法,如此反復(fù),直到整個(gè)滑動(dòng)過程完成。
2)通過動(dòng)畫
動(dòng)畫本身就是一種漸漸地過程,可以很好地實(shí)現(xiàn)彈性滑動(dòng)。
3)使用延時(shí)策略
使用延時(shí)策略完成滑動(dòng),核心思想就是通過發(fā)送一系列的延時(shí)消息從而達(dá)到一種漸進(jìn)的效果。具體的實(shí)現(xiàn)可以采用Handler或View的postDelayed方法,也可以采用sleep休眠。對(duì)于postDelayed方法們可以通過它來延時(shí)發(fā)送一個(gè)消息,然后在消息中進(jìn)行View的滾動(dòng)。如果接連不斷發(fā)的發(fā)送這種消息,則可以達(dá)到彈性滑動(dòng)對(duì)象。
而對(duì)于sleep方法,通過在while循環(huán)中不斷滑動(dòng)View和sleep即可實(shí)現(xiàn)。
五、View的事件分發(fā)機(jī)制
分發(fā)對(duì)象:MotionEvent,所謂的事件分發(fā)其實(shí)就是對(duì)MotionEvent事件的分發(fā)過程,即需要將這個(gè)事件傳遞到一個(gè)具體的View上進(jìn)行處理。而完成這一過程需要三個(gè)重要方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent ev)
用來進(jìn)行事件的分發(fā),返回結(jié)果受當(dāng)前View的onTouchEvent和下級(jí)View的dispatchTouchEvent方法的影響,表示是否消耗當(dāng)前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法內(nèi)部調(diào)用,用于判斷是否攔截某個(gè)時(shí)間,如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列當(dāng)中,此方法不會(huì)被再次調(diào)用。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用,用于處理點(diǎn)擊事件,返回結(jié)果表示是否消耗事件,如果不消耗,則在同一個(gè)事件序列中,當(dāng)前View無法再次收到事件。
建立在上述方法的基礎(chǔ)上,我們簡(jiǎn)單分析一下事件分發(fā)的過程。
由上述流程圖不難發(fā)現(xiàn),在MotionEvent被一個(gè)View所攔截時(shí),其內(nèi)部的事件分發(fā)的過程中,onTouchListener的優(yōu)先級(jí)高于onTouchEvent,而常用的onClickListener的優(yōu)先級(jí)是最低的。即在onTouch->onTouchEvent->onClick。
幾個(gè)重要的結(jié)論:
- 在整個(gè)View樹中的事件分發(fā)中,如果一個(gè)View一旦開始處理事件,但它不消耗ACTION_DOWN事件(onTOuchEvent返回false),那么同一個(gè)事件序列中的其他事件也不會(huì)交給它來處理,而是將事件重新交給它的父元素進(jìn)行處理,即父元素的onTouchEvent會(huì)被調(diào)用。
- 而如果一個(gè)View消耗了ACTION_DOWN,但沒有消耗事件序列中的其他事件,那么這個(gè)點(diǎn)擊事件會(huì)消失,并且此時(shí)父元素的onTouchEvent也不會(huì)被調(diào)用,當(dāng)前View可以持續(xù)受到后續(xù)的事件,最終這些消失的點(diǎn)擊事件會(huì)傳遞給Activity處理。
- ViewGroup默認(rèn)不攔截任何事件
- View沒有onInterceptTouchEvent方法,一旦有點(diǎn)擊事件傳遞給它,就好調(diào)用它的onTouchEvent方法。
- View的onTouchEvent默認(rèn)都是會(huì)消耗事件的,除非它是不可點(diǎn)擊的(clickable和longClickable為false)
- 事件傳遞過程是由外向內(nèi)的,即事件總是傳給父元素,然后再由父元素分發(fā)給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素中的分發(fā)過程(除ACTION_DOWN)
六、View的滑動(dòng)沖突問題
View的滑動(dòng)沖突常見可以簡(jiǎn)單分為三種:
1.外部滑動(dòng)和內(nèi)部滑動(dòng)方向不一致
2.外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致
3.上面兩張情況的嵌套
對(duì)于第一種情況,一個(gè)很好的例子就是ViewPager和Fragment嵌套使用組成的頁(yè)面滑動(dòng)效果,而在Fragment內(nèi)部又會(huì)嵌套一個(gè)ListView。大家都知道ViewPager的滑動(dòng)方向是水平的,而ListView的滑動(dòng)方向是豎直的,這種情形和第一種情況是相符的。當(dāng)然ViewPager在內(nèi)部處理了這種滑動(dòng)沖突,因此采用ViewPager不用考慮這個(gè)問題。而如果我們采用的是ScrollView,則必須手動(dòng)處理這種滑動(dòng)沖突了。
對(duì)于第二種情況,即內(nèi)外兩層都是需要上下滑動(dòng)或者左右滑動(dòng)的??梢耘e一個(gè)常見的例子,即ViewPager和NavigationDrawer。這兩者都是水平方向的滑動(dòng)。當(dāng)然在實(shí)際使用中,會(huì)發(fā)現(xiàn)并沒有滑動(dòng)沖突,還是上一個(gè)原因,ViewPager內(nèi)部處理了這種滑動(dòng)沖突。
第三種情況即前面兩個(gè)例子的融合。
滑動(dòng)沖突的處理規(guī)則
如何解決滑動(dòng)沖突,這就需要用到前面講到的事件分發(fā)機(jī)制了,其核心思想就是根據(jù)實(shí)際事件的特點(diǎn)(down的位置,水平滑動(dòng)距離,豎直滑動(dòng)距離等)來判斷由哪個(gè)View來攔截事件。對(duì)于第一種情況可以簡(jiǎn)單地判斷是水平滑動(dòng)還是豎直滑動(dòng)來判斷由哪個(gè)View來攔截事件。(可以根據(jù)水平和豎直方向上的距離差或速度差來進(jìn)行判斷),而對(duì)于第二種情況,可根據(jù)down的位置來加以區(qū)分。
滑動(dòng)沖突的解決方法
- 外部攔截法 —— 即點(diǎn)擊事件先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,不需要就不攔截,需要重寫父容器的onInterceptTouchEvent方法;在onInterceptTouchEvent方法中,首先ACTION_DOWN這個(gè)事件,父容器必須返回false,即不攔截ACTION_DOWN事件,因?yàn)橐坏└溉萜鲾r截了ACTION_DOWN,那么后續(xù)的ACTION_MOVE/ACTION_UP都會(huì)直接交給父容器處理;其次是ACTION_MOVE,根據(jù)需求來決定是否要攔截;最后ACTION_UP事件,這里必須要返回false,在這里沒有多大意義。
- 內(nèi)部攔截法 —— 所有事件都傳遞給子元素,如果子元素需要就消耗掉,不需要就交給父元素處理,需要子元素配合requestDisallowInterceptTouchEvent方法才能正常工作;父元素需要默認(rèn)攔截除ACTION_DOWN以外的事件,這樣子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時(shí),父元素才能繼續(xù)攔截需要的事件。(ACTION_DOWN事件不受requestDisallowInterceptTouchEvent方法影響,所以一旦父元素?cái)r截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)前點(diǎn)擊事件){ 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方法
//分別記錄上次滑動(dòng)的坐標(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(父容器需要此類點(diǎn)擊事件){ 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事件體系的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android判斷手機(jī)是否是小米MIUI系統(tǒng)的方法
這篇文章主要介紹了Android判斷手機(jī)是否是小米MIUI系統(tǒng)的方法的相關(guān)資料,需要的朋友可以參考下2016-02-02Android網(wǎng)絡(luò)訪問之Retrofit使用教程
Retrofit?是一個(gè)?RESTful?的?HTTP?網(wǎng)絡(luò)請(qǐng)求框架的封裝,網(wǎng)絡(luò)請(qǐng)求的工作本質(zhì)上是?OkHttp?完成,而?Retrofit?僅負(fù)責(zé)?網(wǎng)絡(luò)請(qǐng)求接口的封裝2022-12-12listview與SQLite結(jié)合實(shí)現(xiàn)記事本功能
這篇文章主要為大家詳細(xì)介紹了listview與SQLite結(jié)合實(shí)現(xiàn)記事本功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12androidx下的fragment的lazy懶加載問題詳解
這篇文章主要介紹了androidx下的fragment的lazy懶加載問題詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04android簡(jiǎn)單自定義View實(shí)現(xiàn)五子棋
這篇文章主要為大家詳細(xì)介紹了android簡(jiǎn)單自定義View實(shí)現(xiàn)五子棋,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11Android幀式布局實(shí)現(xiàn)自動(dòng)切換顏色
這篇文章主要為大家詳細(xì)介紹了Android幀式布局實(shí)現(xiàn)自動(dòng)切換顏色,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04kotlin實(shí)戰(zhàn)教程之lambda編程
這篇文章主要給大家介紹了關(guān)于kotlin實(shí)戰(zhàn)教程之lambda編程的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用kotlin具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09