Android技能樹 — View事件體系小結

青蛙要fly發表於2018-02-10

前言

最近年底了,打算把自己的Android知識都整理一下。

Android技能書系列:

Android基礎知識

Android技能樹 — 動畫小結

Android技能樹 — View小結

Android技能樹 — Activity小結

Android技能樹 — View事件體系小結

Android技能樹 — Android儲存路徑及IO操作小結

Android技能樹 — 多程式相關小結

Android技能樹 — Drawable小結

資料結構基礎知識

Android技能樹 — 陣列,連結串列,雜湊表基礎小結

Android技能樹 — 樹基礎知識小結(一)

演算法基礎知識

Android技能樹 — 排序演算法基礎小結

這次是講View的事件體系。特別是不同情況下的事件分發,我會用很簡單的方式教會大家。

還是老樣子,先上腦圖,然後具體一塊塊詳細說明。

腦圖連結:View事件體系

Android技能樹 — View事件體系小結
View事件體系

Android技能樹 — View事件體系小結

我們通過具體案例來學習

View相關的基礎知識

比如我們現在的需求是這樣的:介面上有一個按鈕,我們的手指點選這個按鈕後滑動,這個按鈕可以跟著我們的手指一起滑動。(桌面的一些小的清理垃圾的懸浮窗的操作差不多,明白了吧)

具體實現可以看我以前寫過的文章,十分簡單: 小Demo大知識-控制Button移動來學Android座標

Android技能樹 — View事件體系小結

我們來分析,既然按鈕可以跟著我們手指滑動,我們肯定是不停告訴按鈕,當前你的位置是哪裡,既然涉及到一些基本知識點,比如View的位置引數等等。

View的位置引數

Android技能樹 — View事件體系小結

這裡我配上一張圖,更清楚的來說明這些獲取各自引數的值的說明:

(!!!!!這裡我多畫了getRawX和getRawY方法,View是沒有這二個方法的,請注意!!!!!)

(!!!!!這裡我多畫了getRawX和getRawY方法,View是沒有這二個方法的,請注意!!!!!)

(!!!!!這裡我多畫了getRawX和getRawY方法,View是沒有這二個方法的,請注意!!!!!)

Android技能樹 — View事件體系小結

看了這個圖,是不是馬上很清楚了。

注意點:

Android技能樹 — View事件體系小結

這裡要說明一個誤區,我面試一些初級水平安卓,我說ViewGroup裡面有個View,這個View的getLeft(),getTop(),getTop(),getBottom()是什麼,讓他畫給我看下,有些人會給下面這個答案:

Android技能樹 — View事件體系小結
錯誤的回答

這是錯誤的答案,而且根據正確的描述圖,我們可以通過getLeft(),getTop(),getTop(),getBottom()來獲取相應的View的寬高:

width = getRight() - getLeft();
height = getBottom() - getTop();
複製程式碼

Android技能樹 — View事件體系小結

View操作相關知識

Android技能樹 — View事件體系小結

MotinoEvent

Android技能樹 — View事件體系小結

MotionEvent是什麼,單獨問大家可能有點懵逼,我們來寫下我們平常經常寫的設定觸控的監聽方法:

view.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return false;
    }
});
複製程式碼

有沒有發現,裡面傳遞過來的引數就有MotionEvent

Android技能樹 — View事件體系小結

我們可以看到,MotionEvent是觸屏事件。當使用者觸控螢幕時將產生觸屏事件,事件相關細節(發生觸控的位置、時間、歷史記錄、手勢動作等)被封裝成MotionEvent物件。

具體的介紹真的很多,百度一搜一大把。要細講實在太多了。這裡不多介紹了。

特別提示!!!
很多人會把上面我們提到過的view.getX/Y()和這裡的motionvent.getX/Y()弄混。這裡是有差別的。我再畫個圖來明確下二者的區別。

Android技能樹 — View事件體系小結

所以區別是:

View的getX/Y()是指自己View的左上角相對於父View左上角的距離。MotionEvent的getX/Y()是指點選處離自己View的左上角的距離。

ps:所以面試官問你getX/Y()的時候,一定問清楚是問的哪個。!不然很容易回答錯誤。

Android技能樹 — View事件體系小結

