【Android 系統開發】_「核心技術」篇 -- 事件分發機制

1. 核心源碼

關鍵類 路徑
Activity.java frameworks/base/core/java/android/app/Activity.java
DecorView.java frameworks/base/core/java/com/android/internal/policy/DecorView.java
PhoneWindow.java frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
View.java frameworks/base/core/java/android/view/View.java
ViewGroup.java frameworks/base/core/core/java/android/view/ViewGroup.java
Window.java frameworks/base/core/java/android/view/Window.java

2. 基礎認知

Android 「View」 雖然不是四大組件,但其並不比四大組件的地位低。而 View 的核心知識點 「事件分發機制」 則是很多剛入門童鞋的攔路虎(一、項目中到處遇到事件分發機制;二、面試管最喜歡說起的問題)。java

在實際項目的開發過程當中,ScrollView 嵌套 RecyclerView(或者 ListView)的滑動衝突這種老大難問題的理論基礎就是「事件分發機制」。android

首先,咱們瞭解一下「事件分發機制」中的基礎知識點!面試

2.1 事件分發的對象

事件分發的對象:點擊事件(Touch事件)app

1. 定義ide

當用戶觸摸屏幕時(View 或 ViewGroup 派生的控件),將產生點擊事件(Touch事件)函數

Touch 事件的相關細節(發生觸摸的位置、時間等)被封裝成 MotionEvent 對象。

2. 事件類型佈局

事件類型 具體動做
MotionEvent.ACTION_DOWN 按下 View(全部事件的開始)
MotionEvent.ACTION_UP 擡起 View(與DOWN對應)
MotionEvent.ACTION_MOVE 滑動 View
MotionEvent.ACTION_CANCEL 結束事件(非人爲緣由)

3. 事件序列post

所謂事件序列就是指:從手指接觸屏幕至手指離開屏幕,這個過程產生的一系列事件。測試

通常狀況下,事件列都是以 DOWN 事件開始、UP 事件結束,中間有無數的 MOVE 事件。ui

以下圖:

事件列.png

即當一個點擊事件(MotionEvent )產生後,系統需把這個事件傳遞給一個具體的 View 去處理。

2.2 事件分發的本質

本質: 將點擊事件(MotionEvent)傳遞到某個具體的 View 並處理的整個過程。

即:事件傳遞的過程 = 分發過程

2.3 事件分發的對象

對象: Activity、ViewGroup、View

Android 的 UI 界面由 Activity、ViewGroup、View 及其派生類組成。

以下圖:

UI 界面.png

2.4 事件分發的順序

順序: Activity -> ViewGroup -> View

即:1個點擊事件發生後,事件先傳到 Activity、再傳到 ViewGroup、最終再傳到 View。

事件分發的順序流程圖以下:

傳遞.png

2.5 事件分發的方法

三個重要方法: dispatchTouchEvent() 、onInterceptTouchEvent() 和 onTouchEvent()。

dispatchTouchEvent(事件分發):當監聽到有觸發事件時,首先由 Activity 進行捕獲,而後事件就進入事件分發的流程。Activity 自己沒有事件攔截,從而將事件傳遞給最外層的 View 的 dispatchTouchEvent(MotionEvent ev),該方法將對事件進行分發。

返回值:表示是否消費了當前事件。多是 View 自己的 onTouchEvent() 消費,也多是子 View 的 dispatchTouchEvent() 中消費。返回 true 表示事件被消費,本次的事件終止。返回 false 表示 View 以及子 View 均沒有消費事件,將調用父 View 的 onTouchEvent()。

onInterceptTouchEvent(事件攔截):當一個 ViewGroup 在接到 MotionEvent 事件序列時候,首先會調用此方法判斷是否須要攔截。特別注意,這是 ViewGroup 特有的方法,View 並無攔截方法。

