編輯:關於Android編程
??把android開發藝術探索第三章閱讀了,對事件分發機制有一個大概的了解,關於事件分發的文章也很多,自己也看了一些相關的文章,決定自己分析一遍記錄下來,加深映象和對這個機制的了解。
View的事件分發過程
我們直接分析代碼吧,前面對事件分發的一些結論在上一篇文章Android開發藝術探索第三章 讀書筆記已經提到了,不了解的可以先去看一下,帶著問題來分析源碼。我們先從View的dispatchTouchEvent方法入手,android中的所有事件都是經過dispatchTouchEvent 來分發的,我們通過返回值來決定是否自己處理還是傳分發給子控件:
public boolean dispatchTouchEvent(MotionEvent event) { // If the event should be handled by accessibility focus first. if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (!isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement 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); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
代碼很多,我們慢慢分析,2-10行首先判斷了當前事件是否可以獲取焦點,如果不能獲取焦點或者找不到一個view,直接返回false,緊接著12-22行設置一些標記位和input手勢傳遞等,然後這裡是關鍵
if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement 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; } }
onFilterTouchEventForSecurity(event) 判斷view是否被遮住,然後判斷view是不是enable的,判斷handleScrollBarDragging(event)事件是否為滾動條拖動,是則為true,如果上面條件都為真那這裡直接返回true,接著定義了ListenerInfo 的局部變量,接著就是if裡面幾個條件判斷,可以說是關鍵點,首先li != null 這個自然不會為null,在代碼裡面可以看出:
ListenerInfo getListenerInfo() { if (mListenerInfo != null) { return mListenerInfo; } mListenerInfo = new ListenerInfo(); return mListenerInfo; }
接著li.mOnTouchListener != null 這個屬性為不為null,我們可以在源碼裡看到:
/** * Register a callback to be invoked when a touch event is sent to this view. * @param l the touch listener to attach to this view */ public void setOnTouchListener(OnTouchListener l) { getListenerInfo().mOnTouchListener = l; }
這個主要取決於我們系統view是否setOnTouchListener ,設置了就不為null;第三個條件(mViewFlags & ENABLED_MASK) == ENABLED 判斷view是不是enable的,默認都是enable的;第四個條件li.mOnTouchListener.onTouch(this, event) 這裡主要判斷onTouch的返回值,如果onTouch消費了這個事件,則返回true,那麼這裡直接就返回true了,就不會執行下面這個方法了
if (!result && onTouchEvent(event)) { result = true; }
所以onTouchEvent的執行與否跟onTouch的返回值有很大關系,有時候我們在對view的onTouch裡面返回true,會發現這個view的點擊事件沒效果,點擊事件是不是就在onTouchEvent 方法裡面,我們接著分析,不過我們可以得出一個結論,就是在dispatchTouchEvent 方法裡面首先執行onTouch 方法,如果if裡面所有條件滿足則dispatchTouchEvent 返回值返回true,並不執行onTouchEvent 方法,如果控件不是enable或者mOnTouchListener 返回null,或者onTouch 返回false,那麼就會執行下面的onTouchEvent 方法,那麼dispatchTouchEvent 返回值方法跟onTouchEvent 方法返回值一樣,我們接著分析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); } // 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) || (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) { // 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, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 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(); } mIgnoreNextUpEvent = false; break; 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(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // 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; } return true; } return false; }
這個代碼比上面的更長,我們還是只分析主要的,第2-16行可以看出,如果view是DISABLED 狀態,只要滿足下面三種條件之一那麼這個view雖然被禁用了,但是滿足這三個之一我們還是會消費這個事件,只是不響應它們而已。
// 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) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
17 - 21行,如果view設置了代理,那麼還會執行mTouchDelegate 的onTouchEvent 方法。那麼接下來23行可以看見,一個view是enable且滿足三個狀態之一則進入swift判斷中去,反之則返回false。我們首先看看ACTION_DOWN ,mHasPerformedLongPress = false; 設置了一個長按的標記位,然後
if (performButtonActionOnTouchDown(event)) { break; }
一般的設備都是false,這是一個處理鼠標右鍵的事件。接下來的所有方法都是判斷當前view是否在一個滾動的view容器內,避免把滑動當成一次點擊事件,然後根據判斷的結果查看是否在一個滾動容器內,檢查的方法實現代碼如下:
public boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; }
遍歷整個View樹,通過這個方法來判斷shouldDelayChildPressedState 是否是一個滾動的布局,這個方法不能滾動的布局都重寫了並返回false,能滾動的都是返回true,判斷的結果如果在滾動容器內,先設置一個mPrivateFlags |= PFLAG_PREPRESSED; 准備點擊的標記位,然後在發送一個延遲100ms的消息確定用戶是要滾動還是點擊。先實例化一個mPendingCheckForTap ,然後將它加入到一個消息隊列延遲執行,我們看看這個CheckForTap 實例,
private final class CheckForTap implements Runnable { public float x; public float y; @Override public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; setPressed(true, x, y); checkForLongClick(ViewConfiguration.getTapTimeout(), x, y); } }
裡面設置了按下事件,然後檢查延遲發送一個100ms消息看看是不是長按事件,在給定的時間內如果沒有移動那麼就當做用戶是想點擊,而不是滑動,將 mPendingCheckForTap添加到消息隊列中,延遲執行。如果在這TapTimeout之間用戶觸摸移動了,取消了什麼,則移除此消息。後面會有很多移除這些事件的。否則:執行按下狀態.然後檢查長按.我們看看checkForLongClick 這個函數
private void checkForLongClick(int delayOffset, float x, float y) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }
檢查長按思路等等看看是不是長按事件,主要調用的還是CheckForLongPress 這個類中的方法,我們看一下
private final class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; private float mX; private float mY; @Override public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick(mX, mY)) { mHasPerformedLongPress = true; } } } public void setAnchor(float x, float y) { mX = x; mY = y; } public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; } }
主要是根據view的mWindowAttachCount 方法統計view的attach到window的次數,檢查長按的時候attach次數和長按到形成的attach次數一致的話,則認為是一個長按處理,裡面則執行了一個長按的事件,長按事件就是在這個performLongClick(mX, mY) 方法裡面執行的,我就不繼續深入了,明白一點就是onLongClick事件只要你在長按識別的時間內檢查長按的標志位為true,就會執行,跟click事件不一樣,click事件是在手指抬起後執行,後面分析;不一樣的話,則界面可能發生了其它事情,暫停或者重新啟動造成了界面重新刷新,那長按自然應該不執行。
如果不是在一個滾動容器內,則調用setPressed(true, x, y); 設置一個按下狀態,然後在檢查長按狀態,這裡跟上面長按機制差不多,不在分析。
我們看看ACTION_UP 事件,if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) 判斷了不是在一個滾動操作的容器中,已經可以確定這是一個按下狀態,然後
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); }
三個條件分別是當前view是否能夠獲取焦點,觸摸是否能夠獲得焦點,當前view還沒有獲取焦點,就請求獲取一個焦點。
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, x, y); }
接著就是一個判斷用戶在按下沒有效果之前就不按了,我們還是要進行實際的操作前讓用戶看到一個效果。
接著判斷用戶是否進行了長按,如果沒有移除相關檢測,mHasPerformedLongPress 這個是一個標志位,根據這個判斷是不是一個長按事件,長按事件發生後會將這個標志位設置為true,不是則移除掉長按的延遲消息。
// 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 (!post(mPerformClick)) 然後就是使用post到主線程中執行一個performClick的Runnable,而不是直接執行一個點擊事件,為了讓用戶感覺到一個按下的狀態,我們看看這個重點吧,就是performClick() 方法:
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; }
這個方法和上面執行4個判斷的if語句有點像,先定義一個變量後賦值,然後判斷li.mOnClickListener != null ,這個在哪裡可以看到呢,自然也是在setOnClickListener 裡面賦值的,其實view的Clickable
屬性是否為false,也和具體的view有關系,可以點擊的一般就是true,不可點擊的一般就為false。可以通過調用 setClickable`方法設置,如果調用了點擊事件,那麼都會自動的設為true。
public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; }
然後點擊下執行一個click事件,result返回true,這個事件就被消費了,至此,也就明白了OnClick是在onTouchEvent裡面執行的,只要一個控件滿足clickable,並且設置了監聽,那麼都會執行這個點擊事件。後面就是一個按下的效果顯示時間,由ViewConfiguration.getPressedStateDuration() 這個常量指定,也就是64ms,然後將設置的按下的狀態設置為false。
最後就是還有一個ACTION_CANCEL 這個比較容易理解,就是setPressed(false); 然後移除各種延遲發送的消息。那麼這一部分的源碼大概就分析完了,我們可以得出一個結論,就是點擊事件是在onTouchEvent方法中的ACTION_UP中執行一個click事件,並且我們每次執行一個action,只有前一個action返回true,才會執行下一個action,因為前一個action返回了false,那麼dispatchTouchEvent 將不會派發下一次事件。
我們分析了這麼多,我們下面來簡單的驗證一下,對上述分析來一個驗證。
例子public class TestButton extends Button{ private static final String TAG = "TestButton"; public TestButton(Context context) { super(context); } public TestButton(Context context, AttributeSet attrs) { super(context, attrs); } public TestButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); return super.onTouchEvent(event); } }
public class TestActivity extends AppCompatActivity implements View.OnClickListener,View.OnTouchListener,View.OnLongClickListener{ private TestButton button; private static final String TAG = "TestActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); button = (TestButton) findViewById(R.id.btn); button.setOnClickListener(this); button.setOnTouchListener(this); button.setOnLongClickListener(this); } @Override public void onClick(View v) { Log.d(TAG, "onClick: " + v); } @Override public boolean onTouch(View v, MotionEvent event) { Log.d(TAG, "onTouch: " + event.getAction()); return false; } @Override public boolean onLongClick(View v) { Log.d(TAG, "onLongClick: " + v); return false; } }
現象分析
2.1 我們先不更改任何事件返回,點擊button如下:
發現所有事件都得到了正常派發,我沒有移動所以沒有派發move事件,先執行了長按事件,然後執行了點擊事件。
我們現在修改TestActivity中的onLongClick :
@Override public boolean onLongClick(View v) { Log.d(TAG, "onLongClick: " + v); return true; }
我們返回true,其它代碼不改動。現在看看點擊事件的派發:
我們改為true,前面都一樣,只是沒有執行點擊事件,這是為什麼呢,因為true就是消費了這個事件,我們在ACTION_UP 裡面對長按做了判斷,如果長按事件發生了並返回為true,就不會去執行點擊事件的,上面源碼有做分析,這個比較常用,各位老司機也是輕車熟路。
2.2 我們簡單的修改TestButton的dispatchTouchEvent返回為false,其它代碼不變:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); return false; }
點擊button後的事件如下:
你發現點擊後任何事件都沒有得到觸發。
我們在改為false的基礎上調用super父類的方法,其它不變:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); super.dispatchTouchEvent(event); return false; }
點擊button後的事件如下:
發現跟上面執行的有點不一樣的,返回false不執行下一次派發,但是執行了長按事件,這是為什麼呢,因為我們上面分析過我們按下的時候就延遲發送消息判斷是不是長按事件,只要點擊了一下我們就發送了消息,但是後續事件沒有得到派發,也就沒有移除長按的消息,所以就可以得到執行。
2.3 我們修改TestButton的dispatchTouchEvent返回值為true,不調用super父類方法:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); return true; }
點擊button如下:
你會發現雖然派發了事件,但是沒有執行任何方法,比如onTouchEvent啊,onTouch什麼的,不調用super任何方法都不執行。
我們在上面的基礎上調用super方法,其它不變:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.d(TAG, "dispatchTouchEvent:" + event.getAction()); super.dispatchTouchEvent(event); return true; }
點擊button後的事件如下:
我們發現跟正常派發的事件機制一樣。
2.4 我們修改TestButton的返回值為false:
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); return false; }
點擊button如下
發現返回了false,也就是dispatchTouchEvent返回false,那麼將不派發下一次事件。
我們在上面基礎上添加調用父類的super方法:
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); super.onTouchEvent(event); return false; }
點擊button後的點擊事件如下,:
跟上面差不多,只不過執行了長按事件,上面分析過了,不在說明。
2.5我們修改TestButton的onTouchEvent返回true:
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); return true; }
點擊button後如下:
你會發現所有事件都得到正常派發,只是沒有執行點擊事件,沒用調用super方法。
在上面的基礎上調用super方法:
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent:" + event.getAction()); super.onTouchEvent(event); return true; }
點擊button後如下:
發現所有事件都得到了正常派發和執行。
這篇文章就講完了,主要講的是view的事件分發,其中有些還是沒講到,關於view的知識也比較多,其中難免會有錯誤,歡迎大家提出來。
本文實例講述了Android通過json向MySQL中寫入數據的方法。分享給大家供大家參考,具體如下:先說一下如何通過json將Android程序中的數據上傳到MySQL
前言上一篇寫了補間動畫的使用,由於篇幅原因,就把自定義補間動畫單獨拿出來了。這一篇繼續寫補間動畫~在上一篇中寫到了Android提供了Animation類作為補間動畫的抽
這裡講一下React Native中的一個組件——ActivityIndicator,這是一個加載指示器,俗稱菊花,很常見的,效果如下所示:imp
SlidingPaneLayoutSlidingPaneLayout是Android在android-support-v4.jar中推出的一個可滑動面板的布局,我們提到水