【安卓筆記】touch事件的分發和消費機制

來源:互聯網
上載者:User

Android 中與 Touch 事件相關的方法包括:dispatchTouchEvent(MotionEvent ev)、onInterceptTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent ev);能夠響應這些方法的控制項包括:ViewGroup、View、Activity。繼承ViewGroup的大多是容器控制項,如LinearLayout等,而繼承View的大部分是顯示控制項比如TextView,ImageView等(當然,ViewGroup本身是繼承View的),顯示控制項沒有onInterceptTouchEvent。來看一些例子。情形1:只有activity,沒有子控制項:代碼如下:

package com.example.toucheventdemo;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;public class MainActivity extends Activity{    private static final String TAG = "MainActivity";    @Override    protected void onCreate(Bundle savedInstanceState)    {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev)    {        switch (ev.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"dispatchTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"dispatchTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"dispatchTouchEvent--ACTION_UP");            break;        }        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event)    {        switch (event.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"onTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"onTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"onTouchEvent--ACTION_UP");            break;        }        return super.onTouchEvent(event);    }}
日誌資訊:
可以看到:總是先執行dispatchTouchEvent,再執行onTouchEvent.。

情形2:將上面代碼dispatchTouchEvent的返回值改為true。日誌:
可以看到,只執行了dispatchTouchEvent,而沒有執行onTouchEvent。說明在activity中dispatchTouchEvent先於onTouchEvent執行,如果將dispatchTouchEvent返回值置為true,表示事件被消費了,不再傳遞。

情形3:加入一個子控制項以自訂Button為例:
package com.example.toucheventdemo;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.Button;public class MyButton extends Button{    private static final String TAG = "MyButton";    public MyButton(Context context)    {        super(context);    }    public MyButton(Context context, AttributeSet attrs)    {        super(context, attrs);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev)    {        switch (ev.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"dispatchTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"dispatchTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"dispatchTouchEvent--ACTION_UP");            break;        }        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event)    {        switch (event.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"onTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"onTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"onTouchEvent--ACTION_UP");            break;        }        return super.onTouchEvent(event);    }}
mainActivity代碼如下:
package com.example.toucheventdemo;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.View.OnClickListener;public class MainActivity extends Activity{    private static final String TAG = "MainActivity";    private MyButton but = null;    @Override    protected void onCreate(Bundle savedInstanceState)    {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);                but = (MyButton) findViewById(R.id.but);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev)    {        switch (ev.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"dispatchTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"dispatchTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"dispatchTouchEvent--ACTION_UP");            break;        }        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event)    {        switch (event.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"onTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"onTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"onTouchEvent--ACTION_UP");            break;        }        return super.onTouchEvent(event);    }}
此時點擊Button按鈕,查看日誌:

執行流程是首先由activity捕獲到ACTION_DWON事件,然後調用activity的dispatchTouchEvent,接著繞開activity的onTouchEvent直接將事件傳遞給子控制項,調用MyButton的dispatchTouchEvent,在之後調用該控制項的onTouchEvent,ACTION_UP事件也是一樣的流程。