返回值:是否攔截事件傳遞,返回 true 表示攔截了事件,那麼事件將再也不向下分發而是調用 View 自己的 onTouchEvent()。返回 false 表示不作攔截,事件將向下分發到子 View 的 dispatchTouchEvent()。

onTouchEvent(事件響應):真正對 MotionEvent 進行處理或者說消費的方法,在 dispatchTouchEvent() 中進行調用。

返回值:返回 true 表示事件被消費,本次的事件終止。返回 false 表示事件沒有被消費,將調用父 View 的 onTouchEvent 方法,直到返回 true 爲止。

事件分發的方法.png

咱們能夠作個總結:

Activity 的點擊事件事實上是調用它內部的 ViewGroup 的點擊事件,能夠直接當成 ViewGroup 處理。

ViewGroup 的相關事件方法有三個:onInterceptTouchEventdispatchTouchEventonTouchEvent

View 的相關事件方法只有兩個:dispatchTouchEventonTouchEvent


要想充分理解 Android 事件分發機制,本質上是要理解如下三個部分:

    ✯ Activity - Touch 事件分發     ✯ ViewGroup - Touch 事件分發     ✯ View - Touch 事件分發

3. Touch 事件分發 -- Activity

當一個點擊事件發生時,事件最早傳到 Activity 的 dispatchTouchEvent() ,進行事件分發。

3.1 dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        // 通常事件列開始都是 DOWN 事件,即按下事件,因此此處基本是 true
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();                          // 1
        }
        if (getWindow().superDispatchTouchEvent(ev)) {    // 2
            return true;
        }        
        return onTouchEvent(ev);                          // 3
    }

Activity 的 dispatchTouchEvent 方法很簡單,主要涉及三個方法:onUserInteractionsuperDispatchTouchEventonTouchEvent

3.1.1 onUserInteraction

/**
    * 說明:
    *    a. 該方法爲空方法,主要實現屏保功能
    *    b. 當此 activity 在棧頂時,觸屏點擊按 home,back,menu 鍵等都會觸發此方法
    */
    public void onUserInteraction() {
    }

3.1.2 superDispatchTouchEvent

/**
    * getWindow():獲取 Window 類的對象,Window 類是抽象類,
    * 其惟一實現類是 PhoneWindow 類;即此處的 Window 類對象 = PhoneWindow 類對象。
    */
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }

Window 類的 superDispatchTouchEvent() 是一個抽象方法,由子類 PhoneWindow 類實現。

3.1.2.1 PhoneWindow

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ... ...
    
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    ... ...
}

咱們發現,Activity 的 superDispatchTouchEvent 方法最終會走到 DecorView 的 superDispatchTouchEvent 方法。

3.1.2.2 DecorView

/**
    * public class DecorView extends FrameLayout,
    *       -- DecorView 繼承自 FrameLayout,是全部界面的父類。         
    * public class FrameLayout extends ViewGroup,
            -- FrameLayout 是 ViewGroup 的子類,故 DecorView 的間接父類是 ViewGroup。
    */
  
    public boolean superDispatchTouchEvent(MotionEvent event) {
        // 調用父類的方法:ViewGroup 的 dispatchTouchEvent(),
        // 即:將事件傳遞到 ViewGroup 去處理,咱們在 ViewGroup 的事件分發機制繼續討論。
        return super.dispatchTouchEvent(event);
    }

因此不難發現,Activity 的 dispatchTouchEvent 方法最終會走到 ViewGroup 的 dispatchTouchEvent 方法。

3.1.3 onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
        // 通常返回 true,除非 Touch 事件在 Window 邊界外,因此這邊咱們再也不繼續跟蹤。
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

3.2 小結

Activity 事件分發處理流程.png

至此,咱們分析了 Activity 對點擊事件的分發機制處理流程,咱們不難發現,Activity 的事件走到了 ViewGroup 進行處理,那麼接下來就是分析 ViewGroup 對點擊事件的分發機制了。

4. Touch 事件分發 -- ViewGroup

