編輯:關於Android編程
上一篇已經完整的解析了Android View的事件分發機制,今天給大家代碼ViewGroup事件分發的源碼解析~~凡是自定義ViewGroup實現各種滑動效果的,不可避免的會出現很多事件的沖突,對ViewGroup事件分發機制的了解,也有益於大家了解沖突產生的原因,以及對沖突進行處理~
首先我們接著上一篇的代碼,在代碼中添加一個自定義的LinearLayout:
package com.example.zhy_event03; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.LinearLayout; public class MyLinearLayout extends LinearLayout { private static final String TAG = MyLinearLayout.class.getSimpleName(); public MyLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, dispatchTouchEvent ACTION_DOWN); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, dispatchTouchEvent ACTION_MOVE); break; case MotionEvent.ACTION_UP: Log.e(TAG, dispatchTouchEvent ACTION_UP); break; default: break; } return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, onTouchEvent ACTION_DOWN); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, onTouchEvent ACTION_MOVE); break; case MotionEvent.ACTION_UP: Log.e(TAG, onTouchEvent ACTION_UP); break; default: break; } return super.onTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, onInterceptTouchEvent ACTION_DOWN); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, onInterceptTouchEvent ACTION_MOVE); break; case MotionEvent.ACTION_UP: Log.e(TAG, onInterceptTouchEvent ACTION_UP); break; default: break; } return super.onInterceptTouchEvent(ev); } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { Log.e(TAG, requestDisallowInterceptTouchEvent ); super.requestDisallowInterceptTouchEvent(disallowIntercept); } }
然後看我們的布局文件:
然後MainActivity就是直接加載布局,沒有任何代碼~~~
直接運行我們的代碼,然後點擊我們的Button,依然是有意的MOVE一下,不然不會觸發MOVE事件,看一下日志的輸出:
09-06 09:57:27.287: E/MyLinearLayout(959): dispatchTouchEvent ACTION_DOWN 09-06 09:57:27.287: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_DOWN 09-06 09:57:27.287: E/MyButton(959): dispatchTouchEvent ACTION_DOWN 09-06 09:57:27.297: E/MyButton(959): onTouchEvent ACTION_DOWN 09-06 09:57:27.297: E/MyButton(959): onTouchEvent ACTION_MOVE 09-06 09:57:27.327: E/MyLinearLayout(959): dispatchTouchEvent ACTION_MOVE 09-06 09:57:27.327: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_MOVE 09-06 09:57:27.337: E/MyButton(959): dispatchTouchEvent ACTION_MOVE 09-06 09:57:27.337: E/MyButton(959): onTouchEvent ACTION_MOVE 09-06 09:57:27.457: E/MyLinearLayout(959): dispatchTouchEvent ACTION_UP 09-06 09:57:27.457: E/MyLinearLayout(959): onInterceptTouchEvent ACTION_UP 09-06 09:57:27.457: E/MyButton(959): dispatchTouchEvent ACTION_UP 09-06 09:57:27.457: E/MyButton(959): onTouchEvent ACTION_UP
MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent
可以看出,在View上觸發事件,最先捕獲到事件的為View所在的ViewGroup,然後才會到View自身~
下面我們按照日志的輸出,進入源碼~
ViewGroup - dispatchTouchEvent
首先是ViewGroup的dispatchTouchEvent方法:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { if (!onFilterTouchEventForSecurity(ev)) { return false; } 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; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { // this is weird, we got a pen down, but we thought it was // already down! // XXX: We should probably send an ACTION_UP to the current // target. mMotionTarget = null; } // If we're disallowing intercept or if we're allowing and we didn't // intercept if (disallowIntercept || !onInterceptTouchEvent(ev)) { // reset this event's action (just to protect ourselves) ev.setAction(MotionEvent.ACTION_DOWN); // We know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); 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; if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. mMotionTarget = child; return true; } // The event didn't get handled, try the next view. // Don't reset the event's location, it's not // necessary here. } } } } } ....//other code omitted代碼比較長,決定分段貼出,首先貼出的是ACTION_DOWN事件相關的代碼。
16行:進入ACTION_DOWN的處理
17-23行:將mMotionTarget置為null
26行:進行判斷:if(disallowIntercept || !onInterceptTouchEvent(ev))
兩種可能會進入IF代碼段
1、當前不允許攔截,即disallowIntercept =true,
2、當前允許攔截但是不攔截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false ;
注:disallowIntercept 可以通過viewGroup.requestDisallowInterceptTouchEvent(boolean);進行設置,後面會詳細說;而onInterceptTouchEvent(ev)可以進行復寫。
36-57行:開始遍歷所有的子View
41行:進行判斷當前的x,y坐標是否落在子View身上,如果在,47行,執行child.dispatchTouchEvent(ev),就進入了View的dispatchTouchEvent代碼中了,如果不了解請參考:Android View的事件分發機制,當child.dispatchTouchEvent(ev)返回true,則為mMotionTarget=child;然後return true;
ViewGroup的ACTION_DOWN分析結束,總結一下:
ViewGroup實現捕獲到DOWN事件,如果代碼中不做TOUCH事件攔截,則開始查找當前x,y是否在某個子View的區域內,如果在,則把事件分發下去。
按照日志,接下來到達ACTION_MOVE
首先我們源碼進行刪減,只留下MOVE相關的代碼:
@Override 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; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //...ACTION_DOWN //...ACTIN_UP or ACTION_CANCEL // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; // if have a target, see if we're allowed to and want to intercept its // events if (!disallowIntercept && onInterceptTouchEvent(ev)) { //.... } // 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); return target.dispatchTouchEvent(ev); }
18行:把ACTION_DOWN時賦值的mMotionTarget,付給target ;
23行:if (!disallowIntercept && onInterceptTouchEvent(ev)) 當前允許攔截且攔截了,才進入IF體,當然了默認是不會攔截的~這裡執行了onInterceptTouchEvent(ev)
28-30行:把坐標系統轉化為子View的坐標系統
32行:直接return target.dispatchTouchEvent(ev);
可以看到,正常流程下,ACTION_MOVE在檢測完是否攔截以後,直接調用了子View.dispatchTouchEvent,事件分發下去;
最後就是ACTION_UP了
public boolean dispatchTouchEvent(MotionEvent ev) { if (!onFilterTouchEventForSecurity(ev)) { return false; } 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; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) {...} boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; if(target ==null ){...} if (!disallowIntercept && onInterceptTouchEvent(ev)) {...} 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); return target.dispatchTouchEvent(ev); }
17行:判斷當前是否是ACTION_UP
21,28行:分別重置攔截標志位以及將DOWN賦值的mMotionTarget置為null,都UP了,當然置為null,下一次DOWN還會再賦值的~
最後,修改坐標系統,然後調用target.dispatchTouchEvent(ev);
正常情況下,即我們上例整個代碼的流程我們已經走完了:
1、ACTION_DOWN中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則找到包含當前x,y坐標的子View,賦值給mMotionTarget,然後調用 mMotionTarget.dispatchTouchEvent
2、ACTION_MOVE中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接調用mMotionTarget.dispatchTouchEvent(ev)
3、ACTION_UP中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接調用mMotionTarget.dispatchTouchEvent(ev)
當然了在分發之前都會修改下坐標系統,把當前的x,y分別減去child.left 和 child.top ,然後傳給child;
上面的總結都是基於:如果沒有攔截;那麼如何攔截呢?
復寫ViewGroup的onInterceptTouchEvent方法:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: //如果你覺得需要攔截 return true ; case MotionEvent.ACTION_MOVE: //如果你覺得需要攔截 return true ; case MotionEvent.ACTION_UP: //如果你覺得需要攔截 return true ; } return false; }
原因很簡單,當onInterceptTouchEvent(ev) return true的時候,會把mMotionTarget 置為null ;
如果ViewGroup的onInterceptTouchEvent(ev) 當ACTION_MOVE時return true ,即攔截了子View的MOVE以及UP事件;
此時子View希望依然能夠響應MOVE和UP時該咋辦呢?
Android給我們提供了一個方法:requestDisallowInterceptTouchEvent(boolean) 用於設置是否允許攔截,我們在子View的dispatchTouchEvent中直接這麼寫:
@Override public boolean dispatchTouchEvent(MotionEvent event) { getParent().requestDisallowInterceptTouchEvent(true); int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, dispatchTouchEvent ACTION_DOWN); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, dispatchTouchEvent ACTION_MOVE); break; case MotionEvent.ACTION_UP: Log.e(TAG, dispatchTouchEvent ACTION_UP); break; default: break; } return super.dispatchTouchEvent(event); }
從源碼也可以解釋:
ViewGroup MOVE和UP攔截的源碼是這樣的:
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)) { // target didn't handle ACTION_CANCEL. not much we can do // but they should have. } // 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; }
注:如果ViewGroup在onInterceptTouchEvent(ev) ACTION_DOWN裡面直接return true了,那麼子View是木有辦法的捕獲事件的~~~
我們的實例,直接點擊ViewGroup內的按鈕,當然直接很順利的走完整個流程;
但是有兩種特殊情況
1、ACTION_DOWN的時候,子View.dispatchTouchEvent(ev)返回的為false ;
如果你仔細看了,你會注意到ViewGroup的dispatchTouchEvent(ev)的ACTION_DOWN代碼是這樣的
if (child.dispatchTouchEvent(ev)) { // Event handled, we have a target now. mMotionTarget = child; return true; }
但是如果返回false,那麼mMotionTarget 依然是null
mMotionTarget 為null會咋樣呢?
其實ViewGroup也是View的子類,如果沒有找到能夠處理該事件的子View,或者干脆就沒有子View;
那麼,它作為一個View,就相當於View的事件轉發了~~直接super.dispatchTouchEvent(ev);
源碼是這樣的:
final View target = mMotionTarget; 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); }
2、那麼什麼時候子View.dispatchTouchEvent(ev)返回的為true
如果你仔細看了上篇博客,你會發現只要子View支持點擊或者長按事件一定返回true~~
源碼是這樣的:
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { return true ; }
關於代碼流程上面已經總結過了~
1、如果ViewGroup找到了能夠處理該事件的View,則直接交給子View處理,自己的onTouchEvent不會被觸發;
2、可以通過復寫onInterceptTouchEvent(ev)方法,攔截子View的事件(即return true),把事件交給自己處理,則會執行自己對應的onTouchEvent方法
3、子View可以通過調用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup對其MOVE或者UP事件進行攔截;
本文實例講述了Android手機鬧鐘用法。分享給大家供大家參考。具體如下:一、開發手機鬧鐘主要用到了AlarmManager類,AlarmManager類提供了訪問系統定
側邊欄是Android應用中很常見的一個界面效果(抽屜效果)。而利用DrawerLayout實現右側欄是相對簡單的。而且這個控件自帶滑動效果,十分方便。 DrawerLa
安裝做 Android 安全測試之前你應該知道的工具 (一)分析./androlyze.py -s進入分析的交互界面然後執行apk,d,dx=AnalyzeAPK(&qu
在Android中使用ImageView顯示圖片的時候發現圖片顯示不正,方向偏了或者倒過來了。 解決這個問題很自然想到的分兩步走: 1、自動識別圖像方向,計算旋轉角度;