View的事件分發(fā)機(jī)制小結(jié)

事件的分發(fā)原理圖:

  1. 對(duì)于一個(gè)root viewgroup來說,如果接受了一個(gè)點(diǎn)擊事件,那么首先會(huì)調(diào)用他dispatchTouchEvent方法.
  2. viewgroup的onInterceptTouchEvent 返回true,那就代表要攔截這個(gè)事件.接下來這個(gè)事件就給viewgroup自己處理了,從而viewgroup的onTouchEvent方法就會(huì)被調(diào)用.
  3. viewgroup的onInterceptTouchEvent返回false就代表我不攔截這個(gè)事件,然后就把這個(gè)事件傳遞給自己的子元素,然后子元素的dispatchTouchEvent就會(huì)被調(diào)用,就是這樣一個(gè)循環(huán)直到 事件被處理.

我們可以看下事件分發(fā)的原理圖.
圖1:

img-w400

簡(jiǎn)單的說只要各事件不消費(fèi),返回false,分發(fā)就會(huì)一直走下去:
dispatchTouchEvent(false) -> onInterceptTouchEvent(false) -> onTouchEvent(false) - 事件結(jié)束

重要的事情說一遍:

也就是說在任何View或者ViewGrop中只要它想消費(fèi)Touch事件,就直接onInterceptTouchEvent(true),這樣它就不會(huì)把事件傳下去給孩子view了,而是自己消費(fèi).

知其人先知其心,我們繼續(xù)進(jìn)一步了解下其他事件分發(fā)的api.

  • dispatchTouchEvent 分發(fā)事件
    return false; //不是目標(biāo)對(duì)象,則分發(fā),默認(rèn)false;
    return true; // 是目標(biāo)view,則不分發(fā);

dispatchTouchEvent作用是將touch事件向下傳遞直到遇到被觸發(fā)的目標(biāo)view.
我們可以通過返回的boolean對(duì)touch事件的分發(fā)進(jìn)行處理,是否要向下分發(fā)尋找目標(biāo)view,當(dāng)然這個(gè)方法也可以被重載,手動(dòng)分配事件.

  • onInterceptTouchEvent 攔截事件
    return false; //表示不攔截,默認(rèn)false;
    return true; // 表示攔截;

攔截是相當(dāng)于它的孩子,也就是說不會(huì)攔截自己.
如果攔截,則TouchEvent會(huì)傳給他自己,而它孩子是接收不了.
如果不攔截會(huì)繼續(xù)往他的孩子遞歸是否onInterceptTouchEvent需要攔截.

  • onTouchEvent 觸摸事件
    return false; //表示不消費(fèi),默認(rèn)false;
    return true; // 表示消費(fèi);

當(dāng)onInterceptTouchEvent 確認(rèn)攔截,會(huì)問自己是否要消費(fèi)TouchEvent,如果攔截了又不消費(fèi)則,那么Touch結(jié)束.

  • invalidate 重新繪制
    讓整個(gè)view失效,這樣view會(huì)被重新調(diào)用, 配合onDraw()使用.

下面是調(diào)用流程:

  1. 當(dāng)invalidate時(shí)會(huì)重新調(diào)用draw方法;
  2. draw會(huì)調(diào)用onDraw,而在draw內(nèi)還會(huì)調(diào)用computeScroll();
  3. 此時(shí)如果想讓computeScroll()循環(huán)被調(diào)用可以在computeScroll()內(nèi)自己調(diào)用postInvaildate()重新繪制;

invalidate刷新UI步驟: draw() -> onDraw() -> computeScroll()

computeScroll() 源碼是空實(shí)現(xiàn),具體實(shí)現(xiàn)由自己來寫.

