自定義View系列教程06--詳解View的Touch事件處理


深刻探討Android異步精髓Handlerjava


站在源碼的肩膀上全解Scroller工做機制markdown


Android多分辨率適配框架(1)— 核心基礎
Android多分辨率適配框架(2)— 原理剖析
Android多分辨率適配框架(3)— 使用指南框架


自定義View系列教程00–推翻本身和過往,重學自定義View
自定義View系列教程01–經常使用工具介紹
自定義View系列教程02–onMeasure源碼詳盡分析
自定義View系列教程03–onLayout源碼詳盡分析
自定義View系列教程04–Draw源碼分析及其實踐
自定義View系列教程05–示例分析
自定義View系列教程06–詳解View的Touch事件處理
自定義View系列教程07–詳解ViewGroup分發Touch事件
自定義View系列教程08–滑動衝突的產生及其處理異步


PS:若是以爲文章太長,那就直接看視頻工具


在以前的幾篇文章中結合Andorid源碼還有示例分析完了自定義View的三個階段:measure,layout,draw。 在自定義View的過程當中咱們還常常須要處理View的Touch事件,這就涉及到了大夥常說的Touch事件的分發。其實,這一部分仍是有些複雜的,並且有的地方不是很好理解,尤爲是對於剛上路的新司機來講常常理不清楚,慾求不滿,欲罷不能——想搞懂卻又以爲難,想放棄又以爲捨不得。源碼分析

好吧,我也經歷過這些痛楚,感同身受。post

因此,咱們就從相對而言比較簡單的View的Touch事件處理入手開始這部分知識的學習和總結。學習

滴滴,開車了,車門即將關閉。上車請刷卡,沒卡的乘客請投幣。this


若是一個View(好比Button)接收到Touch,那麼該Touch事件首先會傳入到它的dispatchTouchEvent( )方法,因此咱們從這裏開始學習View對Touch事件的處理。spa

/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (event.isTargetAccessibilityFocus()) {

            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }

            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && 
                (mViewFlags&ENABLED_MASK)==ENABLED && li.mOnTouchListener.onTouch(this,event)) {
                        result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }


        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

嗯哼,這段源碼不長,除了註釋就剩下不到100行了。該方法的輸入參數爲event它表示Touch事件,這個很好理解;那麼它的返回值有是什麼含義呢?該boolean值表示的是Touch事件是否被消費。

在此,對該部分源碼的核心部分和主要邏輯作一個梳理

第一步:
調用TouchListener中的onTouch()處理Touch事件,請參見代碼第31-32行

該if判斷中一共包含了4個條件,必須同時知足時才表示Touch事件被消費

  1. li != null
    ListenerInfo是View中的一個靜態類,包含了幾個Listener,好比TouchListener,FocusChangeListener,LayoutChangeListeners,ScrollChangeListener等等。通常狀況下它均不爲null,因此咱們不用過多關注它。
  2. li.mOnTouchListener != null
    mOnTouchListener是由View設置的,好比mButton.setOnTouchListener()。因此若是View設置了Touch監聽那麼,那麼mOnTouchListener不空;反之,mOnTouchListener爲null
  3. (mViewFlags & ENABLED_MASK) == ENABLED
    當前View可用(ENABLED)。一般可調用view.setEnabled( )設置View是否可用
  4. li.mOnTouchListener.onTouch(this, event)
    這一點實際上是在li.mOnTouchListener != null的基礎上繼續判斷。判斷TouchListener的onTouch( )方法是否消耗了Touch事件。返回值爲true表示消費掉該事件,false表示未消費。

在這四個條件中,咱們一般最關心的就是最後一個:TouchListener的onTouch()方法。假如這四個條件中的任意一個不知足,那麼result仍爲false;則進入下一步

第二步:
調用View自身的onTouchEvent()處理Touch事件,請參見代碼第36-38行

if (!result && onTouchEvent(event)) {
     result = true;
 }