上面咱們說過,Activity 的 dispatchTouchEvent 方法最終會走到 ViewGroup 的 dispatchTouchEvent 方法。

4.1 dispatchTouchEvent

ViewGroup 對點擊事件的分發流程就複雜不少了,咱們來細細研究:

// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;

public boolean dispatchTouchEvent(MotionEvent ev) {

    ... ...
    
    // 這個變量用於記錄事件是否被處理完
    boolean handled = false;
        
    // 過濾掉一些不合法的事件
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
            
        // 判斷是否是 Down 事件,若是是的話,就要作初始化操做
        if (actionMasked == MotionEvent.ACTION_DOWN) {
           // 若是是 Down 事件,就要清空掉以前的狀態,
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 是否攔截事件
        final boolean intercepted;

        // 若是當前是 Down 事件,或者已經有處理 Touch 事件的目標了
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            /**
              * disallowIntercept:是否禁用事件攔截的功能,默認爲 false,
              * 咱們也能夠經過調用 requestDisallowInterceptTouchEvent 方法
              * 對這個值進行修改。
              */
            final boolean disallowIntercept = 
                                     (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                          
            // disallowIntercept 默認爲 false 因此會走如下流程
            if (!disallowIntercept) {
                // 每次事件分發時,都需調用 onInterceptTouchEvent() 詢問是否攔截事件
                intercepted = onInterceptTouchEvent(ev);
                // 從新恢復 Action,以避免 Action 在上面的步驟被人爲地改變了
                ev.setAction(action); // restore action in case it was changed
            } else {
                // 若是禁用了事件攔截功能,則 intercepted 確定爲 false
                intercepted = false;
            }
        } else {
            // 若是說,事件已經初始化過了,而且沒有子 View 被分配處理,
            // 那麼就說明,這個 ViewGroup 已經攔截了這個事件。
            intercepted = true;
        }

        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // Check for cancelation,標誌着取消事件.
        final boolean canceled = resetCancelNextUpFlag(this)
                   || actionMasked == MotionEvent.ACTION_CANCEL;

        // 若是須要(不取消,也沒有被攔截),那麼在觸摸 Down 事件的時候更新觸摸目標列表
        // split:表明當前的 ViewGroup 是否是支持分割 MotionEvent 到不一樣的 View 當中
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

        // 新的觸摸對象
        TouchTarget newTouchTarget = null;
            
        //是否把事件分配給了新的觸摸
        boolean alreadyDispatchedToNewTouchTarget = false;
            
        ★ ★ ★ ★ ★ ★ ★ ★ 重點方法 ★ ★ ★ ★ ★ ★ ★ ★ 
        // 若是事件不是取消事件,也沒有攔截,那麼進入此函數
        if (!canceled && !intercepted) {      

            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            /*
             * 若是是個全新的 Down 事件,或者是有新的觸摸點,或者是光標來回移動事件
             */
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                // 事件的索引,Down 事件的 index:0
                final int actionIndex = ev.getActionIndex(); // always 0 for down

                // 獲取分配的 ID 的 bit 數量
                final int idBitsToAssign = split ? 1 << 
                       ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;

                // 清理以前觸摸這個指針標識,以防它們的目標變得不一樣步
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                    
                // 若是新的觸摸對象爲 null & 當前 ViewGroup 有子元素
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                        
                    final ArrayList<View> preorderedList 
                                              = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                        
                    // 經過 for 循環,遍歷了當前 ViewGroup 下的全部子 View
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);

                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount - 1;
                        }
                        
                        ... ...    // 可有可無的代碼咱們這邊暫且省略

                        // 派發事件到子 View 處理
                        if (dispatchTransformedTouchEvent(ev, ...)) { 
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            // 若是子 View 處理了事件,則 break,
                            // ViewGroup 不在處理事件(事件被攔截)。
                            break;     
                        }

                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                
                ... ...
                 
            }
        }
        
        ... ...
        
    }
    return handled;
}

其實 ViewGroup 的 dispatchTouchEvent 處理流程,咱們只須要關注兩個重點方法:onInterceptTouchEventdispatchTransformedTouchEvent

4.1.1 onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 一堆判斷,咱們只須要知道一點,通常 Touch 事件默認返回 false
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

4.1.2 dispatchTransformedTouchEvent

dispatchTransformedTouchEvent 方法的做用,主要就是把事件下發給子 View 進行處理。

private boolean dispatchTransformedTouchEvent(MotionEvent event, 
            boolean cancel, View child, int desiredPointerIdBits) {
        final boolean handled;
        
        ... ...

        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    ... ...
                    // dispatchTouchEvent()
                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            ... ...
            // dispatchTouchEvent()
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        ... ...

        // Done.
        transformedEvent.recycle();
        return handled;
    }

你有沒有發現?當存在子 View 的時候,會調用 View.dispatchTouchEvent 方法,若是沒有則會向上調用 super.dispatchTouchEvent 方法。

4.2 Demo

Layout 層次:

layout.png

Layout 代碼:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button_01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button_01"
        tools:layout_editor_absoluteX="94dp"
        tools:layout_editor_absoluteY="106dp" />

    <Button
        android:id="@+id/button_02"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button_02"
        tools:layout_editor_absoluteX="94dp"
        tools:layout_editor_absoluteY="211dp" />
</android.support.constraint.ConstraintLayout>

Activity 代碼:

package com.example.marco.myapplication;

import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private Button button_01;
    private Button button_02;
    private ViewGroup myLayout;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button_01 = (Button) findViewById(R.id.button_01);
        button_02 = (Button) findViewById(R.id.button_02);
        myLayout = (ConstraintLayout) findViewById(R.id.my_layout);

        // 1. ViewGroup: myLayout 佈局設置監聽事件
        myLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "點擊了ViewGroup");
            }
        });

        // 2. View: button_01 設置監聽事件
        button_01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "點擊了button_01");
            }
        });

        // 3. View: button_02設置監聽事件
        button_02.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "點擊了button_02");
            }
        });

    }
}

測試結果:

09-26 16:17:51.877 16250 16250 D TAG : 點擊了button_01     // 點擊按鈕 button_01
09-26 16:17:53.875 16250 16250 D TAG : 點擊了button_02     // 點擊按鈕 button_02
09-26 16:17:54.758 16250 16250 D TAG : 點擊了ViewGroup     // 點擊空白處

結果說明:

  • 點擊 Button 時,執行 Button.onClick(),但 ViewGroup_Layout 註冊的 onClick() 不會執行;
  • 只有點擊空白區域時,纔會執行 ViewGroup_Layout 的 onClick() 方法。

結論:Button 的 onClick() 將事件消費掉了,所以事件不會再繼續向下傳遞。

4.3 小結

ViewGroup 事件分發處理流程.png

5. Touch 事件分發 -- View

這邊咱們先作個說明:

其實,只要你觸摸了任何控件,就必定會調用該控件的 dispatchTouchEvent 方法!

嚴格一點來講:當你點擊了某個控件,首先會去調用該控件所在佈局dispatchTouchEvent 方法,而後在佈局dispatchTouchEvent 方法中找到被點擊的相應控件,再去調用該控件dispatchTouchEvent方法。

3.3.1 dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {

        ... ...
        
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED 
                                          && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            
            ★ ★ ★ ★ ★ ★ ★ ★ 重點方法 ★ ★ ★ ★ ★ ★ ★ ★ 
            /*
             * 注意:只有如下3個條件都爲真,dispatchTouchEvent() 才返回 true
             *       1. li != null & li.mOnTouchListener != null
             *       2. (mViewFlags & ENABLED_MASK) == ENABLED
             *       3. mListenerInfo.mOnTouchListener.onTouch(this, event)
             */
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            ★ ★ ★ ★ ★ ★ ★ ★ 分別分析以上幾個條件 ★ ★ ★ ★ ★ ★ ★ ★ 
            
            // 若是上面沒有返回 true,那麼執行 onTouchEvent()
            if (!result && onTouchEvent(event)) {    // 下面再分析
                result = true;
            }
        }

        ... ...
        
        return result;
    }
條件 1:li != null & li.mOnTouchListener != null

mOnTouchListener 這個變量是在哪裏賦值的呢?

/**
      * 條件 1:mListenerInfo.mOnTouchListener != null
      * 說明:mOnTouchListener 變量在 View.setOnTouchListener() 方法裏賦值
      */
    public void setOnTouchListener(OnTouchListener l) {
        // 只要咱們給控件註冊了 Touch 事件,mOnTouchListener 就必定被賦值(不爲空)
        getListenerInfo().mOnTouchListener = l;
    }
條件 2:(mViewFlags & ENABLED_MASK) == ENABLED
/**
      * 條件 2:(mViewFlags & ENABLED_MASK) == ENABLED
      * 說明:
      *     a. 該條件是判斷當前點擊的控件是否 enable
      *     b. 通常 View 默認 enable,故該條件恆定爲 true
      */
條件 3:mListenerInfo.mOnTouchListener.onTouch(this, event)

mListenerInfo.mOnTouchListener.onTouch(this, event),其實也就是去回調控件註冊 touch 事件時的 onTouch 方法。

也就是說若是咱們在 onTouch 方法裏返回 true,就會讓這三個條件所有成立,從而整個方法直接返回 true。若是咱們在 onTouch 方法裏返回 false,就會再去執行 onTouchEvent(event) 方法。

/**
      * 條件 3:mOnTouchListener.onTouch(this, event)
      * 說明:回調控件註冊 Touch 事件時的 onTouch()
      */
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
            return false;  
        }  
    });

3.3.2 onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        ... ...

        // 若該控件可點擊,則進入 switch 判斷中
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                // a. 若當前的事件 = 擡起 View
                case MotionEvent.ACTION_UP:
                    ... ...
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ... ...
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();     // 重點分析函數
                                }
                            }
                        }
                        ...
                    }
                    break;
                    
                // b. 若當前的事件 = 按下 View
                case MotionEvent.ACTION_DOWN:
                    ... ...
                    break;

                // c. 若當前的事件 = 結束事件(非人爲緣由)
                case MotionEvent.ACTION_CANCEL:
                    ... ...
                    break;

                // d. 若當前的事件 = 滑動 View
                case MotionEvent.ACTION_MOVE:
                    ... ...
                    break;
            }

            // 若該控件可點擊,就必定返回true
            return true;
        }
        
        // 若該控件不可點擊,就必定返回false
        return false;
    }

3.3.3 performClick

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        /*
         * 只要咱們經過 setOnClickListener() 爲控件 View 註冊1個點擊事件,
         * 那麼就會給 li.mOnClickListener 變量賦值(即不爲空),
         * 則會往下回調 onClick(),performClick() 返回 true。
         */
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

3.3.4 Demo

Layout代碼:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button_01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button_01"
        tools:layout_editor_absoluteX="94dp"
        tools:layout_editor_absoluteY="106dp" />

</LinearLayout>

Activity代碼:

package com.example.marco.myapplication;

import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends AppCompatActivity {

    private Button button_01;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button_01 = (Button) findViewById(R.id.button_01);

        /**
          * 結論驗證1:在回調 onTouch() 裏返回 false
        */
        // 1. 經過 OnTouchListener() 複寫 onTouch(),從而手動設置返回 false
        button_01.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("TAG", "run onTouch(), action:" + event.getAction());
                return false;
            }
        });

        // 2. 經過 OnClickListener()爲控件設置點擊事件,
        //    爲 mOnClickListener 變量賦值(即不爲空),從而往下回調 onClick()
        button_01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "run onClick()");
            }
        });
        
        /**
          * 結論驗證2:在回調 onTouch() 裏返回 true
          */
        // 1. 經過 OnTouchListener()複寫 onTouch(),從而手動設置返回 true
        button_01.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("TAG", "run onTouch(), action:" + event.getAction());
                return true;
            }
        });

        // 2. 經過 OnClickListener()爲控件設置點擊事件,
        //    爲 mOnClickListener 變量賦值(即不爲空),從而往下回調 onClick()
        button_01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "run onClick()");
            }
        });

    }
}