情形4:跟情形2類似,將情形3的activity的DispatchTouchEvent的返回值改為true,點擊按鈕,很顯然,touch事件將不會被分發給Button,所以點擊按鈕日誌是這樣的:
情形5:將情形3的myButton的DispatchTouchEvent的返回值改為true,點擊按鈕,很顯然,當touch事件傳遞到button時,先被dispatchTouchEvent捕獲,由於返回true,所以事件被消費,便不往下面傳遞,所以Button的onTouchEvent方法不被調用。日誌資訊:
情形6:將情形3的myButton的onTouchEvent的返回值改為false。日誌:
touch事件傳到button時,因為onTouchEvent返回值為false,所以繼續交由activity的onTouchEvent方法去執行,緊接著,ACTION_UP的動作將不再傳遞給button,直接由activity捕獲了。 情形7:給Activity的button按鈕增加onTouchListener和onClickListener
package com.example.toucheventdemo;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.View.OnClickListener;import android.view.View.OnTouchListener;public class MainActivity extends Activity implements OnClickListener,OnTouchListener{    private static final String TAG = "MainActivity";    private MyButton but = null;    @Override    protected void onCreate(Bundle savedInstanceState)    {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);                but = (MyButton) findViewById(R.id.but);        but.setOnClickListener(this);        but.setOnTouchListener(this);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev)    {        switch (ev.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"dispatchTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"dispatchTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"dispatchTouchEvent--ACTION_UP");            break;        }        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event)    {        switch (event.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"onTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"onTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"onTouchEvent--ACTION_UP");        }        return super.onTouchEvent(event);    }    @Override    public void onClick(View v)    {        Log.i("MyButton","ONCLICK");    }    @Override    public boolean onTouch(View v, MotionEvent event)    {        switch (event.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i("MyButton","onTouch--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i("MyButton","onTouch--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i("MyButton","onTouch--ACTION_UP");            break;        }        return false;    }}
現在點擊按鈕日誌列印如下資訊:

首先touch事件由activity捕獲,調用activity的dispatchTouchEvent,緊接著調用Button的dispatchTouchEvent繼續分發touch事件,接著並沒有調用button的onTouchEvent,而是先調用了onTouch方法,這是因為button按鈕註冊了onTouchListener的緣故,待onTouch事件處理完之後,由於返回值為false,所以touch事件傳遞給了button的onTouchEvent。接著ACTION_UP事件也是類似的過程,但當Button的onTouchEvent結束後,還調用了Onclick方法。

