編輯:關於Android編程
理解事件的分發機制,需要對View和ViewGroup事件的分發分別探討。View和ViewGroup的區別,一個View控件是指它裡面不能再包含子控件了,常見的如TextView、Button、ImageView等,而ViewGroup是繼承自View的,但是它裡面可以包含一些子控件,包括View或者嵌套的ViewGroup,常用的大部分布局都是ViewGroup組件,如LinearLayout、RelativeLayout、FrameLayout等。
首先要明白的是,當我們觸摸一個控件時(不論是View還是ViewGroup),都會調用dispatchTouchEvent()方法,開始事件的分發處理。我們先自定義一個簡單的線性布局:
public class MyLinearLayout extends LinearLayout { public MyLayout(Context context, AttributeSet attrs) { super(context, attrs); } }
運行後,我們點擊Button控件,當事件傳遞到Button時會調用Button的dispatchTouchEvent方法(Button本身並沒有dispatchTouchEvent方法,往上最終尋找到其父類View的dispatchTouchEvent方法)。根據dispatchTouchEvent源碼來分析其處理流程:
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { //第一步 return true; } return onTouchEvent(event); //第二步 }第一步:首先進行三個條件的判斷:
如果條件都滿足,則該事件被消耗掉,不再進入onTouchEvent中處理。
第二步:上述三個條件不同時滿足時,事件將交給onTouchEvent方法處理。再根據onTouchEvent源碼分析其處理流程:
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { // 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)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {//第一點 switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; if ((mPrivateFlags & 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 (!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) { mPrivateFlags |= PRESSED; refreshDrawableState(); postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); 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 = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } return true; } return false; }源碼很長,我們只需關注重要的幾點即可。
第一點:這裡有一個長長的if語句
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))用於判斷該View是否是可點擊的或是否可長按的View,明顯我們的Button屬於可點擊的View控件。進入到if裡面後轉入到switch中,當執行完switch語句後,直接執行
return true;呵呵,說明什麼?只要是該控件是可點擊的或者可長按的View,這個事件就會被消耗掉!這也符合我們的認知,按鈕Button之類的不就是讓人來點擊處理的麼,但是對於那些TextView、ImageView之類的非可點擊控件,我們平常不是也能夠處理點擊事件嗎?回憶一下,我們在處理這些點擊事件的時候,一定通過setOnClickListener()給它設置了點擊監聽OnClickListener(或者在布局中聲明了android:clickable="true"),setOnClickListener源碼如下:
public void setOnClickListener (OnClickListener l) { if (!isClickable()) { setClickable( true); } mOnClickListener = l; }可知,設置了點擊事件或長按事件的控件自動變成了CLICKABLE 或LONG_CLICKABLE的狀態;
第二點:我們設置的onclick事件是在手指抬起ACTION_UP的時候執行的。
到這,我們可以總結一下,View事件的處理流程。對於View來說,事件首先進入到dispatchTouchEvent方法中進行分發處理,在dispatchTouchEvent中首先查看該View是否設置了OnTouchListener事件並且實現的監聽中的onTouch方法的返回值是否為true,如果滿足,這個事件就到此被消耗,不再往下處理;如果條件不滿足,則進入到onTouchEvent方法中進行處理,在onTouchEvent方法中,先檢查該View是否是可點擊或長按的(設置監聽、布局中設置android:clickable),如果是則該事件被消耗。
可以看出,onTouch()方法的執行優先於onTouchEvent(),onTouch()方法的返回值決定了能否執行到onTouchEvent()方法,dispatchTouchEvent()方法的返回值,依賴於OnTouchListener的onTouch()方法或者onTouchEvent()方法。
繼續以我們上面那個自定義布局為例,當我們點擊button時,事件其實是先到我們自定義的MyLinearLayout中分發的,同樣,首先進入MyLinearLayout的dispatchTouchEvent(LinearLayout中本身也沒有dispatchTouchEvent,最終找到其父類ViewGroup的dispatchTouchEvent)方法中進行分發,根據dispatchTouchEvent源碼進行分析:
public boolean dispatchTouchEvent(MotionEvent ev) { final int action = ev.getAction(); final float xf = ev.getX(); final float yf = ev.getY(); final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; //這個值默認是false, 然後我們可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法 //來改變disallowIntercept的值 boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //這裡是ACTION_DOWN的處理邏輯 if (action == MotionEvent.ACTION_DOWN) { //清除mMotionTarget, 每次ACTION_DOWN都很設置mMotionTarget為null if (mMotionTarget != null) { mMotionTarget = null; } //disallowIntercept默認是false, 就看ViewGroup的onInterceptTouchEvent()方法 if (disallowIntercept || !onInterceptTouchEvent(ev)) { //第一點 ev.setAction(MotionEvent.ACTION_DOWN); final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; //遍歷其子View for (int i = count - 1; i >= 0; i--) { //第二點 final View child = children[i]; //如果該子View是VISIBLE或者該子View正在執行動畫, 表示該View才 //可以接受到Touch事件 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { //獲取子View的位置范圍 child.getHitRect(frame); //如Touch到屏幕上的點在該子View上面 if (frame.contains(scrolledXInt, scrolledYInt)) { // offset the event to the view's coordinate system final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; //調用該子View的dispatchTouchEvent()方法 if (child.dispatchTouchEvent(ev)) { // 如果child.dispatchTouchEvent(ev)返回true表示 //該事件被消費了,設置mMotionTarget為該子View mMotionTarget = child; //直接返回true return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } } //判斷是否為ACTION_UP或者ACTION_CANCEL boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { //如果是ACTION_UP或者ACTION_CANCEL, 將disallowIntercept設置為默認的false //假如我們調用了requestDisallowInterceptTouchEvent()方法來設置disallowIntercept為true //當我們抬起手指或者取消Touch事件的時候要將disallowIntercept重置為false //所以說上面的disallowIntercept默認在我們每次ACTION_DOWN的時候都是false mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; //mMotionTarget為null意味著沒有找到消費Touch事件的View, 所以我們需要調用ViewGroup父類的 //dispatchTouchEvent()方法,也就是View的dispatchTouchEvent()方法 if (target == null) { // We don't have a target, this means we're handling the // event as a regular view. ev.setLocation(xf, yf); if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } return super.dispatchTouchEvent(ev); } //這個if裡面的代碼ACTION_DOWN不會執行,只有ACTION_MOVE //ACTION_UP才會走到這裡, 假如在ACTION_MOVE或者ACTION_UP攔截的 //Touch事件, 將ACTION_CANCEL派發給target,然後直接返回true //表示消費了此Touch事件 if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; ev.setAction(MotionEvent.ACTION_CANCEL); ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { } // clear the target mMotionTarget = null; // Don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal onTouchEvent(). return true; } if (isUpOrCancel) { mMotionTarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; mMotionTarget = null; } //如果沒有攔截ACTION_MOVE, ACTION_DOWN的話,直接將Touch事件派發給target return target.dispatchTouchEvent(ev); }ViewGroup的dispatchTouchEvent方法很長,主要看兩點:
第一點:首先通過onInterceptTouchEvent方法判斷該ViewGroup是否進行攔截該事件,默認該方法返回false,即不進行攔截,將事件分發給它的子View或子ViewGroup。很多時候,我們在自定義一些復雜控件時,我們可以重寫該方法,根據情況靈活處理其返回值。
第二點:
1、當onInterceptTouchEvent返回false不進行攔截時,if判斷條件成立,進入到if內,開始遍歷該ViewGroup的子ViewGroup或子View,將事件分發給子ViewGroup或子View的dispatchTouchEvent方法,在分發過程中如果中間的ViewGroup沒有進行攔截,Touch事件就會一直往下分發到手指按下的最裡面的View,這個時候,就會按照View事件的分發處理過程調用View的dispatchTouchEvent方法了,從而交給onTouchEvent方法進行處理,
(1)如果onTouchEvent返回true,也即該View的dispatchTouchEvent返回true,表示消耗掉了此事件,事件也就終止傳遞。
(2)如果onTouchEvent返回false,即不消費Touch事件,這個Touch事件就會向上找父布局調用其父布局的onTouchEvent,讓父布局處理。
2、當onInterceptTouchEvent返回true的時候,表示該ViewGroup需要進行攔截事件,此事件就交給該ViewGroup自己來處理,從而調用該ViewGroup的onTouchEvent方法。
可以看出,ViewGroup的事件傳遞實際上是分為兩步的:事件分發和事件處理,首先是Touch事件的分發,先從從頂層的View一直往下分發到手指按下的最裡面的View,到了這裡之後,判斷該View是否進行事件的處理(或消耗),如果不處理(false),就反過來一層一層往上交給父布局處理,如果消耗(true),就不會再交給父布局,事件終止。流程圖如下:
到這裡,我們總結一下,View和ViewGroup的事件分發和處理的總流程,當我們手指觸摸屏幕時,事件會傳入到我們編寫的布局文件的根布局上,如上面我們自定義的MyLinearLayout中,尋找MyLinearLayout的dispatchTouchEvent方法進行事件分發,由於LinearLayout中本身沒有該方法,就往上尋找到其父類ViewGroup的dispatchTouchEvent方法,往下分發前查看該ViewGroup的onInterceptTouchEvent方法判斷是否需要攔截掉該事件,如果不攔截遍歷其子ViewGroup或子View,直到碰到該往下分發過程中被某個ViewGroup攔截掉,或者最後分發到手指按下的最裡面的View,然後按照View的事件處理流程處理該事件。在ViewGroup事件分發過程中,會根據子View或者ViewGroup的dispatchTouchEvent方法的返回值決定是否繼續遍歷分發下去。
好了,到這裡事件分發的理論是差不多了,後面最重要的是多看看一些大神的自定義控件,慢慢在實戰中靈活掌握事件分發的機制。同時結合Android View和ViewGroup的繪制流程,融會貫通。
前言:工欲善其事,必先利其器,工作一段時間後,對於以上十個字的感觸是最深的。剛參加工作的時候,並沒有對於要做的事情有著自己的理解,經常是上面分配了工作,自己就乖乖地跑去做
簡介:為什麼要用Fragment?使用Fragment可以在一個Activity中實現不同的界面。Fragment與Fragment之間的動畫切換,遠比Activity與
1、為什麼需要異步加載。因為我們都知道在Android中的是單線程模型,不允許其他的子線程來更新UI,只允許UI線程(主線程更新UI),否則會多個線程都去更新UI會造成U
使用Eclipse開發Android已經有些年頭了,然而Android Studio(後面簡稱AS)為谷歌自己推的IDE。現在AS已經出了2.0版本,其功能的確要比Ecl