Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義View系列教程06--詳解View的Touch事件處理

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

編輯:關於Android編程

先上圖:

\

說在前面:

View的事件分發簡單記憶方法::dispathTouchEvent----->onTouchEvent------->onClick

如上圖,我把View的事件分發分為兩大塊:

第一塊:在dispatchTouchEvent()方法中。

1 首先判斷當前的OnTouchListener是否為null。

2 判斷當前的控件是否是ENABLED狀態。
3 判斷onTouch方法返回的是true還是false。
如果以上三步:有任何一個步驟返回false。那麼就調用onTouchEvent(onTouchEvent返回true,則dispathTouchEvent 返回true;返回false,則dispathTouchEvent 返回false。)

所有步驟都返回true。不調用其他方法,dispathTouchEvent ()返回true;

第二塊:在onTouchEvent方法中。

1.當控件不可用時:

當控件有點擊事件,返回true,但不會調用onClick等點擊事件

當空間無點擊事件,返回false

2.當控件可用時:

無點擊事件,返回false

有點擊事件,就走Switch()判斷,判斷是move,down,up,cancle,最後返回true

在up時,會調用preformClick()------>onClick()事件。

附錄:

 

           

	       //iamgeView調用view上面setOnTouchListener方法。
		imageView.setOnTouchListener(new OnTouchListener() {
			@Override
			public boolean onTouch(View v, MotionEvent event) {
				//返回true,導致事件全部被響應
				return false;
			}
		   });

		//給指定的iamgeView去設置一個點擊事件
		imageView.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				...
			  }
		   });

		//點擊事件的處理規則,mOnClickListener什麼時候傳遞進來的??
		 public boolean performClick() {
			...	
	              if (mOnClickListener != null) {
	                   ....
	                     mOnClickListener.onClick(this);
	                   ...
	                }
	                   ...
	             }
//view中setOnClickListener方法 public void setOnClickListener(OnClickListener l) { //如果當前控件沒有點擊事件,設置一個點擊事件 if (!isClickable()) { setClickable(true); } mOnClickListener = l; }

 


--------------------------------我是華麗的分割線-----------------------ok請看正文---------------------------------------------------------------------------------------

 

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

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

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

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


如果一個View(比如Button)接收到Touch,那麼該Touch事件首先會傳入到它的dispatchTouchEvent( )方法,所以我們從這裡開始學習View對Touch事件的處理。

/**
     * 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;
    }
這段代碼稍微復雜一些,在此分析幾個核心點。

 

當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( )方法

處理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時調用的。

返回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事件處理流程做一個總結。

這裡寫圖片描述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()
View沒有事件的攔截(onInterceptTouchEvent( )),ViewGroup才有,請勿混淆


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

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

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