Android事件傳遞機制之View

Android Touch事件

在Android中,事件主要包括點按、長按、拖拽、滑動等,點按又包括單擊和雙擊,另外還包括單指操作和多指操作。所有這些都構成了Android中得事件響應。總的來說,所有的事件都由如下三個部分作為基礎:

  • 按下(ACTION_DOWN)
  • 移動(ACTION_MOVE)
  • 抬起(ACTION_UP)

所有的事件首先必須執行ACTION_DOWN操作,之后可以觸發ACTION_MOVE接著ACTION_UP,或者直接ACTION_UP。
應用通過視圖組件和用戶交互,所以所有的事件都是通過視圖組件觸發的。在Android中所有的視圖都繼承于View,對視圖組件進行布局的布局組件(ViewGroup)也繼承于View。所以事件的觸發和傳遞主要在View和ViewGroup之間。

事件的傳遞

View.java

public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)

ViewGroup.java

public boolean dispatchTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event)
public boolean onInterceptTouchEvent(MotionEvent ev)

Android事件的傳遞主要用到了以上的方法。事件傳遞的方法有兩個共同特點,它們的方法返回值都是boolean,參數都是MotionEvent。在Android中,所有的事件都是從開始經過傳遞到完成事件的消費,這些方法的返回值就決定了某一事件是否是繼續往下傳,還是被攔截了,或是被消費了。(true代表事件被消費了,false代表事件繼續往下傳遞)

MotionEvent繼承于InputEvent,用于標記各種動作事件。ACTION_DOWN、ACTION_MOVE、ACTION_UP都是MotinEvent中定義的常量。我們通過MotionEvent傳進來的事件類型來判斷接收的是哪一種類型的事件。

View事件傳遞方法的調用

dispatchTouchEvent

觸摸任何一個控件時,首先都會調用這個方法。dispatchTouchEvent方法代碼如下

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}  

在這個方法內,首先判斷if語句里面的條件:mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event),如果這個條件為true那么直接返回true,否則繼續執行onTouchEvent()。
分析下if語句里的條件:

  • mTouchListener變量是否被賦值,該變量是通過以下方法賦值的
public void setOnTouchListener(OnTouchListener l) {  
    mOnTouchListener = l;  
}  

也就是說我們給控件注冊了觸摸事件監聽器,就是給mTouchListenter賦值。

  • (mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點擊的控件是否是enable的,按鈕默認都是enable的,因此這個條件恒定為true。
  • mOnTouchListener.onTouch(this, event),其實也就是去回調控件注冊touch事件監聽器時的onTouch方法。也就是說如果我們在onTouch方法里返回true,就會讓這三個條件全部成立,從而整個方法直接返回true。如果我們在onTouch方法里返回false,就會再去執行onTouchEvent(event)方法。

綜上所述,在事件分發時首先調用dispatchTouchEvent(),執行onTouch()方法。如果onTouch()返回true后,不會再去調用onTouchEvent()方法。代表事件已經被onTouch()消費掉了。

onTouchEvent

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  
    if ((viewFlags & ENABLED_MASK) == DISABLED) {  
        // A disabled view that is clickable still consumes the touch  
        // events, it just doesn't respond to them.  
        return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    }  
    if (mTouchDelegate != null) {  
        if (mTouchDelegate.onTouchEvent(event)) {  
            return true;  
        }  
    }  
    //判斷控件是否可以點擊
    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
        switch (event.getAction()) {  
            case MotionEvent.ACTION_UP:  
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                    // take focus if we don't have it already and we should in  
                    // touch mode.  
                    boolean focusTaken = false;  
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                        focusTaken = requestFocus();  
                    }  
                    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)) { 
                                 //調用該方法,去處理點擊事件 
                                performClick();  
                            }  
                        }  
                    }  
                    if (mUnsetPressedState == null) {  
                        mUnsetPressedState = new UnsetPressedState();  
                    }  
                    if (prepressed) {  
                        mPrivateFlags |= PRESSED;  
                        refreshDrawableState();  
                        postDelayed(mUnsetPressedState,  
                                ViewConfiguration.getPressedStateDuration());  
                    } else if (!post(mUnsetPressedState)) {  
                        // If the post failed, unpress right now  
                        mUnsetPressedState.run();  
                    }  
                    removeTapCallback();  
                }  
                break;  
            case MotionEvent.ACTION_DOWN:  
                if (mPendingCheckForTap == null) {  
                    mPendingCheckForTap = new CheckForTap();  
                }  
                mPrivateFlags |= PREPRESSED;  
                mHasPerformedLongPress = false;  
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                break;  
            case MotionEvent.ACTION_CANCEL:  
                mPrivateFlags &= ~PRESSED;  
                refreshDrawableState();  
                removeTapCallback();  
                break;  
            case MotionEvent.ACTION_MOVE:  
                final int x = (int) event.getX();  
                final int y = (int) event.getY();  
                // Be lenient about moving outside of buttons  
                int slop = mTouchSlop;  
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                        (y < 0 - slop) || (y >= getHeight() + slop)) {  
                    // Outside button  
                    removeTapCallback();  
                    if ((mPrivateFlags & PRESSED) != 0) {  
                        // Remove any future long press/tap checks  
                        removeLongPressCallback();  
                        // Need to switch from pressed to not pressed  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                    }  
                }  
                break;  
        }  
        return true;  
    }  
    return false;  
}  

