1. 程式人生 > >結合原始碼,重溫 Android View 的事件處理知多少 ?

結合原始碼,重溫 Android View 的事件處理知多少 ?

前言

  • Android View 的 事件處理在我們的程式設計中,可謂是無處不在了。但對於大多數人而言,一直都是簡單的使用,對其原理缺乏深入地認識。
  • 學 Android 有一段時間了,最近發現,很多基礎知識開始有些遺忘了,所以從新複習了 View 的事件分發。特地整理成了這篇文章分享給大家。
  • 本文不難,可以作為大家茶餘飯後的休閒。

祝大家閱讀愉快!

方便大家學習,我在 GitHub 上建立個 倉庫


  • 倉庫內容與部落格同步更新。由於我在 稀土掘金 簡書 CSDN 部落格園 等站點,都有新內容釋出。所以大家可以直接關注該倉庫,即使獲得精彩內容

  • 倉庫地址:
    超級乾貨!精心歸納 Android

    JVM 、演算法等,各位帥氣的老鐵支援一下!給個 Star !

一、View 的事件回撥

  • 我們結合原始碼看看 View 的事件分發是個怎樣的過程,首先我們建立一個類 MyButton 類繼承 AppCompatButton 用於測試:
public class MyButton extends AppCompatButton {

    private final String TAG = "DeBugMyButton";
        public MyButton(Context context) {
        super(context);
    }

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

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

}

1.1 事件分發流程

  • 我們都知道有一個方法叫做 public boolean dispatchTouchEvent(MotionEvent event) 。首先我們要知道,對於我們這個自定義控制元件,他的觸控事件都是從我們 dispatchTouchEvent 這個方法開始往下去分發的。所以可以說:這個方法是一個入口方法。

1.1.1 onTouchEvent 作用

  • 現在我們重寫該方法和另一個方法:onTouchEvent ,並且列印一行日誌:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    Log.d(TAG, "----on dispatch Touch Event----");
    return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.d(TAG, "----on touch event----");
    }
    return super.onTouchEvent(event);
}
  • 然後我們在 MainActivity 中,設定一個例項化一個 MyButton 控制元件物件用於測試,並且給他新增一個 onClickListentersetOnTouchListener
public class MainActivity extends AppCompatActivity {

    private final String TAG = "DeBugMainActivity";

    /**
     * 自定義控制元件 MyButton
     */
    private MyButton mMyButton;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        iniView();
    }

    /**
     * 例項化控制元件
     */
    private void iniView() {
        mMyButton = findViewById(R.id.my_button);

    mMyButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.d(TAG, "----on touch----");
                    break;
                default:
                    break;
            }
            return false;
        }
    });
    
    mMyButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d(TAG, "----on click----");
        }
    });
    }
}
  • 然後我們執行這個 Demo ,點選 MyButton 按鈕,會的到如下日誌:

  • 我們可以看到首先回調了這個 dispatchTouchEvent ,然後是它的監聽器 OnTouch ,接著是它的 onTouchEvent,最後又執行了 dispatchTouchEvent ,那麼這是為什麼呢?

  • 這是因為我們這兒只監聽了 ACTION_DOWN 而當手指抬起時它同樣還回去回撥 dispatchTouchEvent ,最後我們列印 OnClick 的回撥。

  • 總結一下就是:
    dispatchTouchEvent -> setOnTouchListener -> onTouchEvent -> setOnClickListener

  • 說明我們 setOnClickListener 是通過 onTouchEvent 處理,產生了 OnClick 。一會我們再來看看其中的原理。

  • 既然說 dispatchTouchEvent 像一個入口,就先讓我們來看下它是怎麼處理和操作的: 首先,既然我們呼叫了 super.dispatchTouchEvent(event) ,那麼我們就來看看它父類中是怎麼實現該方法的。不信的是,它的父類 AppCompatButton 也沒有實現該方法 ,最後經過層層搜尋,我們發現這個方法是屬於 View 的方法。

1.1.2 dispatchTouchEvent 的實現

  • 那麼現在我們來看看 ViewdispatchTouchEvent 怎麼實現的:
public boolean dispatchTouchEvent(MotionEvent event) {
    ......
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}
  • dispatchTouchEvent 中,我們可以發現下面這樣一個程式碼塊