開發(fā)中事件分發(fā)的常見問題(重點(diǎn))

  • view的onTouchEvent,OnClickListerner和OnTouchListener的onTouch方法 三者優(yōu)先級(jí)如何?
    答:
    onTouchListener優(yōu)先級(jí)最高,也就是說如果onTouch方法返回 true ,那么事件結(jié)束,反之如果返回false,那么onTouchEvent 講會(huì)被調(diào)用,至于OnClickListerner 優(yōu)先級(jí)是最低的.
    優(yōu)先級(jí)如下:
    OnTouchListener > onTouchEvent > OnClickListerner

  • 點(diǎn)擊事件的傳遞順序如何?
    答:

  • Activity > Window > View.從上到下依次傳遞.

  • 如果你最低的那個(gè)view onTouchEvent返回false 那就說明他不想處理 那就再往下拋,都不處理的話最終就還是讓Activity自己處理了。

  • 舉個(gè)例子,pm下發(fā)一個(gè)任務(wù)給leader,leader自己不做 給架構(gòu)師a,小a也不做 給程序員b,b如果做了那就結(jié)束了這個(gè)任務(wù)。b如果發(fā)現(xiàn)自己搞不定,那就找a做,a要是也搞不定 就會(huì)不斷向上發(fā)起請(qǐng)求,最終可能還是pm做。

  • 總結(jié)下流程: view的事件分發(fā)會(huì)從上往下,只要在子不消費(fèi)的情況,又會(huì)接著從下往上,最后結(jié)束.

//activity的dispatchTouchEvent 方法 一開始就是交給window去處理的
//win的superDispatchTouchEvent 返回true 那就直接結(jié)束了 這個(gè)函數(shù)了。返回false就意味
//這事件沒人處理,最終還是給activity的onTouchEvent 自己處理 這里的getwindow 其實(shí)就是phonewindow
 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
//來看phonewindow的這個(gè)函數(shù) 直接把事件傳遞給了mDecor
 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
//devorview就是 我們的rootview了 就是那個(gè)framelayout 我們的setContentView里面?zhèn)鬟f的那個(gè)layout
//就是這個(gè)decorview的 子view了
     @Override
    public final View getDecorView() {
        if (mDecor == null) {
            installDecor();
        }
        return mDecor;
    }
  • enable是否影響view的onTouchEvent返回值?
    我們知道其實(shí)enable的優(yōu)先級(jí)高于cliable,當(dāng)enable=false時(shí)會(huì)屏蔽view的點(diǎn)擊事件.而事實(shí)上enable=false并不會(huì)影響onTouchEvent返回true.

答:

  • 不影響,只要clickable和longClickable有一個(gè)為真,那么onTouchEvent就返回true。
  • 設(shè)置了enable為false的話,onClick事件是完全屏蔽的,而clickable屬性就要看設(shè)置屬性和設(shè)置OnClicListener的先后順序了.

我們可以看下面demo
xml代碼:

android:clickable="true"
android:enabled="false"

android代碼:

//我在XML布局中設(shè)置了enabled="false",雖然是屏蔽了點(diǎn)擊事件,但是在自定義Button中,實(shí)現(xiàn)的`onTouchEvent`方法還是會(huì)返回true.
public class MyButton extends Button {
    public MyButton(Context context) {
        super(context);
    }

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        System.out.println("onTouchEvent:"+super.onTouchEvent(event));
        return super.onTouchEvent(event);
    }
}

輸出結(jié)果:
onTouchEvent:true

  • 滑動(dòng)沖突問題如何解決思路是什么?
    答:
  • 讓誰消費(fèi)滑動(dòng):
    要解決滑動(dòng)沖突 其實(shí)最主要的就是有一個(gè)核心思想。你到底想在一個(gè)事件序列中,讓哪個(gè)view 來響應(yīng)你的滑動(dòng)?比如 從上到下滑,是哪個(gè)view來處理這個(gè)事件,從左到右呢?
  • 攔截內(nèi)外滑動(dòng):
    用業(yè)務(wù)需求來想明白以后剩下的其實(shí)就很好做了。
    核心的方法就2個(gè):
    1. 外部攔截
      也就是父攔截.(重寫父控件的onInterceptTouchEvent即可).
    2. 內(nèi)部攔截
      也就是子view攔截方法.