嗯哼,看到了吧:若是在上一步中Touch事件被消費result爲true,就不會執行這三行代碼了。該處調用了onTouchEvent()若該方法返回值false那麼dispatchTouchEvent()的返回值也爲false;反之,若該方法返回值爲true,那麼dispatchTouchEvent()的返回值亦爲true。
既然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();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {

            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }

            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {

                            removeLongPressCallback();

                            if (!focusTaken) {

                                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)) {
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    boolean isInScrollingContainer = isInScrollingContainer();

                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    if (!pointInView(x, y, mTouchSlop)) {
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {

                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

這段代碼稍微複雜一些,在此分析幾個核心點。

  1. 當View爲disable時對於Touch的處理,請參見代碼第7-16行。
    若一個View是disable的,若是它是CLICKABLE或者LONG_CLICKABLE或CONTEXT_CLICKABLE的就返回true,表示消耗掉了Touch事件。
    可是請注意,該view所對應的ClickListener.onClick( )不會有任何的響應。即官方文檔的描述:

    A disabled view that is clickable still consumes the touch events, it just doesn’t respond to them.

    若View雖然是disable的,但只要知足這三個條件中的一個,它就會消費掉Touch事件但再也不回調view的onClick( )方法

  2. 處理ACTION_DOWN,ACTION_MOVE,ACTION_UP事件等,請參見代碼第24-116行。
    在此請注意在對於ACTION_UP的處理時調用了performClick(),請參見代碼第50行。

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
    
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

    在該方法中調用了view的mOnClickListener.onClick( ),請參見代碼第6行。
    嗯哼,看到了吧:咱們日常見得不少的Click事件是在View的onTouchEvent( )中處理ACTION_UP時調用的。

  3. 返回onTouchEvent()方法的輸出結果,請參見代碼第118-121行。
    在該處請尤爲注意:
    若是View是enable的,只要該View知足CLICKABLE和LONG_CLICKABLE以及CONTEXT_CLICKABLE這三者的任意一個(請參見代碼第24-26行)不論當前的action是什麼,該onTouchEvent()返回的均是true(請參見代碼第118行);並且會在ACTION_UP時處理click事件。
    同理,若是這三個條件都不知足,該onTouchEvent()返回的是false。
    也請注意一個細節:
    View的clickable屬性視不一樣的子View有所差別
    好比:Button的clickable默認爲true,可是TextView的clickable屬性默認爲false。
    View的longClickable屬性默認爲false。
    固然,咱們能夠經過代碼修改這些默認的屬性。
    好比:setClickable()和setLongClickListener()能夠改變View的CLICKABLE和LONG_CLICKABLE屬性。
    除此之外,經過設置監聽器也可改變某些屬性。
    好比:setOnClickListener()會將View的CLICKABLE設置爲true;setOnLongClickListener()會將View的LONG_CLICKABLE設置爲true。

第三步:
返回Touch事件是否被消費,請參見代碼第52行

以上就爲View對於Touch事件的主要步驟。
在此我畫了一個簡單的流程圖,現結合該圖和剛纔的源碼分析對View的Touch事件處理流程作一個總結。

這裏寫圖片描述

  1. View處理Touch事件的整體流程
    dispatchTouchEvent()—>onTouch()—>onTouchEvent()—>onClick()
    Touch事件最早傳入dispatchTouchEvent()中;若是該View存在TouchListener那麼會調用該監聽器中的onTouch()。在此以後若是Touch事件未被消費,則會執行到View的onTouchEvent()方法,在該方法中處理ACTION_UP事件時若該View存在ClickListener則會調用該監聽器中的onClick()
  2. onTouch()與onTouchEvent()以及click三者的區別和聯繫
    2.1 onTouch()與onTouchEvent()都是處理觸摸事件的API
    2.2 onTouch()屬於TouchListener接口中的方法,是View暴露給用戶的接口便於處理觸摸事件,而onTouchEvent()是Android系統自身對於Touch處理的實現
    2.3 先調用onTouch()後調用onTouchEvent()。並且只有當onTouch()未消費Touch事件纔有可能調用到onTouchEvent()。即onTouch()的優先級比onTouchEvent()的優先級更高。
    2.4 在onTouchEvent()中處理ACTION_UP時會利用ClickListener執行Click事件。因此Touch的處理是優先於Click的
    2.5 簡單地說三者執行順序爲:onTouch()–>onTouchEvent()–>onClick()
  3. View沒有事件的攔截(onInterceptTouchEvent( )),ViewGroup纔有,請勿混淆

關於View對Touch事件的處理就分析到此。

滴滴,到站了,下車的乘客們請日後門走。

PS:若以爲文章太長,那就直接看視頻吧。


who is the next one? ——> 詳解ViewGroup的Touch事件分發

相關文章
相關標籤/搜索