Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View 觸摸事件傳遞機制

Android View 觸摸事件傳遞機制

編輯:關於Android編程

PS:以現在的眼光看以前寫的博客感覺寫的很爛,或許或一段時間再看現在的博客會有同樣的感覺。所以每時每刻都去學習,去發現和理解新的東西。

引言

由於之前寫的一篇關於Android事件傳遞順序的博客質量太差,可能是理解不到位的原因,故最近又花了許多時間再次去看Android源碼,看完之後有了新的理解,所以打算重新整理這篇博客。理解Android觸摸事件傳遞機制有助於日後的開發以及自定義一些手勢效果等。注意:這篇博客是基於Android2.0源碼來分析的,不管老版本還是新版本的Android,其內部觸摸事件傳遞機制是不變的。只是說Android2.0的源碼相對比較少,便於讀者理解。

示例

自定義一個MyCustomView

public class MyCustomView extends View {

    private String TAG = MyButton;

    public MyCustomView(Context context) {
        super(context);
    }

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            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);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            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);
    }
}

重寫dispatchTouchEvent和onTouchEvent方法,添加三種觸摸事件的打印日志。

MainActivity調用如下:

public class MainActivity extends Activity {
    private MyCustomView button;
    private String TAG = MainActivity;

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

        button = (MyCustomView) findViewById(R.id.button);

        Log.e(TAG, the view is clickable  + button.isClickable());

        button.setClickable(true);

        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG, onTouch------->>ACTION_DOWN);
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG, onTouch------->>ACTION_MOVE);
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG, onTouch------->>ACTION_UP);
                        break;
                }
                return false;
            }
        });

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, onClick------->>onClick);
            }
        });
    }
}

點擊自定義MyCustomView,結果打印如下:

07-29 11:03:51.714  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ the view is clickable false
07-29 11:03:54.877  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_DOWN
07-29 11:03:54.878  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_DOWN
07-29 11:03:54.878  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ onTouchEvent----->>ACTION_DOWN
07-29 11:03:54.929  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_MOVE
07-29 11:03:54.929  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_MOVE
07-29 11:03:54.929  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ onTouchEvent----->>ACTION_MOVE
07-29 11:03:54.929  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_UP
07-29 11:03:54.931  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_UP
07-29 11:03:54.931  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ onTouchEvent----->>ACTION_UP
07-29 11:03:54.936  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ onClick------->>onClick

分析:有上面的打印可以看出

觸摸事件主要有三種且執行順序為:ACTION_DOWN,ACTION_MOVE,ACTION_UP。也就是先執行ACTION_DOWN按下的行為,按下之後手指可能會移動,移動時就出發了ACTION_MOVE行為,當手指抬起時,觸發了ACTION_UP行為,至此觸摸事件順序執行結束。當然觸摸事件不止這三種行為,但是我們這裡主要分析這三種。 觸摸事件過程執行的方法順序為:dispatchTouchEvent,onTouch,onTouchEvent。最後執行了onClick點擊事件。也就是順序應該為:dispatchTouchEvent–>>onTouch–>>onTouchEvent–>>onClick onClick點擊事件是在觸摸事件ACTION_UP執行完之後才執行。

為什麼會有以上三種現象和情況出現?現在我們只能從打印日志中看到結果,但是並不知道其內部原因,為了窺探其內部原因,read the fuck code 。基於Android2.0源碼分析View。View的觸摸事件分發是從View#dispatchTouchEvent方法開始執行的。至於為什麼從這裡,以後再說。

View#dispatchTouchEvent觸摸事件分發

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

分析:方法實現很簡單,當滿足if條件就返回true退出方法,條件不滿足時,才去執行onTouchEvent方法且返回該方法的返回值。
1.那麼什麼情況下滿足mOnTouchListener != null條件呢?查看View源碼發現調用如下方法時:

public void setOnTouchListener(OnTouchListener l) {
        mOnTouchListener = l;
    }

