Android View事件機制一些事

本文主要講述:

  • 自己對View事件機制的一些理解
  • 在項目中遇到的一些坑,解決方案
  • 收集了一些View的事件機制問題

事件的分發原理圖

  • 對於一個root viewgroup來說,如果接受了一個點擊事件,那麼首先會調用他的dispatchTouchEvent方法。

  • 如果這個viewgroup的onInterceptTouchEvent 返回true,那就代表要攔截這個事件。接下來這個事件就

  • 給viewgroup自己處理了,從而viewgroup的onTouchEvent方法就會被調用。如果如果這個viewgroup的onInterceptTouchEvent

  • 返回false就代表我不攔截這個事件,然後就把這個事件傳遞給自己的子元素,然後子元素的dispatchTouchEvent

  • 就會被調用,就是這樣一個循環直到 事件被處理。
    圖:
    這裏寫圖片描述

完整事件流程:
dispatchTouchEvent(true) -> onInterceptTouchEvent(true) -> onTouchEvent(true) - 事件結束

重要的事情說一遍:

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

api 描述:

  • dispatchTouchEvent 分發事件
    return false; //表示分發,默認false;
    return true; // 表示不分發;

  • onInterceptTouchEvent 攔截事件
    當dispatchTouchEvent 確認分發,會啓動攔截事件;
    return false; //表示不攔截,默認false;
    return true; // 表示攔截;

注意:攔截是相當於它的孩子(也就是說不會攔截自己,如果攔截,則TouchEvent會傳到他自己,而它孩子就接收不)
不攔截會繼續往他的孩子遞歸是否onInterceptTouchEvent ;

  • onTouchEvent 觸摸事件
    return false; //表示不消費,默認false;
    return true; // 表示消費;

當onInterceptTouchEvent 確認攔截,會問自己是否要消費TouchEvent,
如果攔截了又不消費則,Touch結束;

  • invalidate 重新繪製
    讓整個view失效,這樣view會被重新調用, 配合onDraw()使用;
    下面是調用流程:
    當invalidate時會重新調用draw方法,
    draw會調用onDraw,而在draw內還會調用computeScroll(),

此時如果想讓computeScroll()循環被調用可以在computeScroll()內自己調用postInvaildate()重新繪製.
computeScroll() 源碼是空實現,具體實現由自己來寫

常見問題

  • 1.view的onTouchEvent,OnClickListerner和OnTouchListener的onTouch方法 三者優先級如何?
    答:
    onTouchListener優先級最高,如果onTouch方法返回 false ,那onTouchEvent就被調用了,返回true 就不會被調用。至於onClick 優先級最低。

  • 2.點擊事件的傳遞順序如何?
    答:
    Activity-Window-View。從上到下依次傳遞,當然瞭如果你最低的那個view onTouchEvent返回false 那就說明他不想處理 那就再往上拋,都不處理的話
    最終就還是讓Activity自己處理了。舉個例子,pm下發一個任務給leader,leader自己不做 給架構師a,小a也不做 給程序員b,b如果做了那就結束了這個任務。
    b如果發現自己搞不定,那就找a做,a要是也搞不定 就會不斷向上發起請求,最終可能還是pm做。

//activity的dispatchTouchEvent 方法 一開始就是交給window去處理的
//win的superDispatchTouchEvent 返回true 那就直接結束了 這個函數了。返回false就意味
//這事件沒人處理,最終還是給activity的onTouchEvent 自己處理 這裏的getwindow 其實就是phonewindow
 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
//來看phonewindow的這個函數 直接把事件傳遞給了mDecor
 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
//devorview就是 我們的rootview了 就是那個framelayout 我們的setContentView裏面傳遞的那個layout
//就是這個decorview的 子view了
     @Override
    public final View getDecorView() {
        if (mDecor == null) {
            installDecor();
        }
        return mDecor;
    }
  • 3.enable是否影響view的onTouchEvent返回值?
    答:
    不影響,只要clickable和longClickable有一個爲真,那麼onTouchEvent就返回true。

  • 4.滑動衝突問題如何解決 思路是什麼?
    答:
    讓誰消費滑動:
    要解決滑動衝突 其實最主要的就是有一個核心思想。你到底想在一個事件序列中,讓哪個view 來響應你的滑動?比如 從上到下滑,是哪個view來處理這個事件,從左到右呢?
    攔截內外滑動:
    用業務需求 來想明白以後 剩下的 其實就很好做了。核心的方法 就是2個 外部攔截也就是父親攔截,另外就是內部攔截,也就是子view攔截法。 學會這2種 基本上所有的滑動衝突.
    都是這2種的變種,而且核心代碼思想都一樣。

    外部攔截法:思路就是重寫父容器的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因爲這和android自身的事件處理機制 邏輯是一模一樣的

父容器示例代碼:

@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 (你的業務需求) {
//如果確定攔截了 就去自己的onTouchEvent裏 處理攔截之後的操作和效果 即可了
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
 //up事件 我們一般都是返回false的 一般父容器都不會攔截他。 因爲up是事件的最後一步。這裏返回true也沒啥意義
 //唯一的意義就是因爲 父元素 up被攔截。導致子元素 收不到up事件,那子元素 就肯定沒有onClick事件觸發了,這裏的
//小細節 要想明白
                intercepted = false;
                break;
            default:
                break;
        }
        return intercepted;
    }

內部攔截法:內部攔截法稍微複雜一點,就是事件到來的時候,父容器不管,讓子元素自己來決定是否處理。如果消耗了 就最好,沒消耗 自然就轉給父容器處理了。

子元素代碼:

@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);
                break;
            case MotionEvent.ACTION_MOVE:
                if (如果父容器需要這個點擊事件) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }//否則的話 就交給自己本身view的onTouchEvent自動處理了
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

PS: 父親容器代碼也要修改一下,其實就是保證父親別攔截down:

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

案列: 分發的例子

下面紅色框區域的結構是ScrollView, 它的孩子是一些TextView;

分析:

  • 1.當點擊它任意一個孩子(TextView)時,如果ScrollView不進行onInterceptTouchEvent ,則它就不可以在菜單上進行左右滑動;

  • 2.但是如果攔截了全部,則它的孩子又會消費不了TouchEvent;

解決方法:
只有左右移動的時候進行攔截,這樣父親就擁有了TouchEvent,可在菜單上繼續左右滑動,

而上下移動或靜止的時候就不攔截,這樣孩子又有了TouchEvent,那麼孩子就可以點擊了;

實例代碼:

/**
 * 當滑動的時候,需要攔截TouchEvent時間,讓scrollView消化,否則會分發到孩子去;
 * 當不滑動的停止的時候,不攔截,則會分發到孩子去,也就是TexView;
 */
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    // 只有水平滑動時才攔截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) {
            // 水平滑動,只有水平滑動纔會攔截事件
            return true;
        }
        startX = (int) ev.getRawX();// 初始化當前位置
    case MotionEvent.ACTION_UP:
        break;
    }
    return super.onInterceptTouchEvent(ev);
}  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章