情形8:在情形7的代碼中將onTouch方法的返回值該true。相信大家已經猜出來了,改為true之後touch事件將不被button的onTouchEvent捕獲而是直接被消費了,從日誌也可以看出:
但是比較奇怪的是,onClick方法也沒用被執行,我們猜測onClick方法是在button的onTouchEvent方法中被執行的。事實也確實如此:在view的onTouchEvent方法中有這樣一段邏輯:
case MotionEvent.ACTION_UP:                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                    if ((mPrivateFlags & PFLAG_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 (prepressed) {                            // The button is being released before we actually                            // showed it as pressed.  Make it show the pressed                            // state now (before scheduling the click) to ensure                            // the user sees it.                            setPressed(true);                       }                        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) {                            postDelayed(mUnsetPressedState,                                    ViewConfiguration.getPressedStateDuration());                        } else if (!post(mUnsetPressedState)) {                            // If the post failed, unpress right now                            mUnsetPressedState.run();                        }                        removeTapCallback();                    }                    break;
在ACTION_UP分支上執行了click操作,具體由performClick方法執行:
 public boolean performClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        ListenerInfo li = mListenerInfo;        if (li != null && li.mOnClickListener != null) {            playSoundEffect(SoundEffectConstants.CLICK);            li.mOnClickListener.onClick(this);            return true;        }        return false;    }
情形9:以上是在activity中加入顯示控制項(TextView,Button等),下面在activity中加入容器控制項(LinearLayout等),此類控制項繼承ViewGroup,除了擁有dispatchTouchEvent和ontouchEvent之外,還有onInterceptTouchEvent方法,這個方法用於攔截touch事件,預設返回false,表示不攔截。下面我們自己實現一個容器控制項,並複寫onTouchEvent,dispatchTouchEvent和onInterceptTouchEvent:
package com.example.toucheventdemo;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.LinearLayout;public class MyLinearLayout extends LinearLayout{    private static final String TAG = "MyLinearLayout";    public MyLinearLayout(Context context)    {        super(context);    }    public MyLinearLayout(Context context, AttributeSet attrs)    {        super(context, attrs);    }        @Override    public boolean onTouchEvent(MotionEvent event)    {        switch (event.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"onTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"onTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"onTouchEvent--ACTION_UP");        }        return super.onTouchEvent(event);    }        @Override    public boolean dispatchTouchEvent(MotionEvent ev)    {        switch (ev.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"dispatchTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"dispatchTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"dispatchTouchEvent--ACTION_UP");        }        return super.dispatchTouchEvent(ev);    }        @Override    public boolean onInterceptTouchEvent(MotionEvent ev)    {        switch (ev.getAction())        {        case MotionEvent.ACTION_DOWN:            Log.i(TAG,"onInterceptTouchEvent--ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.i(TAG,"onInterceptTouchEvent--ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.i(TAG,"onInterceptTouchEvent--ACTION_UP");        }        return super.onInterceptTouchEvent(ev);    }    }
此時再點擊按鈕,查看日誌:


可以看到,由於加了一層容器控制項,所以activity執行完dispatchTouchEvent之後將touch事件分發給容器控制項MyLinearLayout,緊接著並不是直接將touch事件傳遞給button,而是先執行了onInterceptTouchEvent,這個方法返回false,並沒有攔截touch事件,所以接下來會將touch事件傳遞給button。

情形10:將MyLinearLayout的onInterceptTouchEvent方法返回值置為true,攔截touch事件,使其不再向下傳遞,點擊按鈕,查看日誌:
可以看到,touch事件再被攔截之後就不再傳遞給button了,而是被MyLinearLayout的onTouchEvent接收,接著由MainActivity的onTouchEvent接收並消費,ACTION_UP事件將直接由Activity處理。
總結: 1.事件傳遞的兩種方式:
  • 隧道方式:從根項目依次往下傳遞直到最內層子項目或在中間某一元素中由於某一條件停止傳遞。
  • 冒泡方式:從最內層子項目依次往外傳遞直到根項目或在中間某一元素中由於某一條件停止傳遞。 
2.android對Touch Event的分發邏輯是View從上層分發到下層(dispatchTouchEvent函數)類似於隧道方式,然後下層優先開始處理Event(先mOnTouchListener,再onTouchEvent)並向上返回處理情況(boolean值),若返回true,則上層不再處理,類似於冒泡方式。
3.touch事件分析: 事件分發:public boolean dispatchTouchEvent(MotionEvent ev)
  Touch 事件發生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會以隧道方式(從根項目依次往下傳遞直到最內層子項目或在中間某一元素中由於某一條件停止傳遞)將事件傳遞給最外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法,並由該 View 的 dispatchTouchEvent(MotionEvent ev) 方法對事件進行分發。dispatchTouchEvent 的事件分發邏輯如下:
  • 如果 return true,事件會分發給當前 View 並由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;
  •  如果 return false,事件分發分為兩種情況:
  如果返回系統預設的 super.dispatchTouchEvent(ev),事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。
事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)
  在外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系統預設的 super.dispatchTouchEvent(ev) 情況下,事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件攔截邏輯如下:
  • 如果 onInterceptTouchEvent 返回 true,則表示將事件進行攔截,並將攔截到的事件交由當前 View 的 onTouchEvent 進行處理;
  • 如果 onInterceptTouchEvent 返回 false,則表示將事件允許存取,當前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發;
  • 如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),處理邏輯與返回false相同。
事件響應:public boolean onTouchEvent(MotionEvent ev)
  在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 並且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情況下 onTouchEvent 會被調用。onTouchEvent 的事件響應邏輯如下:
  • 如果事件傳遞到當前 View 的 onTouchEvent 方法,而該方法返回了 false,那麼這個事件會從當前 View 向上傳遞,並且都是由上層 View 的 onTouchEvent 來接收,如果傳遞到上面的 onTouchEvent 也返回 false,這個事件就會“消失”,而且接收不到下一次事件。
  • 如果返回了 true 則會接收並消費該事件。
  • 如果返回 super.onTouchEvent(ev) 預設處理事件的邏輯和返回 false 時相同。





聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.