第3章 View的事件體系

第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類:

  1. left,top,right,bottom
  2. x,y
  3. translationX,translationY

注:
這里需要提前聲明一下哭廉,這些位置參數(shù)都是相對(duì)于父控件的怀跛,因此不要混淆距贷,下面具體介紹一下。

  1. left,top,right,bottom這四種從名字就能知道這是View控件相對(duì)于父控件的左上右底四個(gè)邊緣的坐標(biāo)
  2. x,y是左上角相對(duì)于父控件的坐標(biāo)
  3. 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ì)于父控件的迹辐。

View的位置參數(shù).png

2. MotionEvent和TouchSlop

2.1 MotionEvent
MotionEvent是點(diǎn)擊事件中不可缺少的類醇疼,所有關(guān)于點(diǎn)擊事件的信息都集成在該類上烙肺,因此需要來(lái)了解該事件類钦勘,先總結(jié)一下從MotionEvent中可以獲得到什么示血,主要有兩個(gè)棋傍。

  1. 事件類型信息ACTION:ACTION_DOWN,ACTION_MOVE,ACTION_UP,通過(guò)event.getAction()獲得
  2. 坐標(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)的方式總共有三種:

  1. 通過(guò)View本身提供的scrollTo/ScrollBy方法
  2. 通過(guò)動(dòng)畫(huà)給View施加平移效果
  3. 通過(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ì)減小摆舟,如圖所示亥曹。

mScrollX和mScrollY的變換規(guī)律示意圖.PNG

因此可得出結(jié)論:

  1. scrollTo給定(0,0)參數(shù)時(shí),內(nèi)容返回原位恨诱,給定負(fù)數(shù)的參數(shù)時(shí)媳瞪,內(nèi)容往相對(duì)于原點(diǎn)的正方向移動(dòng)變化
  2. scrollBy給定負(fù)數(shù)的參數(shù)時(shí),內(nèi)容往屏幕正方向移動(dòng)照宝;反之往負(fù)方向移動(dòng)
  3. 記住蛇受,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步:

  1. 先獲取到View的布局參數(shù)對(duì)象(LayoutParams)
  2. 修改該對(duì)象的參數(shù)
  3. 重繪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)完成:

  1. dispatchTouchEvent(MotionEvent ev):用來(lái)進(jìn)行事件的分發(fā)抵拘。如果事件能夠傳遞給當(dāng)前的View,那么此方法一定會(huì)被調(diào)用型豁。
  2. onInterceptTouchEvent(MotionEvent ev):用于進(jìn)行事件的攔截僵蛛。如果當(dāng)前View攔截了某個(gè)事件,那么在同一個(gè)事件序列當(dāng)中迎变,此方法不會(huì)被再次調(diào)用充尉。
  3. 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ī)制。

  1. 同一個(gè)事件序列是指從手指接觸屏幕的那一刻起蔬捷,到手指離開(kāi)屏幕的那一刻結(jié)束時(shí)這一過(guò)程中所產(chǎn)生的一系列事件垄提。即事件序列以down事件開(kāi)始,經(jīng)過(guò)move周拐,到up事件結(jié)束铡俐。
  2. 一個(gè)事件序列只能被一個(gè)View攔截并消耗。這里的攔截有兩種方式妥粟,第一種是從上之下分發(fā)事件的過(guò)程中被某個(gè)View攔截审丘,另一種是底層View不消耗事件,將事件重新返回給父容器處理勾给。
  3. 某個(gè)View一旦決定攔截滩报,那么整個(gè)事件序列都只由它處理,并且它的onInterceptTouchEvent不會(huì)再被調(diào)用播急。
  4. 如果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)難理解)
  5. ViewGroup在事件從上至下分發(fā)過(guò)程中默認(rèn)不攔截任何事件生真,即onInterceptTouchEvent返回false沉噩。View沒(méi)有onInterceptTouchEvent方法。
  6. (可點(diǎn)擊的)View的onTouchEvent默認(rèn)消耗事件(返回true)柱蟀,不可點(diǎn)擊的View比如TextView的onTouchEvent是不消耗事件的(返回false)
  7. onClick發(fā)生的前提是View是可點(diǎn)擊且收到down和up事件川蒙。
  8. 事件傳遞過(guò)程是由外向內(nèi)的,即事件總是先傳遞給父元素长已,然后再由父元素分發(fā)給子元素畜眨,通過(guò)requestDisallowInterceptTouchEvent方法可以在子元素中干預(yù)父元素的事件分發(fā)過(guò)程昼牛,但是ANTION_DOWN除外(這點(diǎn)后面會(huì)根據(jù)源碼解釋)。

下面是事件分發(fā)的規(guī)律圖康聂。

事件分發(fā)機(jī)制.PNG

*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é)論:

  1. dispatchTouchEvent中
    返回true時(shí)氓癌,丟棄事件靖榕;
    返回false時(shí),表示不分發(fā)顽铸,將事件返回給父容器的onTouchEvent處理;
    返回super.dispatchTouchEvent時(shí)料皇,將事件傳給onInterceptTouchEvent谓松;
  2. onInterceptTouchEvent中
    返回true時(shí),表示攔截践剂,觸發(fā)onTouchEvent鬼譬;
    返回false時(shí),表示不攔截逊脯,將事件分發(fā)給子View优质;
    返回super.onInterceptTouchEvent時(shí),跟返回false一樣(默認(rèn)不攔截事件)
  3. 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;
}

從源碼中可以看出:

  1. 注意intercepted標(biāo)志位后面是ed結(jié)尾的拍皮,因此它表示的意思是歹叮,已經(jīng)被攔截
  2. 有兩種情況來(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冯键。
  3. 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)行攔截)
  4. 當(dāng)面對(duì)ACTION_DOWN事件時(shí)揍鸟,ViewGroup總是會(huì)調(diào)用onInterceptTouchEvent方法來(lái)詢問(wèn)自己是否要攔截事件

然后可以得出上面所說(shuō)的兩個(gè)結(jié)論:

  1. (結(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)
  2. (結(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):

  1. ViewGroup會(huì)遍歷ViewGroup內(nèi)部的子View,然后找出符合點(diǎn)擊條件的子View
    條件:子View是否在播動(dòng)畫(huà)
    點(diǎn)擊事件的坐標(biāo)是否落在子View的區(qū)域內(nèi)
  2. 通過(guò)dispatchTransformedTouchEvent分發(fā)處理事件
  3. 如果事件被子View處理寺惫,mFirstTouchTarget就會(huì)在addTouchTarget中被賦值萨驶,此時(shí)父元素對(duì)后面的事件都不會(huì)攔截(結(jié)論3)
  4. 如果事件不能被子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處理流程既绩。

ViewGroup的dispatchTouchEvene處理流程.PNG

上面說(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主要有兩步:

  1. 判斷有沒(méi)有設(shè)置OnTouchListener腾降,如果有則調(diào)用OnTouchListener的onTouch方法
  2. 調(diào)用onTouchEvent方法

有以下結(jié)論:

  1. 當(dāng)onTouch方法返回true的時(shí)候,onTouchEvent不會(huì)被調(diào)用
  2. 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):

  1. 如果View是可點(diǎn)擊的(CLIKABLE或者LONG_CLICKABLE),就會(huì)消耗掉事件梢卸,即返回true走诞。比如Button是可點(diǎn)擊的,TextView是不可點(diǎn)擊的蛤高。
    如果View不是可點(diǎn)擊的蚣旱,默認(rèn)返回false,即不消耗事件
  2. 點(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ì)攔截以后的所有事件。

  1. 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自行處理事件

  1. 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)返回false

  2. View的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)主要有:

  1. DOWN和UP事件都需要返回false,因此如果返回true的話璃吧,事件會(huì)被攔截導(dǎo)致子類直接收不到事件而不是經(jīng)過(guò)判斷之后才決定攔不攔截楣导。
  2. 在MOVE事件中判斷是否需要攔截,也就是判斷是否為橫向滑動(dòng)畜挨,如果是橫向滑動(dòng)則返回true筒繁,反之返回false。
  3. 最后不要忘了在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;
    }


    ...
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末逮刨,一起剝皮案震驚了整個(gè)濱河市呕缭,隨后出現(xiàn)的幾起案子堵泽,更是在濱河造成了極大的恐慌,老刑警劉巖恢总,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迎罗,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡片仿,警方通過(guò)查閱死者的電腦和手機(jī)纹安,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)砂豌,“玉大人厢岂,你說(shuō)我怎么就攤上這事⊙艟啵” “怎么了塔粒?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)筐摘。 經(jīng)常有香客問(wèn)我卒茬,道長(zhǎng),這世上最難降的妖魔是什么蓄拣? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任扬虚,我火速辦了婚禮,結(jié)果婚禮上球恤,老公的妹妹穿的比我還像新娘辜昵。我一直安慰自己,他們只是感情好咽斧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布堪置。 她就那樣靜靜地躺著,像睡著了一般张惹。 火紅的嫁衣襯著肌膚如雪舀锨。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天宛逗,我揣著相機(jī)與錄音坎匿,去河邊找鬼。 笑死雷激,一個(gè)胖子當(dāng)著我的面吹牛替蔬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播屎暇,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼承桥,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了根悼?” 一聲冷哼從身側(cè)響起凶异,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蜀撑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后剩彬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體酷麦,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年喉恋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贴铜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瀑晒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出徘意,到底是詐尸還是另有隱情苔悦,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布椎咧,位于F島的核電站玖详,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏惩琉。R本人自食惡果不足惜悲酷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一籽孙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧向臀,春花似錦、人聲如沸诸狭。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)驯遇。三九已至芹彬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叉庐,已是汗流浹背舒帮。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陡叠,地道東北人玩郊。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像匾竿,于是被迫代替她去往敵國(guó)和親瓦宜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容