當開發者給相應的View設置了View#setOnTouchListener觸摸事件之後,mOnTouchListener != null條件就成立。
2.View默認都是enabled狀態,所以第二個條件成立。
3.當前兩個條件都成立了,執行第三個條件接口方法mOnTouchListener.onTouch(this, event)。根據該方法的返回值來決定if條件是否成立。該方法在開發者設置View#setOnTouchListener觸摸事件實現,當onTouch方法返回false時,dispatchTouchEvent方法就會執行onTouchEvent方法,否則不執行onTouchEvent方法。

總結:
1.onTouch接口方法的返回值決定是否執行onTouchEvent方法。
2.只要onTouch接口方法返回值為true,dispatchTouchEvent方法一定返回true,否則根據onTouchEvent方法返回值決定dispatchTouchEvent返回值。

View#onTouchEvent

進入onTouchEvent方法:

public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;

     ................

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    if ((mPrivateFlags & PRESSED) != 0) {
                        // 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
                            if (mPendingCheckForLongPress != null) {
                                removeCallbacks(mPendingCheckForLongPress);
                            }

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                performClick();
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mPrivateFlags |= PRESSED;
                    refreshDrawableState();
                    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
                        postCheckForLongClick();
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    mPrivateFlags &= ~PRESSED;
                    refreshDrawableState();
                    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 = ViewConfiguration.get(mContext).getScaledTouchSlop();
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||
                            (y < 0 - slop) || (y >= getHeight() + slop)) {
                        // Outside button
                        if ((mPrivateFlags & PRESSED) != 0) {
                            // Remove any future long press checks
                            if (mPendingCheckForLongPress != null) {
                                removeCallbacks(mPendingCheckForLongPress);
                            }

                            // Need to switch from pressed to not pressed
                            mPrivateFlags &= ~PRESSED;
                            refreshDrawableState();
                        }
                    } else {
                        // Inside button
                        if ((mPrivateFlags & PRESSED) == 0) {
                            // Need to switch from not pressed to pressed
                            mPrivateFlags |= PRESSED;
                            refreshDrawableState();
                        }
                    }
                   break;
            }
            return true;
        }

        return false;
    }

分析:代碼有點長,首先進入該方法判斷if條件是否成立?如果該View是可點擊的或者是可以長按點擊,則if條件成立,進入if判斷,執行ACTION_UP分支。
1.代碼第26行,調用了performClick方法來執行View的點擊事件

public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        if (mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            mOnClickListener.onClick(this);
            return true;
       }

        return false;
    }

該方法判斷如果mOnClickListener!=null條件成立,則執行mOnClickListener.onClick(this);接口方法。什麼時候條件成立呢?當給當前View設置了點擊監聽事件之後,條件成立,因此調用接口onClick方法。在View類中有如下方法:

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

先判斷當前View是否可點擊的狀態?如果不可點擊的話,先設置成可點擊,之後對mOnClickListener賦值操作。總結:只要給任何一個View設置了setOnClickListener點擊監聽事件,不管這個View是否是可點擊的狀態,最後都設置為了可點擊的狀態了。

2.只有當前View是可點擊或者長按的狀態,才進入if條件判斷,然後執行相應的手勢操作,最後返回true。也就是說,只要View是可點擊的,onTouchEvent方法返回的就是true,從而dispatchTouchEvent方法返回的也是true。

3.只要是當前View是不可點擊或者長按的狀態,if條件不成立,不執行任何操作,直接返回false。也就是說,View不可點擊的時候,onTouchEvent方法返回的就是false,從而dispatchTouchEvent方法返回的也是false。

4.onClick方法是在ACTION_UP手勢裡面執行的,也就是當手勢抬起時才去執行onClick方法。

到此,Android View觸摸事件傳遞已經分析結束。如果條件都滿足,則整個觸摸事件傳遞過程就是:dispatchTouchEvent–>>onTouch–>>onTouchEvent–>>onClick。現在我們來驗證一下如下幾種情況:

onTouch方法返回值為true

改成如下

 button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG, onTouch------->>ACTION_DOWN);
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG, onTouch------->>ACTION_MOVE);
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG, onTouch------->>ACTION_UP);
                        break;
                }
                return true;
            }
        });

此時點擊View的打印如下:

07-29 14:42:22.969    2964-2964/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_DOWN
07-29 14:42:22.970    2964-2964/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_DOWN
07-29 14:42:22.987    2964-2964/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_MOVE
07-29 14:42:22.987    2964-2964/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_MOVE
07-29 14:42:22.987    2964-2964/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_UP
07-29 14:42:22.988    2964-2964/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_UP

當onTouch方法返回true時,就不執行onTouchEvnet方法,因此也就不執行onClick點擊事件。可以理解成此時onTouch把觸摸事件已經消費掉了,也就不會繼續往下傳遞觸摸事件。所以如果你不想自己的View執行onTouchEvent方法,你可以設置onTouch事件,且返回值為true即可。

View不可點擊情況

在文章開頭的 MainActivity裡面,我是添加了一行代碼 button.setClickable(true); 目的是讓當前View可點擊,但是默認情況下除了Button,TextView少數控件外,其他大部分View控件默認都是不可點擊的狀態,除非你設置了View#setClickable(true)或者View#setOnClickListener。現在我將MainActivity中的button.setClickable(true);這一行代碼去掉且不設置setOnClickListener事件,

public class MainActivity extends Activity {
    private MyCustomView button;
    private String TAG = MainActivity;

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

        button = (MyCustomView) findViewById(R.id.button);

        Log.e(TAG, the view is clickable  + button.isClickable());

//        button.setClickable(true);

        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG, onTouch------->>ACTION_DOWN);
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG, onTouch------->>ACTION_MOVE);
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG, onTouch------->>ACTION_UP);
                        break;
                }
                return false;
            }
        });

//        button.setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View v) {
//                Log.e(TAG, onClick------->>onClick);
//            }
//        });
    }
}

Log打印日志如下:

07-29 14:57:03.656    4896-4896/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_DOWN
07-29 14:57:03.658    4896-4896/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_DOWN
07-29 14:57:03.658    4896-4896/com.xjp.toucheventdemo E/MyButton﹕ onTouchEvent----->>ACTION_DOWN

不知道你發現木有?此處打印看出只執行了ACTION_DOWN手指操作,其他的手勢操作呢?沒有執行,為什麼呢?
情況是這樣的:當onTouch方法返回false,則dispatchTouchEvent方法就會執行onTouchEvent方法,但是由於View不可點擊,所以onTouchEvent是不執行if條件體的,也就是onTouchEvent方法返回false,從而導致dispatchTouchEvent方法返回false,由於dispatchTouchEvent方法返回false,導致後面的手勢操作ACTION_MOVE,ACTION_UP得不到執行。

總結:如果我們將手勢操作分為三個過程的話:ACTION_DOWN,ACTION_MOVE,ACTION_UP。只有當dispatchTouchEvent方法返回true時,系統才會執行對應過程後面的手勢操作。

總結

至此Android View 觸摸事件傳遞機制已經分析結束,現在用一個流程圖來體現:

這裡寫圖片描述

觸摸事件傳遞順序:dispatchTouchEvent–>>onTouch–>>onTouchEvent–>>onClick。 onTouch和onTouchEvent區別:兩個方法先後在dispatchTouchEvent中調用,只有給View設置了觸摸事件View#setOnTouchListener才會執行onTouch方法;onTouch方法的返回值決定是否執行onTouchEvent方法。 手勢操作執行的順序為ACTION_DOWN,ACTION_MOVE,ACTION_UP,只有dispatchTouchEvent方法返回true值時後面的手勢才會被執行。 onClick方法的調用是在onTouchEvent的ACTION_UP手勢裡面執行的,也就是當手勢抬起時,手勢操作結束才會觸發onClick方法的調用。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved