1. 程式人生 > >android進階4step4:Android實戰開發——事件分發機制

android進階4step4:Android實戰開發——事件分發機制

Android事件分發機制

為什麼需要事件分發機制?

比如:上圖

Button(View)的ViewGroup是FrameLayout2

FragmeLayout2的ViewGroup是FragmeLayout1

當點選Button時,所觸發的事件到底是交給誰來處理呢?

常見的事件分發分為兩種

  • 冒泡(自下而上的過程)  View—>ViewGroup—>Activity
  • 捕獲(自上而下的過程)  Activity—>ViewGroup—>View

Android中的事件分發機制 

注意:嚴格來說以下流程只是ACTION_DOWN的一種特殊的情況

 程式碼實現:

 MyFrameLayout.java 自定義的ViewGroup

重寫了以下三個方法

  • public boolean dispatchTouchEvent(MotionEvent ev)   事件分發
  • public boolean  onInterceptTouchEvent(MotionEvent ev)  事件攔截(ViewGroup的方法)
  • public boolean onTouchEvent(MotionEvent event)   事件處理(是否消費該事件)
/**
 * 自定義ViewGroup(FrameLayout本身就是一個ViewGroup)的MyFragmentLayout
 */
public class MyFrameLayout extends FrameLayout
{
    private static final String TAG = "MyFrameLayout";

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev)
    {
        final int action = ev.getAction();

        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent - ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent - ACTION_UP");
                break;
        }

        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev)
    {
        final int action = ev.getAction();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onInterceptTouchEvent - ACTION_DOWN");
                mLastY = ev.getAction();
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onInterceptTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent - ACTION_UP");
                break;
        }
        return super.onInterceptTouchEvent(ev);

    }

    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        final int action = event.getAction();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent - ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent - ACTION_UP");//
                break;
        }
        return super.onTouchEvent(event);
    }
}

MyView.java 自定義View

重寫了以下兩個方法

  • public boolean dispatchTouchEvent(MotionEvent ev)   事件分發
  • public boolean onTouchEvent(MotionEvent event)   事件處理(是否消費該事件)
/**
 * 自定義View MyView
 */
public class MyView extends View {
    private static final String TAG = "MyView";

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        final int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent - ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent - ACTION_UP");
                break;
        }
        return super.dispatchTouchEvent(event);
    }


    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent - ACTION_DOWN");
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent - ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }
}

 TouchSystemActivity.java  Activity

重寫了以下兩個方法

  • public boolean dispatchTouchEvent(MotionEvent ev)   事件分發
  • public boolean onTouchEvent(MotionEvent event)   事件處理(是否消費該事件)
public class TouchSystemActivity extends AppCompatActivity {

    private static final String TAG = "TouchSystemActivity";

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


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent - ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent - ACTION_UP");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        final int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent - ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent - ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }
}

佈局檔案:

activity_touch_system.xml 

<com.demo.android4step4.view.MyFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#728dff"
    >

    <com.demo.android4step4.view.MyView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:background="#ffc481"/>


</com.demo.android4step4.view.MyFrameLayout>

現在來操作:

當手指點選MyView時會執行 ACTION_DOWN ——>ACTION_MOVE

手指離開執行  ACTION_UP

那麼這幾個事件會怎麼進行分發呢?

注意:嚴格來說以下流程只是ACTION_DOWN的一種特殊的情況

前面4個階段是捕獲(自上而下)的事件分發模式

後面3個階段是冒泡(自下而上)的事件分發模式

當所有的View和ViewGroup都不消費該事件,那麼就會自動傳給當前Activity進行處理

之後的事件(直到下次點選開始,完一次點選事件的完成過程)都由它進行處理了。

可見:下面的MOVE 和UP都是由Activity的onTouchEvent方法進行(消費)處理的

E/TouchSystemActivity: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: onInterceptTouchEvent - ACTION_DOWN
E/MyView: dispatchTouchEvent - ACTION_DOWN

E/MyView: onTouchEvent - ACTION_DOWN
E/MyFrameLayout: onTouchEvent - ACTION_DOWN
E/TouchSystemActivity: onTouchEvent - ACTION_DOWN

E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE

E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE

E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE

E/TouchSystemActivity: dispatchTouchEvent - ACTION_UP
E/TouchSystemActivity: onTouchEvent - ACTION_UP

 對事件感興趣的View

上面講的是一個預設情況下,會交由Activity進行事件的處理

那View自身如何表明對事件感興趣呢? 

最主要是View.dispatchOnTouchEvent()在 ACTION_DOWN的時候返回true

但是一般情況下,我們主要重寫的方法是onTouchEvent, 所以要保證ACTION_DOWN返回true

  • 注:凡是clickable = true 或者 longClickable = ture的控 件,正常情況下View.onTouchEvent()一定返回true

還是手指點選View的過程:DOWN-MOVE*-UP 

如果在View中的OnTouchEvent方法中返回True 表明對該事件感興趣(消費該事件),進行相應的處理

只要ACTION_DOWN 中返回true其他的事件也是預設交由該View進行處理

那麼事件分發機制是以下的流程,虛線是不走的

 有兩種方式返回true

  1. 直接在末尾返回true 
  2. 在Action_Down的時候返回true,末尾執行父類的super.onTouchEvent也是可以
  @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        //Log.e(TAG,MotionEvent.actionToString(event.getAction()));
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent - ACTION_DOWN");
                //getParent().requestDisallowInterceptTouchEvent(true);
                //只要ACTION_DOWN 返回true其他的事件也是預設交由該View進行處理
                //1。這裡返回true也可以,其他兩個預設返回false
                 return true ;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent - ACTION_UP");
                break;
        }
        //2
        return super.onTouchEvent(event);
    }

log日誌:  

E/TouchSystemActivity: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: onInterceptTouchEvent - ACTION_DOWN
E/MyView: dispatchTouchEvent - ACTION_DOWN
E/MyView: onTouchEvent - ACTION_DOWN

E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onInterceptTouchEvent - ACTION_MOVE
E/MyView: dispatchTouchEvent - ACTION_MOVE
E/MyView: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE

E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onInterceptTouchEvent - ACTION_MOVE
E/MyView: dispatchTouchEvent - ACTION_MOVE
E/MyView: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE


E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onInterceptTouchEvent - ACTION_MOVE
E/MyView: dispatchTouchEvent - ACTION_MOVE
E/MyView: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE

E/TouchSystemActivity: dispatchTouchEvent - ACTION_UP
E/MyFrameLayout: dispatchTouchEvent - ACTION_UP
E/MyFrameLayout: onInterceptTouchEvent - ACTION_UP
E/MyView: dispatchTouchEvent - ACTION_UP
E/MyView: onTouchEvent - ACTION_UP
E/TouchSystemActivity: onTouchEvent - ACTION_UP

你會發現前面的Down 返回了true該事件經由View消費

但是當Move 和 Up操作時會到 Activity的OnTouchEvent 方法

為什麼呢?

在Activity中的dispatchTouchEvent(ev)的原始碼中

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
  • 看第二個if 如果該事件轉發下去返回true則,直接返回true。因為在View的轉發Move和Up事件轉發是返回的事false,則activity會執行它本身的onTouchEvent的方法,這就是它會輸出的原因。

ViewGroup對事件進行攔截

 攔截的目的是交給自己處理(onTouchEvent)

在MyFragmentLayout中的

onInterceptTouchEvent 返回true表示攔截該事件

onTouchEvent 中返回 true表示處理該事件

E/TouchSystemActivity: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: onInterceptTouchEvent - ACTION_DOWN
E/MyFrameLayout: onTouchEvent - ACTION_DOWN

E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onTouchEvent - ACTION_MOVE

E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onTouchEvent - ACTION_MOVE

E/TouchSystemActivity: dispatchTouchEvent - ACTION_UP
E/MyFrameLayout: dispatchTouchEvent - ACTION_UP
E/MyFrameLayout: onTouchEvent - ACTION_UP

可以看到