測試結果:

// 經過 OnTouchListener() 複寫 onTouch(),從而手動設置返回 false
09-26 18:14:19.299 23350 23350 D TAG : run onTouch(), action:0    // ACTION_DOWN
09-26 18:14:19.327 23350 23350 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:14:19.343 23350 23350 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:14:19.383 23350 23350 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:14:19.384 23350 23350 D TAG : run onTouch(), action:1    // ACTION_UP
09-26 18:14:19.385 23350 23350 D TAG : run onClick()
// 經過 OnTouchListener() 複寫 onTouch(),從而手動設置返回 true
09-26 18:16:29.758 23847 23847 D TAG : run onTouch(), action:0    // ACTION_DOWN
09-26 18:16:29.773 23847 23847 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:16:29.856 23847 23847 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:16:29.858 23847 23847 D TAG : run onTouch(), action:1    // ACTION_UP

3.3.5 小結

View 事件分發機制流程圖.png

6. 總結

       ✒ 一、Android 事件分發機制主要由【「事件分發」 —> 「事件攔截」 —> 「事件響應」】這三步來進行邏輯控制的。

       ✒ 二、ViewGroup 默認不攔截任何事件。

       ✒ 三、onInterceptTouchEvent 返回 true 表示事件攔截,onTouchEvent 返回 true 表示事件消費。

       ✒ 四、點擊事件的分發過程以下:dispatchTouchEvent —> onTouchListener 的 OnTouch 方法 —> onTouchEvent —> onClickListener 的 onClick 方法。從而也能夠看出 onTouch 是優先於 onClick 執行,事件傳遞的順序是先通過 onTouch,再傳遞到 onClick。

       ✒ 五、Android 中的事件 onClick、onLongClick、onScroll 等,都是由多個 Touch 事件(一個 ACTION_DOWN,多個 ACTION_MOVE,一個 ACTION_UP)組成。

       ✒ 六、子 View 能夠經過使用 getParent().requestDisallowInterceptTouchEvent(true) ,阻止 ViewGroup 對其 MOVE 或 UP 事件進行攔截。

       ✒ 七、MotionEvent 對象的四種狀態:MotionEvent.ACTION_DOWN:手指按下屏幕的瞬間

                                                                   MotionEvent.ACTION_MOVE:手指在屏幕上移動

                                                                   MotionEvent.ACTION_UP:手指離開屏幕瞬間

                                                                   MotionEvent.ACTION_CANCEL:取消手勢

       ✒ 八、點擊某個控件,首先會去調用該控件所在佈局的 dispatchTouchEvent方法,而後在佈局的 dispatchTouchEvent 方法中找到被點擊的相應控件,再去調用該控件的 dispatchTouchEvent方法。

       ✒ 九、若是 View 沒有消費 ACTION_DOWN 事件,則以後的 ACTION_MOVE 等事件都不會再接收。

       ✒ 十、事件在從 Activity.dispatchTouchEvent 往下分發的過程當中:

若是中間的 ViewGroup 都不攔截,進入最底層的 View 後,由 View.onTouchEvent 處理,若是 View 也沒有消費事件,最後會返回到 Activity.onTouchEvent。 若是中間任何一層 ViewGroup 攔截事件,則事件再也不往下分發,交由攔截的 ViewGroup 的 onTouchEvent 來處理。
相關文章
相關標籤/搜索