TouchSlop

Android技能樹 — View事件體系小結

TouchSlop是系統所能識別出來的被認為滑動的最小的距離。如果你手指在螢幕上滑動的時候小於這個值,系統就認為你不是滑動。

VelocityTracker

滑動時候我們可能還要監聽速度,比如說我們的需求就是滑動的快和滑動慢,移動的最終距離不同等。這時候我們一定要知道當前使用者在N時間段內的速度到底是什麼。這時候我們就需要速度(Velocity)追蹤者(Tracker)。

Android技能樹 — View事件體系小結

GestureDetector

Android技能樹 — View事件體系小結

我們先來看看英文翻譯:

Android技能樹 — View事件體系小結

沒錯,既然你在螢幕上操作,你可能是劃來劃去,可能是單擊,可能是雙擊。很多情況。所以這個類就可以幫我們來監聽不同的操作。

ScaleGestureDetector

Android技能樹 — View事件體系小結

在GestureDetector前面新增了一個Scale。

Android技能樹 — View事件體系小結

那就明顯是比例的手勢監測,通俗來說就是放大縮小的手勢監測。

比如我們的需求是在檢視圖片的時候,可以二個手指放大縮小圖片,那我恩就可以用這個ScaleGestureDetector來監測。十分方便。

附上我以前寫過的文章:圖片操作系列 —(1)手勢縮放圖片功能

Android技能樹 — View事件體系小結

View的事件分發機制

Android技能樹 — View事件體系小結

事件傳遞三個階段及事件處理的類

Android技能樹 — View事件體系小結

Android技能樹 — View事件體系小結

其實這二個算是基礎知識。

接下去我會用一個真實的例子帶你們更好的理解事件分發,如果講的不合理,可以提出來哦✧(≖ ◡ ≖✿)

Android技能樹 — View事件體系小結

舉個例子:

PS:(如果例子不適合,大家可以評論反饋。因為如果例子不適合反而誤導了讀者,反而是我的問題了。)


好比你們公司是一個軟體外包公司,現在有個客戶手點了一下滑鼠發給你們老闆一封郵件,說要開發這麼一個APP。你們老闆是不是會一層層的分發下去,老闆 ——> 主管 ——>開發人員。

額外提到點:

  1. 你們老闆收到了通知就是把這個任務分下去,不可能說第一反應先想想說我要不要把這個任務攔下來自己做,不要叫手下的人去做了(不然還請你們幹嘛,請了你們還要每次想著要不要自己做)。所以他沒有攔截功能,預設肯定不會去攔截,肯定第一反應就是直接給手下。

  2. 主管都是有權利把任務攔下來的,不給手下的人去做,可以自己處理,畢竟主管不只是就分配下任務就夠了,這麼簡單我也想去做主管,可能因為手下都有任務在做,忙不過來的時候,主管會自己去做一些開發任務。

  3. 最底層的開發人員,沒有攔截功能,因為任務分到你這裡了。你還能再給誰呢,攔了也是你做,不攔你又沒有下級可以給背鍋,還是你做。

所以對比下知道是不是發現跟我們的Activity,ViewGroup,View很像:

PS:當收到觸控事件傳遞到某個層的時候,這個的dispactchEvent會被呼叫。(相當於上面接受到通知任務的時候會執行這個方法)

老闆 - Activity: 有收到通知的能力,所以會呼叫dispatchTouchEvent(),然後因為他可以去通知主管,所以是

客戶通知老闆你有專案了。老闆的dispatchEvent()會被呼叫。
老闆.dispatchTouchEvent(){
    //老闆先通知主管去處理,
    如果主管給的回覆是:老闆你不用管接下去的事。我們會處理的。
    if(主管.dispatchTouchEvent()){
        return true;//就直接結束了。
    }
    
    //手下的人說這個app開發不了,只能老闆出馬做事(跟客戶去溝通去)
    return 老闆.onTouchEvent();
}
複製程式碼

所以只有dispatchEvent()onTouchEvent()方法。

主管 - ViewGroup