if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}
  • 不難看出:如果執行了這個程式碼段,那麼後面的方法就不會執行了,並且 dispatchTouchEvent 會返回 true 。我們再仔細觀察下其中的條件:在 if 條件中我們發現:只有當其滿足 li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) 時才會執行 if 內的操作

  • 經過上面分析,我們可以知道: onTouch 事件必須返回 true 時,才會執行該方法塊。那麼我們就回到 MainActivity 中。我們發現 setOnTouchListeneronTouch 預設返回值是 false( 不滿足返回值為 true ), 這就表明他會繼續去執行下一個程式碼塊:

if (!result && onTouchEvent(event)) {
    result = true;
}
  • 執行這個 if 語句的過程中。首先呼叫了 onTouchEvent 方法。這就解釋了,為什麼它先執行了 mOnTouchListener ,然後再執行 onTouchEvent

  • 現在我們就可以總結一下:首先我們回調了 dispatchTouchEvent ,然後回撥 OnTouchListener 。這個時候,如果 TouchListener 沒有 return true ,那麼就會接著去執行 onTouchEvent ( 當然,如果 return true 後面的層級就不會執行了 。一句話說就是:到那個層級 return true 那麼哪個層級就消費掉了這個事件 )。

1.1.3 onTouchEvent 的處理

  • 同時我們還有一個結果:我們 onClick ( 包括我們的 onLongClick ) 是來自於我們 onTouchEvent 這個方法的處理。那麼下面我們就來看看 View 中是怎麼處理 onTouchEvent 的:
public boolean onTouchEvent(MotionEvent event) {
    。。。

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                。。。
                break;

            case MotionEvent.ACTION_DOWN:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                。。。
                break;

            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    // Remove any future long press/tap checks
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                break;
        }

        return true;
    }

    return false;
}

二、onClick 和 OnLongClick

  • 因為我們是拿 ACTION_DOWN 作為舉例的。那麼我們先來分析一下 case MotionEvent.ACTION_DOWN : 中 onTouchEvent 是怎麼執行的,以及 onClickOnLongClick 是如何產生的:

2.1 onClick 和 OnLongClick 的產生

  • 首先,當我們手指按下時,有一個 mHasPerformedLongPress 標識會先被設為 false 。再往下會執行一行 postDelayed(mPendingCheckForTapViewConfiguration.getTapTimeout()); 我們來看看這一行的作用:

  • 首先,從名字我們就可以猜測,這是個延時執行的方法。我們進一步閱讀發現 mPendingCheckForTap 是一個 Runnable 動作; ViewConfiguration.getTapTimeout() 是一個 100mm 的延時。也就是說延時 100mm 後去執行 mPendingCheckForTap 中的動作。那麼我們就來看看 mPendingCheckForTap 中做了什麼:

private final class CheckForTap implements Runnable {
    public float x;
    public float y;

    @Override
    public void run() {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        setPressed(true, x, y);
        checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
    }
}
  • 也就是說,停一百秒後就開始檢查,使用者的手指是否離開了螢幕。( 就是當前 ACTION_DOWN 之後,有沒有觸發了 ACTION_UP 這個環節 ),但是 ACTION_DOWN 後,我們還有一個 ACTION_MOVE 過程。在這個 ACTION_MOVE 中,如果 100mm 內離開了螢幕、或者離開了這個控制元件就會觸發 ACTION_UP ,那麼就認為這是一個點選事件 onClick 。如果沒有觸發 ACTION_UP 的話,就會再延時 400mm

2.2 ACTION_DOWN 之後流程

  • ACTION_DOWN 之後,會先等 100mm
  • 如果沒有離開螢幕或者離開控制元件,就是沒有觸發 ACTION_UP 的話,就會再延時 400mm。
  • 500mm 後就會觸發 onLongClick 事件。

2.3 那麼我們現在來驗證一下 onLongClick :

  • 首先再 MainActivity 中加上:
mMyButton.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {

        return true;
    }
});
  • 接著,我們發現 OnLongClick 是有返回值的,如果返回值是 false 還會接著去觸發 onClick 事件,如果返回 true 的話,那麼這個長按事件就直接被消費掉了( 也就是這個點選事件就不會完後傳遞到 OnClickListener 中去了 )。