學(xué)會(huì)這2種,基本上所有的滑動(dòng)沖突.都是這2種的變種,而且核心代碼思想都一樣,下面是兩種情況是示例代碼:

  • 外部攔截法:思路就是重寫父控件的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因?yàn)檫@和android自身的事件處理機(jī)制 邏輯是一模一樣的.
    父控件示例代碼:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
   //down事件肯定不能攔截 攔截了后面的就收不到了
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (你的業(yè)務(wù)需求) {
//如果確定攔截了 就去自己的onTouchEvent里 處理攔截之后的操作和效果 即可了
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
 //up事件 我們一般都是返回false的 一般父控件都不會(huì)攔截他。 因?yàn)閡p是事件的最后一步。這里返回true也沒啥意義
 //唯一的意義就是因?yàn)?父元素 up被攔截。導(dǎo)致子元素 收不到up事件,那子元素 就肯定沒有onClick事件觸發(fā)了,這里的
//小細(xì)節(jié) 要想明白
                intercepted = false;
                break;
            default:
                break;
        }
    return intercepted;
}
  • 內(nèi)部攔截法:內(nèi)部攔截法稍微復(fù)雜一點(diǎn),就是事件到來的時(shí)候,父控件不管,讓子元素自己來決定是否處理。如果消耗了就最好,沒消耗自然就轉(zhuǎn)給父控件處理了。
    子控件示例代碼:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);//子元素自己消費(fèi),父控件不進(jìn)行攔截
                break;
            case MotionEvent.ACTION_MOVE:
                if (如果父控件需要這個(gè)點(diǎn)擊事件) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }//否則的話 就交給自己本身view的onTouchEvent自動(dòng)處理了
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
}

PS: 父控件代碼也要修改一下,其實(shí)就是保證父控件別攔截down:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            return false;
        }
        return true;
}

事件分發(fā)的項(xiàng)目

需求:仿QQ側(cè)滑菜單

  1. google給我們提供了DrawerLayout控件來作為側(cè)滑菜單控件,但是側(cè)滑菜單一般都是覆蓋到主頁面頂部的.
  2. 我們需求在向右拖拉滑動(dòng)時(shí)可以側(cè)滑出菜單,并且要求菜單不可覆蓋到主頁面上,需求主頁面跟著側(cè)滑菜單的滑動(dòng)和位移.

圖2:


img-w230

上面紅色框區(qū)域的結(jié)構(gòu)可以這樣設(shè)計(jì)ScrollView+多TextView.使用scrollView的原因是當(dāng)菜單的item增加時(shí)可以滾動(dòng),當(dāng)然listview,rv也可以做到.

問題分析:

  1. 當(dāng)點(diǎn)擊它任意一個(gè)孩子(TextView)時(shí),如果ScrollView不進(jìn)行onInterceptTouchEvent ,則它就不可以在菜單上進(jìn)行左右滑動(dòng).
  2. 但是如果攔截了全部,則它的孩子又會(huì)消費(fèi)不了TouchEvent.

問題解決:

  1. 只有左右移動(dòng)的時(shí)候進(jìn)行攔截,這樣父控件就擁有了TouchEvent,可在菜單上繼續(xù)左右滑動(dòng).
  2. 上下移動(dòng)或靜止的時(shí)候就不攔截,這樣孩子又有了TouchEvent,那么孩子就可以點(diǎn)擊了.

實(shí)例代碼:

/**
 * 當(dāng)滑動(dòng)的時(shí)候,需要攔截TouchEvent時(shí)間,讓scrollView消化,否則會(huì)分發(fā)到孩子去;
 * 當(dāng)不滑動(dòng)的停止的時(shí)候,不攔截,則會(huì)分發(fā)到孩子去,也就是TexView;
 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    // 只有水平滑動(dòng)時(shí)才攔截touch
    case MotionEvent.ACTION_DOWN:
        startX = (int) (ev.getRawX() + 0.5f);
        startY = (int) (ev.getRawY() + 0.5f);
        break;
    case MotionEvent.ACTION_MOVE:
        int newX = (int) (ev.getRawX() + 0.5f);
        int newY = (int) (ev.getRawY() + 0.5f);
        int dx = Math.abs(startX - newX);
        int dy = Math.abs(startY - newY);
        if (dx > dy) {
            // 水平滑動(dòng),只有水平滑動(dòng)才會(huì)攔截事件
            return true;
        }
        startX = (int) ev.getRawX();// 初始化當(dāng)前位置
    case MotionEvent.ACTION_UP:
        break;
    }
    return super.onInterceptTouchEvent(ev);
}  

參考來源:希爾瓦娜斯女神

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

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

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