編輯:關於Android編程
上一篇我們主要主要是從ViewGroup分發的角度測試了下事件分發機制,但沒有涉足多少View的事件分發,也就是說我們沒有為MyRelativeLayout、MyLinearLayout、以及MyButton設置Touch和Click監聽事件,這一篇將來測試下View的事件分發過程,為了比較簡潔的顯示打印信息,我簡化了布局文件,具體的布局文件代碼如下:
也即布局文件圖是醬紫的:
具體的測試代碼就是在MainActivity和MyButton中的dispatchTouchEvent以及onTouchEvent方法中打印Log,以及在MyRelativeLayout的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent中打印Log,並且為MyRelativeLayout、MyButton設置了onTouchListener、onLongClickListener、以及onClickListener事件監聽器;
我們點擊MyButton按鈕,查看Logcat輸出結果如下:
06-30 10:27:37.127: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN 06-30 10:27:37.127: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN 06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN 06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN--->false 06-30 10:27:37.132: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN 06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouch--->DOWN 06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN 06-30 10:27:37.144: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN--->true 06-30 10:27:37.144: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN--->true 06-30 10:27:37.144: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN--->true 06-30 10:27:37.144: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN--->true 06-30 10:27:37.689: I/System.out(2705): MyButton--->onLongClick 06-30 10:27:37.832: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP 06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP 06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP 06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP--->false 06-30 10:27:37.832: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP 06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouch--->UP 06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP 06-30 10:27:37.842: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP--->true 06-30 10:27:37.842: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP--->true 06-30 10:27:37.842: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP--->true 06-30 10:27:37.842: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP--->true 06-30 10:27:37.852: I/System.out(2705): MyButton--->OnClick
如果你仔細查看輸出的話,有一部分會讓你覺得很奇怪的,就是輸出的第12行的onLongClick方法和第24行的onClick方法是在dispatchTouchEvent方法執行結束之後才開始執行的,這一點讓我感到很詫異,所以專門寫了這篇博客來試著從代碼層面解釋下這種現象的原因,因為網上看別人的分析過程均沒有涉足到我想要的部分,所以打算自己分析一次View分發過程的源碼,有什麼錯誤還請指正,源碼分析結束之後我們再來看看Logcat輸出或許你會明白點了;
一個事件傳遞到View上面首先執行的就是他的dispatchTouchEvent方法,那麼很自然首先應該從View的dispatchTouchEvent開始分析:
public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }
第6行判斷是否過濾掉當前事件系列,什麼情況下會被過濾呢?View在被遮蓋的時候,onFilterTouchEventForSecurity方法會返回true,進而直接在第22行dispatchTouchEvent返回了false;如果當前View沒有被遮蓋的話,執行7--16行的if語句塊,首先獲取到ListenerInfo對象,他是View的靜態內部類,這個對象主要存儲的就是一些我們所設置的事件監聽器了,稍微看看裡面的幾個屬性字段:
static class ListenerInfo { public OnClickListener mOnClickListener; protected OnLongClickListener mOnLongClickListener; private OnTouchListener mOnTouchListener; }接著走到第9行的if判斷語句處,這個地方有四個判斷條件,第1個li指的就是ListenerInfo對象,第2個li.mOnTouchListener其實是在判斷是否設置Touch事件監聽器,具體li.mOnTouchListener的值等於什麼呢?從下面代碼中可以看出來:
public void setOnTouchListener(OnTouchListener l) { getListenerInfo().mOnTouchListener = l; }這是一個public類型的方法,我們通常在程序中為某個控件設置Touch監聽器就是調用的這個方法,那麼其實第2個判斷條件是在查看我們是否有設置Touch事件監聽器,第三個條件是在查看我們當前的View是否是enable的,也就是說當前View本身是否能夠接受觸摸事件,第4個就是onTouch方法的返回值了,這個方法可以被重寫,默認情況下是返回false的;如果這個if判斷的四個條件都滿足的話,執行11行,直接返回,也就是當前事件已經分發結束了,從這裡可以看出View事件分發首先執行的是onTouch(當然你必須設置Touch事件監聽器);如果if的四個條件中有一個是false,就會執行第14行的if語句,調用onTouchEvent來處理事件,這裡我們有必要來看看onTouchEvent方法了;
該方法是public修飾的,所以你可以在子類中重寫它,方法比較長,我們截段分析:
if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); }
首先判斷當前View如果本身就不支持觸摸的話,進入if語句塊,第2行判斷當前事件是UP並且設置了PFLAG_PRESSED標志的話,則調用setPressed將標志置位,因為整個事件的最後一步就是UP了,所以我們必須在事件結束之前將設置的標志還原;
public void setPressed(boolean pressed) { final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED); if (pressed) { mPrivateFlags |= PFLAG_PRESSED; } else { mPrivateFlags &= ~PFLAG_PRESSED; } if (needsRefresh) { refreshDrawableState(); } dispatchSetPressed(pressed); }這裡傳給setPressed的參數是false,所以執行7行代碼取反還原;回到onTouchEvent方法中,第7行查看View是否有設置點擊和長點擊,有的話返回true,沒有返回false,從這裡可以看出onTouchEvent的返回值是跟你View是enable還是disable沒有多大關系,只要你設置了clickable或者longClickable,那麼他就會返回true;
接著分析onTouchEvent下面代碼:
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }查看是否有設置事件代理,有的話,則將事件交給代理處理,根據代理事件onTouchEvent方法來判斷是否返回true;
接下來的onTouchEvent代碼比較長,我們先來整理一個大體框架:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { ............... ............... ............... return true; } return false;可以看到只要clickable和longClickable有一個被設置就會返回true,只有在兩者都沒設置的情況下才會返回false,這也更加印證了onTouchEvent方法的返回值只和你有沒有設置clickable和longClickable有關,和View的enable和disable沒什麼關系;
如果clickable和longClickable有一個被設置,那麼進入if語句塊中,該語句塊是一個switch語句,我們按照事件的觸發順序來進行分析,即DOWN--->MOVE--->UP:
先來看DOWN部分:
case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true); checkForLongClick(0); } break;剛進來首先設置mHasPerformedLongPress的值為false,這個比較關鍵了,用來表示是否有執行長點擊事件,如果有設置長點擊事件並且onLongClick方法返回true的話,這個值是會被改變成true的,等會你就看到什麼原因啦,接著第9行判斷當前View是否在正在滾動的控件中,在的話就滿足第13行的if條件語句調用postDelay方法來延期press的反饋,為什麼要這麼做呢?從第11行的注釋看出來是為了防止當前事件是一個滾動事件,進入if語句塊之後執行第14行,設置PREPRESSED標志,這個標志表示的是prepressed狀態,這個狀態存在於ACTION_DOWN和真正意識到是press之間,用於識別是不是tap事件,接著第18行執行了postDelayed方法,參數ViewConfiguration.getTapTimeout()的值是150ms,這個方法的代碼如下:
public boolean postDelayed(Runnable action, long delayMillis) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.postDelayed(action, delayMillis); } // Assume that post will succeed later ViewRootImpl.getRunQueue().postDelayed(action, delayMillis); return true; }可以看到這個方法其實是調用了handler的postDelayed方法,將action加入到了MessageQueue消息隊列中,熟悉handler機制的應該知道隨後調用的將是action的run方法了,也就是mPendingCheckForTap的run方法了,mPendingCheckForTap是CheckForTap類型的對象,具體定義如下:
private final class CheckForTap implements Runnable { public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; setPressed(true); checkForLongClick(ViewConfiguration.getTapTimeout()); } }run方法首先是將PREPRESSED標志置位,接著執行setPressed方法,設置PRESSED標志,這個方法在前面又出現過,只不過前面調用的是setPressed(false)而已;接著便調用checkForLongClick來查看是否有長點擊事件了,傳入的參數是150ms;如果當前View不在滾動的控件中的話,則直接執行第19行的else語句,接著調用setPressed以及checkForLongClick方法,這裡執行的內容就和CheckForTap的run方法一致了,只不過傳入的checkForLongClick參數值不同而已,那麼我們就該看看checkForLongClick方法了:
private void checkForLongClick(int delayOffset) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }該方法第2行判斷你有沒有設置longClick事件,有的話進入if語句塊,首先還是將mHasPerformedLongPress設置為false,接著第9行同樣調用了postDelayed方法,傳入的第二個參數是ViewConfiguration.getLongPressTimeout()-delayOffset,ViewConfiguration.getLongPressTimeout()的默認值是500ms,從這句話我們可以看出來不管你是通過checkForLongClick(0)還是checkForLongClick(delayOffset)其中delayOffset大於0,調用checkForLongClick方法,其實檢測你是不是長點擊的時間是一致的,都是500ms,你點擊的時間超過500ms的話,會認為是長點擊,很自然調用的是mPendingCheckForLongPress的run方法:
public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { mHasPerformedLongPress = true; } } }注意到這個方法第2行會判斷isPressed(),什麼意思呢?就是說如果你500ms之後還是處於點擊狀態,那麼你就是長點擊了,執行if語句塊中的內容,第4行執行的是performLongClick方法,而這個方法就主要是執行的我們的OnLongClickListener監聽方法了,來看看裡面的代碼:
public boolean performLongClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } if (!handled) { handled = showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }同樣OnLongClickListener存儲在ListenerInfo對象裡面,第7行執行了onLongClick方法,並且獲取到返回值,回到前面的run方法第4行會判斷這個返回值是true的話,執行mHasPerformedLongPress=true語句,我們有必要說明下mHasPerformedLongPress
從DOWN事件的處理中,我們可以知道longclick是在它裡面進行檢測的,並且如果500ms之後還處於press狀態的話會調用它的performLongClick,而這個方法是在子線程中調用的,所以就出現了我們上面Log輸出第12行在dispatchTouchEvent返回之後才執行的結果;
接下來分析的是MOVE事件:
case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break;MOVE相對來說比較簡單,首先是獲取你當前觸摸處的位置,接著第6行判斷你觸摸的地方是否處於當前View的邊界內,不處於的話會執行7--15行代碼,處於的話不做任何事,我們來看看不處於情況下做了些什麼,首先執行第8行的removeTapCallback方法,這個方法:
private void removeTapCallback() { if (mPendingCheckForTap != null) { mPrivateFlags &= ~PFLAG_PREPRESSED; removeCallbacks(mPendingCheckForTap); } }主要是置位PREPRESSED標志,並且從當前的MessageQueue消息隊列中移出封裝有mPendingCheckForTap這個線程的消息,為什麼要這麼做呢?因為你都已經不在我當前View的控制范圍內了,我也沒必要看你接下來的一些操作了;第9行如果我們設置了PRESSED標志的話,說明在DOWN事件中也在MessageQueue裡面添加了封裝有監聽長點擊事件的Message,那麼就需要調用第11行的removeLongPressCallback方法,將該Message從MessageQueue中移出,並且13行調用setPressed方法將PRESSED標志置位;
接下來就是UP事件了:
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;UP事件代碼相對來說比較多,第3行的判斷條件說明只要你在MOVE的過程中沒有移出邊界都會滿足,接著第19行會判斷mHasPerformedLongPress的值,這個值只有在你設置了longclick監聽事件,並且在onLongClick方法中返回true的情況下才會是true,否則均是false,這個在上面已經說過了,我們假定這裡mHasPerformedLongPress的值是false,進入if語句塊,首先調用removeLongPressCallback,從MessageQueue中移出長點擊監聽Message,接著第28--33行的代碼比較關鍵,這裡將是解釋我們上面Log輸出的重要部分,如果沒有PerformClick對象則創建,並在第31行通過post方法將PerformClick對象添加到MessageQueue消息隊列中,接下來將是執行PerformClick的run方法了:
private final class PerformClick implements Runnable { public void run() { performClick(); } }
很明顯PerformClick是一個線程,在他的run方法裡面也會執行performClick,也就是說不管第31行post方法有沒有執行成功都會執行performClick方法的,那麼這裡為什麼要用到post通過子線程來執行performClick而不是直接執行performClick呢?根據官方的注釋看到這樣做的目的是為了在click執行之前讓view上面的其他visual 狀態能夠更新,來看看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; }可以看到如果我們設置了OnClickListener監聽器的話,會執行第7行代碼,也就執行了我們的onclick方法了;因為如果我們調用post的話,performClick是執行在PerformClick類型的子線程中的,所以我們上面的Log輸出會出現onClick方法在dispatchTouchEvent事件返回之後才執行的情況了;UP事件後面的一些操作是用於狀態置位的,我們再次不做過多牽涉;
這樣的話,View的事件分發源碼分析完畢了,我們做個小結以此來解釋上面的Log輸出:
(1)View中如果我們設置了onTouchListener、onLongClickListener以及onClickListener的話,三者的執行順序是onTouch--->onLongClick--->onClick;
(2)如果我們在onLongClick方法中返回true的話,那麼隨後的onClick方法將不再會執行;
(3)我們的onLongClick方法以及onClick方法可能會在dispatchTouchEvent方法返回之後才去執行,原因在於onLongClick方法是在CheckForLongPress類型的子線程中執行的,onClick是在PerformClick類型的子線程中執行的,也即解釋了上面Log輸出第12行出現在第9行之後,以及第24行出現在第21行之後的問題;
好了,這篇先到這裡了,下篇從實例測試的角度進行不同情況下的分析;
本文實例講述了Android編程使用android-support-design實現MD風格對話框功能。分享給大家供大家參考,具體如下:首先上效果圖: 測試設備
概述:常用的布局類型並不能滿足所有需求,這時就會用到ViewGroup。ViewGroup作為一個放置View的容器,並且我們在寫布局xml的時候,會告訴容器(凡是以la
日期顯示和選擇類庫,可以用來選擇一段連續的和多個不連續的日期,具體的UI完全抽象出來了,可以高度自定義(GITHUB地址)支持的功能:1、選擇一段連續的和多個不連續的日期
實現效果如下:實現思路:1、如何實現圓中水面上漲效果:利用Paint的setXfermode屬性為PorterDuff.Mode.SRC_IN畫出進度所在的矩形與圓的交集