performClick()方法來觸發點擊事件

public boolean performClick() {  
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
    if (mOnClickListener != null) {  
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}  

由上面代碼可以看到只要點擊事件的監聽器不為null,那么就去回調onClick()方法。同上面mTouchListener類似,通過以下方法注冊監聽器:

public void setOnClickListener(OnClickListener l) {  
    if (!isClickable()) {  
        setClickable(true);  
    }  
    mOnClickListener = l;  
}  

這里其實就是給控件注冊點擊事件監聽器。
綜上所述,事件從ACTION_DOWN開始,調用dispatchTouchEvent方法。當ACTION_UP觸發,并且onTouch()方法返回false,那么就回去執行onTouchEvent方法。最后會回調onClick,執行點擊事件響應的處理。

實踐

Button事件傳遞:touch事件傳遞至觸發點擊事件

public class ButtonActivity extends AppCompatActivity{
    private static final String TAG = "ButtonActivity";
    private Button mButton;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_button);
        mButton = (Button) findViewById(R.id.button);
        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch(event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.d(TAG, "button ontouch execute, action " + event.getAction());
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG, "button ontouch execute, action " + event.getAction());
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d(TAG, "button ontouch execute, action " + event.getAction());
                        break;
                }
                return false;
            }
        });
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "button onClick execute");
            }
        });
    }
}

log打印如下:

Button事件傳遞.png

onTouch方法返回true,查看是否觸發點擊事件(調用onTouchEvent方法):

public class ButtonActivity extends AppCompatActivity{
    private static final String TAG = "ButtonActivity";
    private Button mButton;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_button);
        mButton = (Button) findViewById(R.id.button);
        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch(event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.d(TAG, "button ontouch execute, action " + event.getAction());
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG, "button ontouch execute, action " + event.getAction());
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d(TAG, "button ontouch execute, action " + event.getAction());
                        break;
                }
                return true;
            }
        });
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "button onClick execute");
            }
        });
    }
}

log日志打印如下:

Button事件傳遞-攔截點擊事件.png

這里有個重要的知識點,touch事件的層級傳遞。我們點擊控件時,都會觸發一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。這里需要注意,如果你在執行ACTION_DOWN的時候返回了false,后面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,才會觸發后一個action。原理分析:
首先在onTouch事件里返回了false,就一定會進入到onTouchEvent方法中,然后我們來看一下onTouchEvent方法的細節。由于我們點擊了按鈕,就會進入到事件類型判斷switch中,然后你會發現,不管當前的action是什么,最終都一定會返回一個true。那么我可以這樣理解,touch事件都是從ACTION_DOWN開始的,如果ACTION_DOWN事件在dispatchTouchEvent()后返回false,那么就代表這個事件沒有消費掉。然而view(如Button)沒有子view,所以這個事件相當于沒有處理,touch事件也就無法傳遞下去,也就沒有ACTION_UP和ACTION_MOVE事件觸發。

ImageView事件傳遞:

public class ImageViewActivity extends AppCompatActivity{
    private static final String TAG = "ImageViewActivity--->";
    private ImageView mImageView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_image_view);
        mImageView = (ImageView) findViewById(R.id.img);
        mImageView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch(event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.d(TAG, "imageView onTouch execute, action " + event.getAction());
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.d(TAG, "imageView onTouch execute, action " + event.getAction());
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.d(TAG, "imageView onTouch execute, action " + event.getAction());
                        break;
                }
                return false; //這里我們可以直接返回true,實現觸發三種action.或者在ImageView屬性中設置可點擊
            }
        });
    }
}

log日志打印如下:

ImageView事件傳遞.png

在ACTION_DOWN執行完后,后面的一系列action都不會得到執行了。這是因為ImageView和按鈕不同,它是默認不可點擊的,因此在onTouchEvent的可否點擊判斷時無法進入到事件類型判斷,直接跳到最后返回了false,也就導致后面其它的action都無法執行了。

參考

文獻1文獻2文獻3

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容