第一次在Down中攔截事件後,事件就不再往下轉發給View,而是交給自己的onTouchEvent方法進行處理

之後的Move*-Up都不執行攔截操作了,預設之後的事件都交給該ViewGroup處理

以下是Down的分發過程: 

下面是Move*-Up的分發過程 

模擬真實攔截過程:

在ViewGroup中 onInterceptTouchEvent 中

如果點選的點到最後的觸控點的Y大於200dp(實際就是手指下滑200dp後觸發攔截事件)

一旦攔截,之後的所有事件都由ViewGroup處理

    //記錄Down時的點的位置
    private int mLastY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onInterceptTouchEvent - ACTION_DOWN");
                mLastY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onInterceptTouchEvent - ACTION_MOVE");
     //如果當前的觸控點和我們之前down時候的點大於200dp
                if (ev.getY() - mLastY > 200) {
                    Log.e(TAG, "down時候Y的位置" + mLastY);
                    Log.e(TAG, "up時候Y的位置 + ev.getY());
                    Log.e(TAG, "onInterceptTouchEvent - ACTION_MOVE - return true ");
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onInterceptTouchEvent - ACTION_UP");
                break;
        }
        return super.onInterceptTouchEvent(ev);
//        return true;

    }

有沒有方法?讓ViewGroup不攔截子View要處理的事件呢?

 getParent().requestDisallowInterceptTouchEvent(true);

請求父View不攔截這個事件

    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        final int action = event.getAction();
        Log.e(TAG,MotionEvent.actionToString(event.getAction()));
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent - ACTION_DOWN");
                getParent().requestDisallowInterceptTouchEvent(true);
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent - ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent - ACTION_UP");
                break;
        }
        //return super.onTouchEvent(event);
        return true ;
    }

 

View與ViewGroup相關方法職責

  •  dispatchTouchEvent ACTION_DOWN的時候:逆序遍歷子View,找出對該事件感興趣的View,標記為targetView,然後對於該手勢(DOWN-MOVE*-UP)的後續事件都傳給targetView。
  •  onInterceptTouchEvent 在ACTION_DWON,或者存在targetView的情況下,可以隨時對該事件進行攔截,交給自己處理。
  • onTouchEvent 拿到事件後做針對當前View的相關操作。

 

自定義View中的事件分發

  •  處理Touch事件

– 可以編寫setOnTouchListener(無需繼承View) – 複寫onTouchEvent

  • 注意標明對事件感興趣

– 如果需要自己獲取touch事件進行處理,ACTION_DOWN必須 返回true,保證整個手勢的事件都能夠傳遞到該View。 

• 包含上述View的所有事項

  • • 攔截子View事件

– 可以在onInterceptTouchEvent()中子View的事件進行攔截, 交給自己的onTouchEvent進行處理。

– 注意:一旦攔截針對當然的手勢所有事件都將由當前的 ViewGroup處理。會傳遞一個ACTION_CANCEL交給當前的子 View,讓子View明白後續的事件不會到來了。

 其他?

requestDisallowInterceptTouchEvent

來讓父佈局禁用攔截事件功能,從而父佈局忽略該事件之後的一切Action

ViewConfiguration 是系統中關於檢視的各種特性的常量記錄物件

• ViewConfiguration

  • – getScaledTouchSlop()

getScaledTouchSlop是一個距離,表示滑動的時候,手的移動要大於這個距離才開始移動控制元件。如果小於這個距離就不觸發移動控制元件,如viewpager就是用這個距離來判斷使用者是否翻頁

  • – getScaledMinimumFlingVelocity()

用於設定最小加速率

  • – getLongPressTimeout() 

長按事件閾值

  • • OnScrollListener / View.onScrollChanged()  滑動監聽
  • • GestureDetector  手勢檢測
  • • ScaleGestureDetector 伸縮手勢

  擴充套件學習

• Mastering the Android Touch System

– 視訊(中文字幕) http://v.youku.com/v_show/id_XODQ1MjI2MDQ0.html?from=s1.8- 1-1.2

– 英文(Google搜尋下)

– 程式碼 https://github.com/devunwired/custom-touc-examples