第3章 View的事件體系
[TOC]
3.1 View基礎(chǔ)知識(shí)
1. View的位置參數(shù)
首先來(lái)認(rèn)識(shí)一下View的位置參數(shù),因?yàn)檫@關(guān)系到View的測(cè)量丰刊,這跟View的繪制有關(guān)系,因此認(rèn)識(shí)View的位置參數(shù)是很有必要的嚣潜。
簡(jiǎn)單來(lái)說(shuō)有這么8個(gè),可以分為3類:
- left,top,right,bottom
- x,y
- translationX,translationY
注:
這里需要提前聲明一下哭廉,這些位置參數(shù)都是相對(duì)于父控件的怀跛,因此不要混淆距贷,下面具體介紹一下。
- left,top,right,bottom這四種從名字就能知道這是View控件相對(duì)于父控件的左上右底四個(gè)邊緣的坐標(biāo)
- x,y是左上角相對(duì)于父控件的坐標(biāo)
- translationX,translationY是左上角相對(duì)于父控件的偏移量吻谋,一般為0忠蝗,那為什么要有這兩個(gè)屬性?因?yàn)橛袝r(shí)View會(huì)移動(dòng)漓拾,但是父控件沒(méi)有移動(dòng)阁最,此時(shí)translationX和translationY有助于獲取到移動(dòng)后的左上角的XY坐標(biāo)澡腾,由此可得到換算公式:
x = left + translationX
y = top + translationY
然后這幾個(gè)位置參數(shù)都是可以通過(guò)方法獲取到的滩字,如下所示凭峡。
Log.i(TAG, "(left " + v.getLeft()
+", top " + v.getTop()
+", right " + v.getRight()
+", bottom " + v.getBottom()
+", tranxlationX " + v.getTranslationX()
+", x " + v.getX()
+", translationY " + v.getTranslationY()
+", y " + v.getY()
* ")");
然后通過(guò)實(shí)際效果更能清楚的認(rèn)識(shí)這幾個(gè)類跃捣,從下圖中可以看出來(lái)上面那些位置參數(shù)都是相對(duì)于父控件的迹辐。
2. MotionEvent和TouchSlop
2.1 MotionEvent
MotionEvent是點(diǎn)擊事件中不可缺少的類醇疼,所有關(guān)于點(diǎn)擊事件的信息都集成在該類上烙肺,因此需要來(lái)了解該事件類钦勘,先總結(jié)一下從MotionEvent中可以獲得到什么示血,主要有兩個(gè)棋傍。
- 事件類型信息ACTION:ACTION_DOWN,ACTION_MOVE,ACTION_UP,通過(guò)event.getAction()獲得
- 坐標(biāo)信息:(x,y)相對(duì)點(diǎn)擊的控件左上角的坐標(biāo)难审,(rawX,rawY)絕對(duì)坐標(biāo)瘫拣,分別通過(guò)event.getX()和event.getRawX()獲得
首先最主要的是MotionEvent中的三個(gè)事件類型:
ACTION_DOWN:手指剛接觸屏幕
ACTION_MOVE:手指在屏幕上移動(dòng)
ACTION_UP:手指從屏幕上松開(kāi)的一瞬間
然后MotionEvent可以獲取到點(diǎn)擊的位置位于所點(diǎn)擊的控件的相對(duì)坐標(biāo)以及絕對(duì)坐標(biāo):
x:相對(duì)x坐標(biāo),相對(duì)于點(diǎn)擊控件左上角的x坐標(biāo)告喊,比如點(diǎn)擊在Button的左上角時(shí)麸拄,x == 0
y:相對(duì)y坐標(biāo),相對(duì)于點(diǎn)擊控件左上角的y坐標(biāo)葱绒,比如點(diǎn)擊在Button的左上角時(shí)感帅,y == 0
rawX:絕對(duì)x坐標(biāo),相對(duì)于屏幕左上角
rawX:絕對(duì)y坐標(biāo)地淀,相對(duì)于屏幕左上角
2.2 TouchSlop
TouchSlop是系統(tǒng)所能識(shí)別出的被認(rèn)為是滑動(dòng)的最小距離常量失球,意思就是當(dāng)手指在屏幕上滑動(dòng)時(shí),如果滑動(dòng)的距離小于該常量,則不認(rèn)為在進(jìn)行滑動(dòng)操作实苞。
不同的屏幕對(duì)應(yīng)有不同的TouchSlop的值
3.2 View的滑動(dòng)
在Android中View的滑動(dòng)時(shí)很重要的豺撑,掌握滑動(dòng)的方法是實(shí)現(xiàn)絢麗的自定義控件的基礎(chǔ)。實(shí)現(xiàn)View的滑動(dòng)的方式總共有三種:
- 通過(guò)View本身提供的scrollTo/ScrollBy方法
- 通過(guò)動(dòng)畫(huà)給View施加平移效果
- 通過(guò)改變View的LayoutParams使得View重新布局
1. 使用scrollTo/scrollBy
scrollTo(x,y)是絕對(duì)滑動(dòng)
scrollBy(x,y)是相對(duì)于當(dāng)前位置的滑動(dòng)黔牵,即相對(duì)滑動(dòng)聪轿,其實(shí)內(nèi)部也是用scrollTo(x,y)實(shí)現(xiàn)的
下面是源碼:
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
這里需要知道的是,scrollTo和scrollBy滑動(dòng)的只是View內(nèi)容的位置猾浦,View處于布局中的位置并沒(méi)有改變陆错。
由源碼中可以發(fā)現(xiàn),有兩個(gè)變量在滑動(dòng)中有重要的作用金赦,即mScrollX和mScrollY音瓷,它們的意思是view的邊緣與view內(nèi)容邊緣的距離。
mScrollX = View的左邊緣 - View內(nèi)容的左邊緣
mScrollY = View的上邊緣 - View內(nèi)容的上邊緣
也就是說(shuō)夹抗,在內(nèi)容移動(dòng)之前绳慎,mScrollX和mScrollY都是0,當(dāng)內(nèi)容偏向左邊100單位的時(shí)候漠烧,mScrollX== 0 - (-100) == 100杏愤,當(dāng)內(nèi)容偏向右邊100單位的時(shí)候,xScrollX == -100已脓,總而言之珊楼,當(dāng)View的內(nèi)容往屏幕正方向(包括x和y方向)移動(dòng)時(shí),mScrollX和mScrollY就會(huì)減小摆舟,如圖所示亥曹。
因此可得出結(jié)論:
- scrollTo給定(0,0)參數(shù)時(shí),內(nèi)容返回原位恨诱,給定負(fù)數(shù)的參數(shù)時(shí)媳瞪,內(nèi)容往相對(duì)于原點(diǎn)的正方向移動(dòng)變化
- scrollBy給定負(fù)數(shù)的參數(shù)時(shí),內(nèi)容往屏幕正方向移動(dòng)照宝;反之往負(fù)方向移動(dòng)
- 記住蛇受,scroll的移動(dòng)只是移動(dòng)View的內(nèi)容,而不是移動(dòng)View
下面是簡(jiǎn)單使用scrollTo和scrollBy的方法厕鹃,使用效果是兢仰,每點(diǎn)擊一次按鍵,btn1的內(nèi)容就會(huì)向下移動(dòng)1單元剂碴,移動(dòng)到10單元的時(shí)候就會(huì)回到原位置把将。
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
switch (v.getId()) {
case R.id.btn_move:
Log.i("MainActivity", btn1.getScrollY()+"");
if(btn1.getScrollY() == -10)
{
btn1.scrollTo(0, 0);
}else
{
btn1.scrollBy(0, -1);
}
break;
default:
break;
}
}
2. 使用動(dòng)畫(huà)
另一種使View滑動(dòng)的方式是使用動(dòng)畫(huà),而動(dòng)畫(huà)的本質(zhì)是通過(guò)改變View的translationX和translationY位置參數(shù)來(lái)達(dá)到移動(dòng)的目的的忆矛。還記得在第一節(jié)講過(guò)的關(guān)于translationX(Y)的意義和計(jì)算公式嗎察蹲,這里再?gòu)?fù)習(xí)一遍请垛。
x = left + translationX
y = top + translationY
其中x,y為View相對(duì)于父控件的位置洽议,left和top是View相對(duì)于父控件的左邊緣和上邊緣宗收,原始原始情況下translationX和translationY為0,如果這兩個(gè)變量改變的話亚兄,x和y就會(huì)被改變混稽,導(dǎo)致View的位置發(fā)生改變從而發(fā)生滑動(dòng)。
而動(dòng)畫(huà)分為兩種审胚,分為傳統(tǒng)的View動(dòng)畫(huà)和屬性動(dòng)畫(huà)匈勋,屬性動(dòng)畫(huà)不兼容3.0以下的,需要采用開(kāi)源動(dòng)畫(huà)庫(kù)nineoldandroid菲盾。
關(guān)于動(dòng)畫(huà)需要注意的是颓影,動(dòng)畫(huà)移動(dòng)的只是影像,“真身”還是留在了原來(lái)的位置懒鉴,意思就是,比如使用動(dòng)畫(huà)將一個(gè)Button移動(dòng)到了下方100px碎浇,但是會(huì)發(fā)現(xiàn)點(diǎn)擊移動(dòng)后的Button沒(méi)有點(diǎn)擊效果临谱,反而點(diǎn)擊原來(lái)的位置才有效。這個(gè)問(wèn)題使用屬性動(dòng)畫(huà)可以解決奴璃。
3. 改變布局參數(shù)
這就跟View的繪制有關(guān)系了悉默,View的繪制都是通過(guò)獲取View的布局參數(shù)來(lái)將View繪制到屏幕上的,那么我只要將View的布局參數(shù)改變了苟穆,然后再重新繪制它就可以造成移動(dòng)的效果了抄课。所以改變布局參數(shù)步驟上大概可以分成3步:
- 先獲取到View的布局參數(shù)對(duì)象(LayoutParams)
- 修改該對(duì)象的參數(shù)
- 重繪View或者View重新設(shè)置布局參數(shù)
下面示例一個(gè)向右移動(dòng)100px的實(shí)例
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) btn1.getLayoutParams();
marginLayoutParams.leftMargin += 100;
btn1.requestLayout();
//或者
//btn1.setLayoutParams(marginLayoutParams);
4. 3種滑動(dòng)方式的比較
scrollTo/By:
優(yōu)點(diǎn):對(duì)View內(nèi)容實(shí)現(xiàn)滑動(dòng)效果操作簡(jiǎn)單
缺點(diǎn):只能滑動(dòng)View的內(nèi)容,并不能滑動(dòng)View本身
動(dòng)畫(huà):
優(yōu)點(diǎn):操作簡(jiǎn)單雳旅,主要適用于沒(méi)有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫(huà)效果
缺點(diǎn):使用傳統(tǒng)動(dòng)畫(huà)的話會(huì)造成不能改變View本身的屬性
改變布局參數(shù):
優(yōu)點(diǎn):適用于有交互的View滑動(dòng)
缺點(diǎn):操作稍微復(fù)雜
3.3 View的事件分發(fā)
1. 點(diǎn)擊事件的傳遞規(guī)則
事件分發(fā)的傳遞過(guò)程:Activity(Decor View)->Windows->View
事件分發(fā)機(jī)制是View的一個(gè)核心知識(shí)點(diǎn)跟磨。首先要明白,事件的分發(fā)指的就是MotionEvent對(duì)象的分發(fā)攒盈。分發(fā)的過(guò)程主要由這3個(gè)方法來(lái)完成:
- dispatchTouchEvent(MotionEvent ev):用來(lái)進(jìn)行事件的分發(fā)抵拘。如果事件能夠傳遞給當(dāng)前的View,那么此方法一定會(huì)被調(diào)用型豁。
- onInterceptTouchEvent(MotionEvent ev):用于進(jìn)行事件的攔截僵蛛。如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列當(dāng)中迎变,此方法不會(huì)被再次調(diào)用充尉。
- onTouchEvent(MotionEvent ev):用于進(jìn)行事件的消費(fèi)。如果不消耗衣形,則在同一事件序列中驼侠,當(dāng)前View無(wú)法再次接收到事件(效果和onInterceptTouchEvent返回false一樣)。
下面通過(guò)一段偽代碼來(lái)表現(xiàn)這三個(gè)方法的關(guān)系:
public boolean dispatchTouchEvent(MotionEvent ev)
{
boolean consume = false;
if(onInterceptTouchEvent(ev))
{
//其實(shí)還有個(gè)onTouch()在onTouchEvent的前面
//if(!onTouch(ev))
consume = onTouchEvent(ev);
}else
{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
其實(shí)在onTouchEvent之前還有個(gè)onTouch方法,如果onTouch返回false時(shí)onTouchEvent才會(huì)被執(zhí)行泪电。
這里先提前說(shuō)一個(gè)規(guī)律般妙,便于后面的理解,那就是如果一個(gè)View的onTouchEvent返回false相速,那么它的父容器的onTouchEvent將會(huì)被調(diào)用碟渺。這個(gè)過(guò)程可以這么理解,假設(shè)點(diǎn)擊事件是一個(gè)難題突诬,難題被領(lǐng)導(dǎo)分給了程序員處理(事件分發(fā))苫拍,如果程序員處理不了(onTouchEvent返回了false),就會(huì)將難題分發(fā)給領(lǐng)導(dǎo)(上級(jí)的onTouchEvent被調(diào)用)旺隙。
下面給出一些重要的結(jié)論绒极,根據(jù)這些結(jié)論可以更好地理解整個(gè)傳遞機(jī)制。
- 同一個(gè)事件序列是指從手指接觸屏幕的那一刻起蔬捷,到手指離開(kāi)屏幕的那一刻結(jié)束時(shí)這一過(guò)程中所產(chǎn)生的一系列事件垄提。即事件序列以down事件開(kāi)始,經(jīng)過(guò)move周拐,到up事件結(jié)束铡俐。
- 一個(gè)事件序列只能被一個(gè)View攔截并消耗。這里的攔截有兩種方式妥粟,第一種是從上之下分發(fā)事件的過(guò)程中被某個(gè)View攔截审丘,另一種是底層View不消耗事件,將事件重新返回給父容器處理勾给。
- 某個(gè)View一旦決定攔截滩报,那么整個(gè)事件序列都只由它處理,并且它的onInterceptTouchEvent不會(huì)再被調(diào)用播急。
- 如果View不消耗除ACTION_DOWN以外的其他事件(即View在onTouchEvent中攔截消耗了DOWN事件脓钾,對(duì)MOVE,UP事件都返回false)旅择,對(duì)于其他的事件(MOVE,UP)此時(shí)父容器的onTouchEvent并不會(huì)被調(diào)用(畢竟事件序列已經(jīng)被View攔截了)惭笑,而是會(huì)傳遞給Activity的onTouchEvent處理。(這點(diǎn)有點(diǎn)難理解)
- ViewGroup在事件從上至下分發(fā)過(guò)程中默認(rèn)不攔截任何事件生真,即onInterceptTouchEvent返回false沉噩。View沒(méi)有onInterceptTouchEvent方法。
- (可點(diǎn)擊的)View的onTouchEvent默認(rèn)消耗事件(返回true)柱蟀,不可點(diǎn)擊的View比如TextView的onTouchEvent是不消耗事件的(返回false)
- onClick發(fā)生的前提是View是可點(diǎn)擊且收到down和up事件川蒙。
- 事件傳遞過(guò)程是由外向內(nèi)的,即事件總是先傳遞給父元素长已,然后再由父元素分發(fā)給子元素畜眨,通過(guò)requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過(guò)程昼牛,但是ANTION_DOWN除外(這點(diǎn)后面會(huì)根據(jù)源碼解釋)。
下面是事件分發(fā)的規(guī)律圖康聂。
*2. 事件分發(fā)的源碼解析
接收到事件的首先是Activity贰健,Activity不是View,但是具有和View差不多的事件傳遞的方法恬汁,該方法之中其實(shí)是由Activity中的decor view來(lái)進(jìn)行事件的分發(fā)的伶椿,以下先來(lái)分析Activity對(duì)事件的傳遞。
源碼:Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看到里面用了getWindow().superDispatchTouchEvent(ev)將事件分發(fā)下去氓侧,里面其實(shí)就是通過(guò)一個(gè)Decor View來(lái)進(jìn)行事件的分發(fā)的脊另。這里為了說(shuō)明主要問(wèn)題,所以不展開(kāi)介紹约巷。
Activity將事件傳遞下去偎痛,一般接收到事件的就是ViewGroup,因此接下來(lái)看ViewGroup的事件分發(fā)源碼独郎。在看源碼之前踩麦,我們先來(lái)回憶一下,ViewGroup分發(fā)源碼的一些結(jié)論:
- dispatchTouchEvent中
返回true時(shí)氓癌,丟棄事件靖榕;
返回false時(shí),表示不分發(fā)顽铸,將事件返回給父容器的onTouchEvent處理;
返回super.dispatchTouchEvent時(shí)料皇,將事件傳給onInterceptTouchEvent谓松; - onInterceptTouchEvent中
返回true時(shí),表示攔截践剂,觸發(fā)onTouchEvent鬼譬;
返回false時(shí),表示不攔截逊脯,將事件分發(fā)給子View优质;
返回super.onInterceptTouchEvent時(shí),跟返回false一樣(默認(rèn)不攔截事件) - onTouchEvent中
返回true時(shí)军洼,表示消耗事件巩螃;
返回false時(shí),表示不處理匕争,將事件返回給父容器的onTouchEvent避乏;
返回super.onTouchEvent,跟true一樣
然后我們帶著結(jié)論看源碼甘桑,從源碼中找出得出該結(jié)論的原因:
源碼片段:ViewGroup#dispatchTouchEvent
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
從源碼中可以看出:
- 注意intercepted標(biāo)志位后面是ed結(jié)尾的拍皮,因此它表示的意思是歹叮,已經(jīng)被攔截
- 有兩種情況來(lái)判斷是否要攔截當(dāng)前事件:
ACTION_DOWN:按下事件時(shí);
mFirstTouchTarget != null:這里mFirstTouchTarget是一個(gè)很重要的變量铆帽,可以理解為處理事件的子元素(后繼有人)咆耿,即當(dāng)之前(注意是之前)往下傳遞的事件被某個(gè)View成功處理之后,就知道“后繼有人”了爹橱,即mFirstTouchTarget被賦值萨螺。也就是說(shuō)當(dāng)后繼有人時(shí),就會(huì)進(jìn)入判斷需不需要攔截宅荤。
如果兩個(gè)條件都不能滿足時(shí)屑迂,則說(shuō)明已經(jīng)被當(dāng)前對(duì)象攔截,intercepted設(shè)成true冯键。 - FLAG_DISALLOW_INTERCEPT標(biāo)志位決定了要不要觸發(fā)onInterceptTouchEvent惹盼;
當(dāng)該標(biāo)志位為0時(shí),表示攔截惫确,觸發(fā)onInterceptTouchEvent處理事件ev手报;
當(dāng)該標(biāo)志位為1時(shí),表示不攔截改化,設(shè)置intercepted為false掩蛤;
(其實(shí)onInterceptTouchEvent默認(rèn)返回false,因此只要FLAG_DISALLOW_INTERCEPT被設(shè)置了陈肛,就不會(huì)進(jìn)行攔截) - 當(dāng)面對(duì)ACTION_DOWN事件時(shí)揍鸟,ViewGroup總是會(huì)調(diào)用onInterceptTouchEvent方法來(lái)詢問(wèn)自己是否要攔截事件
然后可以得出上面所說(shuō)的兩個(gè)結(jié)論:
- (結(jié)論3)當(dāng)某個(gè)View(包括ViewGroup)決定攔截(DOWN事件被消費(fèi)),那么這一個(gè)事件序列都只能由它來(lái)處理句旱,并且它的onInterceptTouchEvent不會(huì)再被調(diào)用阳藻,而且它的父元素對(duì)同一序列事件不再攔截。
原因:在dispatchTouchEvent之中的mFirstTouchTarget為null谈撒,不會(huì)再向下分發(fā)事件腥泥,然后onInterceptTouchEvent也執(zhí)行不到,因此在攔截之后onInterceptTouchEvent只會(huì)執(zhí)行一次啃匿。對(duì)于父元素來(lái)說(shuō)蛔外,由于mFirstTouchTarget不為null,因此對(duì)后面的事件都不攔截(注意溯乒,每個(gè)ViewGroup都有一個(gè)mFirstTouchTarget) - (結(jié)論8)在子元素中可以干預(yù)父元素的事件分發(fā)夹厌,但ACTION_DOWN除外。
原因:子元素通過(guò)requestDiallowInterceptTouchEvent可以改變父元素的FLAG_DISALLOW_INTERCEPT
標(biāo)志位橙数,而該標(biāo)志位決定了父元素要不要執(zhí)行onInterceptTouchEvent崖技。而ACTION_DOWN除外的原因在于在每個(gè)DOWN事件到來(lái)時(shí)瞎访,標(biāo)志位就會(huì)復(fù)位冀瓦。
接著再看當(dāng)ViewGroup不攔截事件的時(shí)候拾徙,事件會(huì)向下分發(fā)交由它的子View,這里就不貼出源碼了崖瞭,直接把源碼中的特點(diǎn)列出來(lái):
- ViewGroup會(huì)遍歷ViewGroup內(nèi)部的子View,然后找出符合點(diǎn)擊條件的子View
條件:子View是否在播動(dòng)畫(huà)
點(diǎn)擊事件的坐標(biāo)是否落在子View的區(qū)域內(nèi) - 通過(guò)dispatchTransformedTouchEvent分發(fā)處理事件
- 如果事件被子View處理寺惫,mFirstTouchTarget就會(huì)在addTouchTarget中被賦值萨驶,此時(shí)父元素對(duì)后面的事件都不會(huì)攔截(結(jié)論3)
- 如果事件不能被子View處理(包括子View為空或者子View的onTouchEvent返回false)叁温,mFirstTouchTarget為空谤草,則會(huì)將事件傳遞給作為一個(gè)View的自身(就是ViewGroup本身)
源碼片段:ViewGroup#dispatchTransformedTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
注:這里不要混淆了super和父容器的概念冀宴,super并不是表示父容器,super的意思是父類對(duì)象览妖,指的還是本身,只不過(guò)引用變了桅打,因此上面super.dispatchTouchEvent的意思是ViewGroup作為一個(gè)View(View是ViewGroup的父類)對(duì)象來(lái)調(diào)用dispatchTouchEvent方法來(lái)處理事件站绪。
從上面我們已知mFirstTouchTarget會(huì)決定了父容器要不要對(duì)之后的事件進(jìn)行攔截馁筐,這很重要盟迟,而且已知是在addTouchEvent中被賦值的,因此我們從源碼中看一下實(shí)現(xiàn)過(guò)程:
源碼片段:ViewGroup#dispatchTouchEvent
/*
當(dāng)事件分發(fā)到child中被處理之后就會(huì)觸發(fā)addTouchTarget方法
*/
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
源碼片段:ViewGroup#addTouchTarget
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
//在這里mFirstTouchTarget被賦值
mFirstTouchTarget = target;
return target;
}
還是用流程圖比較清晰地顯示ViewGroup的dispatchTouchEvent處理流程既绩。
上面說(shuō)明了ViewGroup的對(duì)事件的分發(fā)處理過(guò)程,總結(jié)來(lái)說(shuō)就是铝耻,看是否攔截事件蹬刷,不攔截的話就遍歷子View將事件傳遞下去瓢捉,若事件被處理了則返回,若事件沒(méi)有被處理則ViewGroup自行處理該事件办成;若攔截事件也是自行處理該事件泡态。
下面來(lái)介紹View對(duì)點(diǎn)擊事件的處理過(guò)程。(注意ViewGroup也是個(gè)View迂卢,當(dāng)ViewGroup攔截事件時(shí)某弦,ViewGroup是作為一個(gè)View來(lái)處理事件的,因此View的點(diǎn)擊事件的處理也是很重要的)
由于View的dispatchTouchEvent不用再往下分發(fā)事件了而克,只需要看看需不需要消耗該事件靶壮,因此比ViewGroup的dispatchTouchEvent簡(jiǎn)單得多。
源碼片段:View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
...
return false;
}
可以發(fā)現(xiàn)员萍,View的dispatchTouchEvent主要有兩步:
- 判斷有沒(méi)有設(shè)置OnTouchListener腾降,如果有則調(diào)用OnTouchListener的onTouch方法
- 調(diào)用onTouchEvent方法
有以下結(jié)論:
- 當(dāng)onTouch方法返回true的時(shí)候,onTouchEvent不會(huì)被調(diào)用
- onTouchEvent返回true時(shí)碎绎,View的dispatchTouchEvent返回true蜂莉,表明View消耗了該事件,此時(shí)返回到父元素ViewGroup的dispatchTouchEvent中(因?yàn)樽宇惖膁ispatchTouchEvent是在父元素的dispatchTouchEvent中調(diào)用的)混卵,mFirstTouchTarget會(huì)被設(shè)置;反之返回false窖张,表明View并不消耗該事件
看完View的事件分發(fā)流程之后幕随,來(lái)看一下View對(duì)事件的處理流程,即onTouchEvent宿接。
源碼片段:View#onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//觸發(fā)點(diǎn)擊事件
performClick();
}
}
}
...
}
break;
...
}
//只要View是可點(diǎn)擊的(CLIKABLE或者LONG_CLICKABLE)赘淮,就會(huì)消耗掉事件
return true;
}
//默認(rèn)返回false辕录,即不消耗事件
return true;
}
在View的onTouchEvent中主要關(guān)注以下幾點(diǎn):
- 如果View是可點(diǎn)擊的(CLIKABLE或者LONG_CLICKABLE),就會(huì)消耗掉事件梢卸,即返回true走诞。比如Button是可點(diǎn)擊的,TextView是不可點(diǎn)擊的蛤高。
如果View不是可點(diǎn)擊的蚣旱,默認(rèn)返回false,即不消耗事件 - 點(diǎn)擊事件onClick會(huì)在onTouchEvent的UP事件中被觸發(fā)戴陡,因此如果onTouchEvent不能被執(zhí)行的話塞绿,onClick也不會(huì)被執(zhí)行。
View事件分發(fā)總結(jié)
View事件分發(fā)我們從ViewGroup將事件往下分發(fā)恤批,因?yàn)锳ctivity中接收到的事件也是由頂級(jí)View分發(fā)的异吻,而這頂級(jí)View也是一個(gè)ViewGroup。
在總結(jié)事件分發(fā)之前喜庞,一定要明白诀浪,一般分析的是一個(gè)事件序列的事件分發(fā),即從DOWN事件到UP事件的分發(fā)延都,因?yàn)閿r截一般是對(duì)DOWN以后的事件進(jìn)行的雷猪,因?yàn)橥怯捎贒OWN被消耗之后就決定了該View會(huì)攔截以后的所有事件。
- ViewGroup的dispatchTouchEvent
首先ViewGroup接收到事件窄潭,調(diào)用dispatchTouchEvent分發(fā)事件春宣。首先有兩個(gè)判斷條件決定要不要攔截,一個(gè)是否為DOWN事件嫉你,另一個(gè)是mFirstTouchTarget對(duì)象是否為空
如果為DOWN事件或者mFirstTouchTarget不為空就會(huì)觸發(fā)onInterceptTouchEvent方法(中間省略了FDD標(biāo)志位這一步)月帝,而onInterceptTouchEvent方法默認(rèn)情況下返回false,也就是不攔截幽污,將intercepted變量設(shè)為false嚷辅;
如果不為DOWN事件并且mFirstTouchTarget為空的話,表示攔截距误,將intercepted變量設(shè)為true
然后如果決定不攔截簸搞,則遍歷ViewGroup里面的子View,找到滿足條件的子View(沒(méi)有在進(jìn)行動(dòng)畫(huà)和事件位置落在控件區(qū)域內(nèi))准潭,調(diào)用子View的dispatchTouchEvent處理事件趁俊,完成事件的分發(fā)。此時(shí)如果子View的dispatchTouchEvent返回true刑然,則說(shuō)明事件被子View攔截處理了寺擂,此時(shí)mFirstTouchTarget會(huì)被賦值,如果返回false,說(shuō)明事件沒(méi)有被處理怔软,mFirstTouchTarget還是為空垦细。
最后判斷mFirstTouchTarget是否為空,如果為空則說(shuō)明事件沒(méi)有被子類處理挡逼,所以此時(shí)ViewGroup調(diào)用super.dispatcherTouchEvent作為View來(lái)處理事件
這就是ViewGroup的dispatchTouchEvent流程括改,簡(jiǎn)單總結(jié)就是兩個(gè)過(guò)程:
從上至下:判斷事件需不需要攔截,如果不需要?jiǎng)t將事件分發(fā)給子View處理家坎,如果需要攔截則自行處理事件()嘱能;
從下至上:如果之前事件不攔截交與子View處理,并且子View并沒(méi)有處理(dispatchTouchEvent返回false)乘盖,此時(shí)ViewGroup本身會(huì)作為View來(lái)調(diào)用dispatchTouchEvent自行處理事件
View的dispatchTouchEvent
上面說(shuō)到了ViewGroup不攔截事件時(shí)會(huì)調(diào)用子View的dispatchTouchEvent方法焰檩,或者子View不處理事件時(shí)自行作為View處理事件,因此View的dispatchTouchEvent是很重要的订框。
在View的dispatchTouchEvent中析苫,首先會(huì)判斷View是否設(shè)置了OnTouchListener以及調(diào)用onTouch方法,如果onTouch方法返回true穿扳,則dispatchTouchEvent會(huì)返回true表明事件被消費(fèi)衩侥,并且不會(huì)執(zhí)行onTouchEvent方法;
如果onTouch方法返回false矛物,則onTouchEvent會(huì)被調(diào)用茫死,此時(shí)如果onTouchEvent返回true,則dispatchTouchEvent也會(huì)返回true履羞。
最后View的dispatchTouchEvent默認(rèn)返回falseView的onTouchEvent
在事件的最后肯定是onTouchEvent在處理峦萎,而onTouchEvent中也比較簡(jiǎn)單,主要有兩點(diǎn)忆首,一是可點(diǎn)擊的View默認(rèn)返回true爱榔,不可點(diǎn)擊的View默認(rèn)返回false;二是onClick方法會(huì)在UP事件中被調(diào)用糙及,因此onClick方法調(diào)用的前提是onTouchEvent方法被執(zhí)行详幽。
到此事件分發(fā)便完成了,主要從ViewGroup的dispatchTouchEvent浸锨,View的dispatchTouchEvent和View的onTouchEvent三個(gè)方法來(lái)進(jìn)行分析的唇聘。
3.4 View的滑動(dòng)沖突
滑動(dòng)沖突產(chǎn)生原因:內(nèi)外兩層同時(shí)可以滑動(dòng),這個(gè)時(shí)候會(huì)產(chǎn)生滑動(dòng)沖突柱搜。
滑動(dòng)沖突是一個(gè)核心章節(jié)迟郎,前面幾節(jié)的內(nèi)容都是為此節(jié)服務(wù)的。
總的來(lái)說(shuō)View的滑動(dòng)沖突有兩種解決方法聪蘸,一種是外部攔截法宪肖,一種是內(nèi)部攔截法炒嘲。
外部攔截法比較簡(jiǎn)單也比較容易理解,因此只介紹外部攔截法匈庭。這里拿ViewPager來(lái)舉例,比如微信的界面浑劳,左右滑是切換界面阱持,上下滑是滑動(dòng)列表,因此這里需要解決滑動(dòng)沖突魔熏。沖突的解決目的很明確衷咽,就是橫向滑動(dòng)時(shí)滑動(dòng)的就是界面,縱向滑動(dòng)時(shí)滑動(dòng)的就是列表蒜绽,因此根據(jù)目的我們可以知道镶骗,只要將不同的事件分發(fā)給不同的控件就行了。而這里假設(shè)外層是一個(gè)橫向的LinearLayout躲雅,界面里面是一個(gè)ListView鼎姊,因此我們需要把橫向滑動(dòng)的事件給LinearLayout處理,把縱向滑動(dòng)事件給ListView處理相赁。而ListView是位于LinearLayout里面的相寇,因此LinearLayout是ListView的父容器,所以整個(gè)問(wèn)題就轉(zhuǎn)化成了钮科,在事件分發(fā)的過(guò)程中唤衫,如果判定為橫向滑動(dòng),則LinearLayout把事件攔截處理绵脯,否則將事件分發(fā)給ListView佳励。
在前面一節(jié)中可以知道,在ViewGroup的onInterceptTouchEvent中默認(rèn)返回的是false蛆挫,因此我們可以重寫(xiě)onInterceptTouchEvent來(lái)達(dá)到攔截事件的目的赃承。需要注意的點(diǎn)主要有:
- DOWN和UP事件都需要返回false,因此如果返回true的話璃吧,事件會(huì)被攔截導(dǎo)致子類直接收不到事件而不是經(jīng)過(guò)判斷之后才決定攔不攔截楣导。
- 在MOVE事件中判斷是否需要攔截,也就是判斷是否為橫向滑動(dòng)畜挨,如果是橫向滑動(dòng)則返回true筒繁,反之返回false。
- 最后不要忘了在onTouchEvent中進(jìn)行事件的處理巴元。
以下是簡(jiǎn)單的代碼毡咏,只要是為了體現(xiàn)外部攔截法的寫(xiě)法。
public class HorizontalScrollViewEx extends ViewGroup
{
...
@Override
public boolean onInterceptTouchEvent(MotionEvent event)
{
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if(Math.abs(deltaX) > Math.abs(deltaY))
{
//橫向滑動(dòng)
intercepted = true;
}else
{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvene(MotionEvent event)
{
...
return true;
}
...
}