2.4 總結

  • 100mm 時為點選,500mm 時為長按,接著觸發長按事件。
  • 再看長按事件的返回值,如果時 true 就結束。
  • 如果時 false 那麼 OnClickListener 就同樣也被執行。
  • 這就是由 obTouchEvent 產生出來的 onClick/onLongClick 的來龍去脈。

總結

  • 我們 View 的事件方法,基本上就是這麼一個思路,從 dispatchTouchEventOnTouchListener 監聽器,再到 onTouchEvent,接著 onTouchEvent 由產生了 onClick/onLongClick
  • 如果大家感興趣的話可以更深入的去閱讀原始碼。
  • 重點:學 Android 有一段時間了,我打算好好的梳理一下所學知識,包括 ActivityServiceBroadcastRecevier 事件分發、滑動衝突、新能優化等所有重要模組,歡迎大家關注 _yuanhao 的 部落格園 ,方便及時接收更新
  • 如果有可以補充的知識點,歡迎大家在評論區指出。

碼字不易,你的點贊是我總結的最大動力!


  • 由於我在「稀土掘金」「簡書」「CSDN」「部落格園」等站點,都有新內容釋出。所以大家可以直接關注我的 GitHub 倉庫,以免錯過精彩內容!

  • 倉庫地址:
    超級乾貨!精心歸納 AndroidJVM 、演算法等,各位老鐵支援一下!給個 Star !

  • 一萬多字長文,加上精美思維導圖,記得點贊哦,歡迎關注 _yuanhao 的 部落格園 ,我們下篇文章見!

相關推薦

結合原始碼重溫 Android View事件處理多少 ?

前言 Android View 的 事件處理在我們的程式設計中,可謂是無處不在了。但對於大多數人而言,一直都是簡單的使用,對其原理缺乏深入地認識。 學 Android 有一段時間了,最近發現,很多基礎知識開始有些遺忘了,所以從新複習了 View 的事件分發。特地整理成了這篇文章分享給大家。 本文不難,可以作

Android-View事件處理機制

             又遇到過監聽事件無效的情況,然後找了一些資料。在這裡做點筆記方便以後忘了複習! 一,先看一段程式碼:我自定義一個MyButton繼承Button,並重寫了dispatchT

Activity啟動流程介面繪製到事件處理的整個流程(基於Android6.0原始碼)(2)

void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) { synchronized (this) { ...... mWindowAttributes

Android View 事件分發機制原始碼詳解(ViewGroup篇)

前言 我們在學習View的時候,不可避免會遇到事件的分發,而往往遇到的很多滑動衝突的問題都是由於處理事件分發時不恰當所造成的。因此,深入瞭解View事件分發機制的原理,對於我們來說是很有必要的。由於View事件分發機制是一個比較複雜的機制,因此筆者將寫成兩篇文

Android View 事件分發機制 原始碼解析(ViewGroup篇)

1. 前言 android點選 事件一直以來都是很多安卓程式設計師的心病,之前通過demo模擬總結出一些經驗,但是不看原始碼的程式設計師不是好程式設計師,這段時間,系統的梳理了下整個事件傳遞的原始碼,希望可以幫助大家徹底理解andriod的點選

Android按鍵事件處理流程 -- 從事件被派發到View層次結構的根節點DecorView開始分析

http://www.2cto.com/kf/201406/311432.html http://www.cxyclub.cn/n/48237/         剛接觸Android開發的時候,對touch、key事件的處理總是一知半解,一會是Activity裡的方法,一會

談談我對Android View事件分發的理解

event 調用 ack 處理 group ans import ras 運行 寫這篇博客的緣由。近期因為項目中用到相似一個LinearLayout中水平布局中,有一個TextView和Button,然後對該LinearLayout布局設置點擊事件。點擊

Android View事件分發機制

作為程式猿,最不想 看的但是也不得不去看的就是原始碼!所謂知其然也要知其所以然,神祕的大佬曾經說過進階的方法就是READ THE FUCKING CODE! 認識MotionEvent 負責集中處理所有型別裝置的輸入事件.我們對螢幕的點選,滑動,擡起等一系的

Android View事件傳遞機制

view事件傳遞機制,在很多面試中會問道,我曾經也被問道,卻沒有回答上來。 今天我在這裡寫了一個demo去理解這個view的事件傳遞機制。 首先這個view包括兩種,viewGroup和普通view。viewGroup就是裡面還可以包含子控制元件的那種,如Linear

Android View事件體系總結一

什麼是View? View是Android中所有控制元件的基類,是一種介面層的控制元件的一種抽象,它代表了一個控制元件。 View的位置引數 View的位置主要由它的四個頂點決定,分別對應View的四個屬性:top,left,right,bottom。其中top是左上角

Fragment中RecyclerView的使用解析以及監聽事件處理

RecyclerView是可以代替listview使用的新元件,個人感覺其主要特色:其介面卡adapter中,重寫的東西少了,頁面展示的效果跟加多了,比如可以在RecyclerView設定listview的顯示效果,也可以設定gridview的顯示效果,也可以設定瀑布流的顯示效果!下面程式碼主要

Android UI事件處理——實現事件監聽介面的4種方法

前段時間看到一個同學的android課程有這樣一個作業要求:....... 非內部類實現onClickListener監聽介面的方式監聽按鈕單擊事件 ....... 感覺蠻奇怪,一般對於UI事件的處理

【js事件詳解】js事件封裝函式js跨瀏覽器事件處理機制

一、事件流 事件流描述的是從頁面中接受事件的順序。 IE的事件流是事件冒泡流,而Netscape的事件流是事件捕獲流 1、事件冒泡 事件冒泡,即事件最開始由最具體的元素(文件中巢狀層次最深的那個節點)接收,然後逐級向上轉播至最不具體的節點(文件)。 2、事件捕獲 事件捕獲的

android view事件OnTouch重複觸發

程式碼: img.setOnTouchListener(         object : View.OnTouchListener{        &nb

Android按鍵事件處理流程

 剛接觸Android開發的時候,對touch、key事件的處理總是一知半解,一會是Activity裡的方法,一會是各種View 中的,自己始終不清楚到底哪個在先哪個在後,總之對整個處理流程沒能很好的把握。每次寫這部分程式碼的時候都有些心虛, 因為我不是很清楚什麼時候、

一文讀懂Android View事件分發機制

Android View 雖然不是四大元件,但其並不比四大元件的地位低。而View的核心知識點事件分發機制則是不少剛入門同學的攔路虎。ScrollView巢狀RecyclerView(或者ListView)的滑動衝突這種老大難的問題的理論基礎就是事件分發機制。 事件

jQuery中的屬性操作jQuery中的事件處理、jQuery 中的動畫簡單介紹

jQuery中的屬性操作,jQuery中的事件處理、jQuery 中的動畫簡單介紹 getAttribute(‘name’) setAttribute(‘name’, ‘Tom’) attr(): 獲取屬性和設定屬性 當為該方法傳遞一個引數時, 即為某元素的獲取指定屬性 當為該方法傳遞兩個引數時, 即為

Android View事件(四)】View滑動與實現滑動的幾種方法

1 前言 在前面的幾篇文章,我向大家介紹的都是單一View事件,而在這篇文章中,我將向大家介紹連續的事件 —— 滑動。 在安卓裝置上滑動幾乎是應用的標配,由於安卓手機螢幕較小,為了給使用者呈現更多的內容,就需要使用滑動來隱藏和顯示一些內容。

Android-view事件傳遞機制

Android中dispatchTouchEvent,onInterceptTouchEvent, onTouchEvent的理解 Android中的事件型別分為按鍵事件和螢幕觸控事件,Touch事件是螢幕觸控事件的基礎事件,有必要對它進行深入的瞭解。

android 開發 View _14 MotionEvent和事件處理詳解與實踐自定義滑動條View

MotionEvent MotionEvent物件是與使用者觸控相關的時間序列,該序列從使用者首次觸控式螢幕幕開始,經歷手指在螢幕表面的任何移動,直到手指離開螢幕時結束。手指的初次觸控(ACTION_DOWN操作),滑動(ACTION_MOVE操作)和擡起(ACTION