老闆通知了主管有個app要你們部門去開發。主管的dispatchTouchEvent()會被呼叫
主管.dispatchTouchEvent(){
    //主管把這個活攔下來準備自己來開發這個app
    if(主管.interceptTouchEvent()){
        return 主管.onTouchEvent();//主管也有做事能力
    }else{
        //主管不攔截,主管也可以去通知開發人員,
        //如果開發人員回饋說主管你別管了。我們這個app能做好
        if(開發人員.dispatchTouchEvent()){
            return true; //直接就結束了。
        }else{
            //如果手下的開發人員也反饋給主管說搞不定。
            //就只能主管自己出來做事了。
            return 主管.onTouchEvent();
        }
    }
}
複製程式碼

所以有dispatchTouchEvent()、interceptTouchEvent()、onTouchEvent()

開發人員 - View

主管通知了開發人員有個app要開發。開發人員的dispatchTouchEvent()會被呼叫
開發人員.dispatchTouchEvent(){
    return 開發人員.onTouchEvent();
}
複製程式碼

所以有dispatchTouchEvent()、onTouchEvent()

Android技能樹 — View事件體系小結

不同返回值導致不同的流程

我知道大家一定看到過類似下面的這種圖:

Android技能樹 — View事件體系小結

很多人都會死記硬背的去記下來,說return true/false/super等不同情況下不同的呼叫流程。但是這樣其實很不好記住的。很多人會問我是怎麼記住的,我就是用虛擬碼來幫忙記住,什麼事虛擬碼,上面那種表達方式就是虛擬碼。我們現在正是來看具體的虛擬碼。

Activity的真實程式碼:

public boolean dispatchTouchEvent(MotionEvent ev){
    if(ev.getAction == MotionEvent.ACTION_DOWN){
        onUserInteraction();
    }
    
    /**
    呼叫window的superDispatchTouchEvent方法,
    然後再呼叫下面的ViewGroup(DecorView)的dispatchTouchEvent()方法。
    
    我們就直接這麼想,這裡就Activity通知了ViewGroup的dispatchTouchEvent方法。
    
    1.如果這裡getWindow.superDispatchTouchEvent()返回了true,
    這時候就會執行return true語句。
    2.如果這裡getWindow.superDispatchTouchEvent()返回了false,
    這時候就會執行return onTouchEvent(ev);這句,
    
    所以只有當上面的if語句返回false,
    才有機會呼叫Activity自己的onTouchEvent()方法。
    
    */
    if(getWindow.superDispatchTouchEvent()){
        return true;
    }
    
    return onTouchEvent(ev);
}
複製程式碼

所以很多人會所你重寫Activity的dispatchTouchEvent()方法,返回true/false,都直接結束了事件。返回super才能正常分發,這個說法是不合理的。實際應該這麼描述:

預設重寫Activity的dispatchTouchEvent方法:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    /**
        其實是呼叫了super.dispatchTouchEvent方法,
        才會呼叫上面我們貼出的Activity的dispatchTouchEvent方法,
        才能繼續把事件分發下去。
    */
    return super.dispatchTouchEvent(ev);
}
複製程式碼

而大家通俗上說返回true/false就事件結束,是因為沒有呼叫了super.dispatchTouchEvent(ev);。所以就不會分發下去,也就事件結束了。

那假如我這麼寫呢:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    super.dispatchTouchEvent(ev);
    return true/false;
}
複製程式碼

沒錯,事件也是一樣會分發下去。子View的方法也會被呼叫,而不會說直接結束了。

ViewGroup

因為上面我們已經說過了getWindow.superDispatchTouchEvent()可以直接理解為是去呼叫了ViewGroup的dispatchTouchEvent();

ViewGroup的虛擬碼:

public boolean dispatchTouchEvent(MotionEvent ev){
    /**
    如果ViewGroup做了攔截,
    則直接返回了ViewGroup的onTouchEvent()事件的結果。
    */    
    if(onInterceptTouchEvent(ev)){
        return onTouchEvent(ev);
    }else{
        /**
        如果ViewGroup不做攔截,則先分發給child,
        看他們的反應,他們都不接受,則一定會返回false,
        則只能ViewGroup自己去執行自己的onTouchEvent(ev);
        */
        if(child.dispatchTouchEvent(ev)){
            return true;
        }else{
            return onTouchEvent(ev);
        }
    }
}
複製程式碼

View的虛擬碼:

public boolean dispatchTouchEvent(MotionEvent ev){
    /**
    View 就返回自己的onTouchEvent()
    */
    return onTouchEvent();
}
複製程式碼

可能很多人還是說我看了這些程式碼還是不懂啊,我連起來給你看,你就理解了。

Android技能樹 — View事件體系小結

這樣,在不同情況下,返回不同的false/true,執行順序就知道了。

額外補充:

《補充1》:

當然其實還有更復雜的情況,我們知道有ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL等,比如我們直接ViewGroup攔截Down事件,或者Down事件傳遞到了View後,我們在MOVE處再攔截,都會執行不同的:

  1. DOWN事件被傳遞給ViewGroup的onInterceptTouchEvent()後,該方法返回true,表示攔截該事件,說明ViewGroup自己要處理該事件(事件不再往下傳遞);呼叫自身的onTouchEvent()處理事件(DOWN事件將不再往上傳遞給Activity的onTouchEvent());該事件列的其他事件(Move、Up)將直接傳遞給ViewGroup 的onTouchEvent()。
  2. 若 ViewGroup 攔截了一個半路的事件(如MOVE),該事件將會被系統變成一個CANCEL事件 並且 傳遞給之前處理該事件的子View; 該事件不會再傳遞給ViewGroup 的onTouchEvent(); 只有再到來的事件才會傳遞到ViewGroup的onTouchEvent()。

《補充2》:

Android技能樹 — View事件體系小結
我們剛記不記得我們的View的虛擬碼是這樣的:

public boolean dispatchTouchEvent(MotionEvent ev){
    return onTouchEvent();
}
複製程式碼

其實上面是做了簡化,其實除了onTouchEvent,還有onTouch事件和onClick事件,我們繼續用虛擬碼來說明規則:

public boolean dispatchTouchEvent(MotionEvent ev){
    
    if(設定了TouchListener){
        if(onTouch的返回值){
            return true;
        }else{
            return onTouchEvent();
        }
    }
    return onTouchEvent();
}

public boolean onTouchEvent(){
    if(設定了ClickListener){
        執行onClick;
    }
    
    .......
}


複製程式碼

Android技能樹 — View事件體系小結

View的滑動

既然我們學會了View的事件體系,很多人說那我學會了能怎麼樣,最明顯的就是我們可以用來解決很多滑動衝突事件。因為我們可以根據實際需求,選擇性的攔截,然後做自己的事件處理。

所以我們具體來看View的滑動有關的知識:

Android技能樹 — View事件體系小結

View的滑動的基本知識我就不特意提出來了。大家可以分別去搜尋。

主要是第二塊View的滑動衝突。我們就以最簡單的外部左右滑動,內部上下滑動為例子。

Android技能樹 — View事件體系小結
外部左右滑動,內部上下滑動

比如我們規定,滑動的角度是N度以內的時候就是說明我們在內部滑動,角度是N度以外的時候是外部滑動。

  1. 外部攔截法
    預設父元素攔截,然後再適合的條件下,不讓父元素攔截。
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;
            //必須不能攔截,否則後續的ACTION_MOME和ACTION_UP事件都會攔截。
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器需要當前點選事件){
                intercepted=true;
            }else {
                intercepted=false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted=false;
            break;
        default:
        break;
    }
    mLastXIntercept=x;
    mLastXIntercept=y;
    return intercepted;
}
複製程式碼
  1. 內部攔截法:
    預設剛開始是不允許父元素做攔截,也就是子元素剛開始就呼叫requestDisallowInterceptTouchEvent(true);方法,禁止父元素做攔截,然後再適合的條件再讓父元素攔截。
子元素的dispatchTouchEvent()重寫:

@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 - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            
            if (父容器需要當前點選事件) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:{
            break;
        }
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return super.dispatchTouchEvent(ev);
}
複製程式碼

同時還要修改父容器的onInterceptTouchEvent()方法,不能做攔截,因為如果剛開始DOWN就攔截了,後面的MOVE,UP都沒機會到子元素的上面的程式碼。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}
複製程式碼

結語

Android技能樹 — View事件體系小結

歡迎大家檢視糾正,?。。。。讓吐槽來的更猛烈些吧。

相關文章