Android的事件分发
1. Touch事件和繪制事件的異同之處
Touch事件和繪制事件很類似,都是由ViewRoot派發下來的,但是不同之處在繪制事件是由應用中的某個View發起請求,一層一層上傳到ViewRoot,再有ViewRoot下發繪制,傳遞canvas給所有子View讓其繪制自身,繪制好后,再通知WMS進行畫到屏幕上。而Touch事件是由硬件捕獲到觸摸后由系統傳遞給應用的ViewRoot,再由ViewRoot往下一層一層傳遞。
他們的處理過程都是自上而下的分發,但是繪制多了一層自下往上的請求。
事件存在消耗,事件的處理方法都會返回一個boolean值,如果該值為true,則本次事件下發將會終止。
2. MotionEvent
2.1 MotionEvent對象的產生
系統有一個線程在循環收集屏幕硬件信息,當用戶觸摸屏幕時,該線程會把從硬件設備收集到的信息封裝成一個MotionEvent對象,然后把該對象存放到一個消息隊列中。
系統的另一個線程循環的讀取消息隊列中的MotionEvent,然后交給WMS去派發,WMS把該事件派發給當前處于活動的Activity,即處于活動棧最頂端的Activity。
這就是一個先進先出的消費者和生產者的模板,一個線程不停的創建MotionEvent對象放入隊列中,另一個線程不斷的從隊列中取出MotionEvent對象進行分發。
當用戶的手指從接觸屏幕到離開屏幕,是一個完整的觸摸事件,在該事件中,系統會不斷收集事件信息封裝成MotionEvent對象。收集的間隔時間取決于硬件設備,例如屏幕的靈敏度以及cpu的計算能力。目前的手機一般在20毫秒左右。
MotionEventCompat.getActionMasked()
2.2 MotionEvent對象詳解
MotionEvent對象包含了觸摸事件的時間、位置、面積、壓力、以及本次事件的Dwon發生的時間。
MotionEvent常用的Action分為5種:Down 、Up、Move、Cancel、OutSide
MotionEvent中我們常用的方法就是獲取點擊的坐標,因為這是與我們操作息息相關的。獲取坐標有兩種方式:
- getX和getY用于獲取以該View左上角為坐標原點的坐標
- getRowX和getRowY用于獲取以屏幕左上角為坐標原點的坐標
2.3 5種Touch事件
- Down:一次觸摸事件的第一個MotionEvent對象,即手指初次接觸屏幕。
- Up:通常為一次觸摸事件的最后一個MotionEvent對象,即手指離開屏幕。
- Move:通常多次發生在一次觸摸事件之中。表示觸摸點發生了移動,我們通常把手指放到屏幕上,實際也會觸發該事件,因為人手總是在輕微抖動的。
- Cancel:常用于取消某個觸摸事件,一般是由程序邏輯來指定該事件,用于取消某次觸摸事件。
- OutSide:當觸摸點發生在響應事件的View之外時,傳遞的事件,通常由程序邏輯來指定。
在上面5種事件中,Down為最重要的事件,因為這是一個觸摸事件的起始點,程序的很多邏輯判斷,都需要根據該事件做處理,例如分發攔截。一次觸摸事件必須要有Down事件,這也是MotionEvent對象中都包含了本次觸摸事件的Down事件發生的時間點這個屬性。其次是Move和Up,通過這3個事件的邏輯處理,就構建出來滑動,點擊,長按,雙擊等多種效果。
2.4 創建一個MotionEvent對象
public static MotionEvent obtain(long downTime, //當用戶最初按下開始一連串的位置事件。這必須得到SystemClock.uptimeMillis()long eventTime, //當這個特定的事件是生成的。這必須得到SystemClock.uptimeMillis() int action, //該次事件的Action float x, //該次事件的x坐標 float y, //該次事件的y坐標 float pressure, //該次事件的壓力,通常感覺標準壓力,從0-1取值 float size, //點擊的區域大小,通常根據特定標準范圍從0-1取值 int metaState, //一個修飾性的狀態,好像一直都是0 float xPrecision, //x坐標的精確度 float yPrecision, //y坐標的精確度 int deviceId, //觸屏設備id,如果是0,說明這個事件不是來自物理設備 int edgeFlags //系統默認都是返回0,程序在傳遞時,可以通過邏輯判斷加入方向位置 )或者一個更簡單的方式:
public static MotionEvent obtain(long downTime,long eventTime,int action,float x,float y,int metaState)也可以通過一個MotionEvent來創建一個新的
public static MotionEvent obtain(MotionEvent event)通過以上的方式,我們知道,我們也可以通過代碼來構建一個虛假的MotionEvent,并分發下去。
view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),SystemClock.uptimeMillis(),MotionEvent.ACTION_DOWN,100,100,0));然后通過延遲以此往下派發Move和Up時間,形成一個完整的觸摸操作。
3. dispatchTouchEvent觸摸事件分發
之前我們知道觸摸事件是被包裝成MotionEvent進行傳遞的,而該對象是繼承了Parcelable接口,正因為如此,才可以從系統中傳遞到我們的應用中。系統通過跨進程通知ViewRoot,ViewRoot會調用DecorView的dispatchTouchEvent下發。
這里有一個和其他事件傳遞不同的地方,DecorView會優先傳遞給Activity,而不是它的子View。而Activity如果不處理又會回傳給DecorView,DecorView才會再將事件傳給子View。
dispatchTouchEvent就是觸摸事件傳遞的對外接口,無論是DecorView傳給Activity,還是ViewGroup傳遞給子View,都是直接調用對方的dispatchTouchEvent方法,并傳遞MotionEvent參數。
我們首先來看看Activity中的dispatchTouchEvent邏輯:
public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();//這是一個空實現的方法,以便子類實現,該方法在Key事件和touch事件的dispatch方法中都被調用,// 就是方便用戶在事件被傳遞之前做一下自己的處理。}//這才是事件真正的分發if (getWindow().superDispatchTouchEvent(ev)) {//superDispatchTouchEvent是一個抽象方法,但是getWindow()獲取的對象實際是FrameWork層的// PhoneWindow,該對象實現了這個方法,內部是直接調用DecorView的superDispatchTouchEvent// 是直接調用dispatchTouchEvent,這樣就傳遞到子View中了 return true;}//如果上面事件沒有被消費掉,那么就調用Activity的onTouchEvent事件。return onTouchEvent(ev); } //PhoneWindow的superDispatchTouchEvent方法直接調用了mDecor的superDispatchTouchEvent public boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event); } //mDecor即為Activity真正的根View,我們通過setContentView所添加的內容就是添加在該View上, // 它實際上就是一個FrameLayout public boolean superDispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);//FrameLayout.dispatchTouchEvent }至此我們已經至少明白了以下幾點:
1、我們可以重載Activity的onUserInteraction方法,在Down事件觸發傳遞前,實現我們的一些需求,實際上源碼中有很多這樣的方法,再某個方法體的第一行提供一個空實現的回調方法,在某個方法的最后一行提供一個空實現的回調方法,以便子類去實現自己的邏輯,例如AsyncTask就有類似的方式。這些技巧都能很好的提高我們代碼的擴展性。
2、Activity會間接的調用根View的dispatchTouchEvent,并通過if判斷返回值,如果為true,即向上層返回true,也就是調用Activity的dispatchTouchEvent的WMS,即操作系統。
3、如果if判斷為false,即根View和根View下的所有子View均為消費掉該事件,那么下面的代碼就有執行機會,即Activity的onTouchEvent,并把該方法的返回值作為結果返回給上層。
3.1 View的dispatchTouchEvent
View中的處理相當簡單明了,因為不涉及到子View,所以只在自身內部進行分發。首先判斷是否設置了觸摸監聽,并且可以響應事件,就交由監聽的onTouch處理。如果上述條件不成立,或者監聽的onTouch事件沒有消費掉該事件,則交由onTouchEvent進行處理,并把返回結果交給上層。
public boolean dispatchTouchEvent(MotionEvent event) {if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&mOnTouchListener.onTouch(this, event)) {//判斷mOnTouchListener是否存在,并且控件可點的情況下,執行onTouch,如果onTouch返回true,就消耗該事件return true;}//如果以上條件都不成立,則把事件交給onTouchEvent來處理return onTouchEvent(event); }3.2 ViewGroup的dispatchTouchEvent
3.3 Down事件
- 通過onInterceptTouchEvent方法判斷是否要攔截事件,默認fasle
- 根據scroll換算后的坐標找出所接受的子View。有動畫的子View將不接受觸摸事件。
- 找到能接受的子View后把event中的坐標轉換成子View的坐標
- 調用子View的dispatchTouchEvent把事件傳遞給子View。
- 如果子View消費了該事件,則把target記錄為子View,方便后面的Move和Up事件的傳遞。
- 如果子View沒有消費,則繼續尋找下一個子View。
- 如果沒找到,或者找到的子View都不消費,就會調用View的dispatchTouchEvent的邏輯,也就是判斷是否有觸摸監聽,有的話交給監聽的onTouch處理,沒有的話交給自己的onTouchEvent處理
接下來我們來研究ViewGroup的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; //當前ViewGroup的視圖矩陣boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//是否禁止攔截if (action == MotionEvent.ACTION_DOWN) {//如果事件是按下事件if (mMotionTarget != null) { //判斷接受事件的target是否為空//不為空肯定是不正常的,因為一個事件是由DOWN開始的,而DOWN還沒有被消費,所以目標也不是不可能被確定,//造成這個的原因可能是在上一次up事件或者cancel事件的時候,沒有把目標賦值為空mMotionTarget = null; //在此處挽救}//不允許攔截,或者onInterceptTouchEvent返回false,也就是不攔截。注意,這個判斷都是在DOWN事件中判斷if (disallowIntercept || !onInterceptTouchEvent(ev)) {//從新設置一下事件為DOWN事件,其實沒有必要,這只是一種保護錯誤,防止被篡改了ev.setAction(MotionEvent.ACTION_DOWN);//開始尋找能響應該事件的子Viewfinal 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可見,或者有動畫,獲取該child的矩陣child.getHitRect(frame);if (frame.contains(scrolledXInt, scrolledYInt)) {// 設置系統坐標final float xc = scrolledXFloat - child.mLeft;final float yc = scrolledYFloat - child.mTop;ev.setLocation(xc, yc);if (child.dispatchTouchEvent(ev)) {//調用child的dispatchTouchEvent//如果消費了,目標就確定了,以便接下來的事件都傳遞給childmMotionTarget = child;return true; //事件消費了,返回true}}}}//能到這里來,證明所有的子View都沒消費掉Down事件,那么留給下面的邏輯進行處理}}//判斷是不是up或者cancel事件boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||(action == MotionEvent.ACTION_CANCEL);if (isUpOrCancel) {//如果是取消,把禁止攔截這個標志位給取消mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}final View target = mMotionTarget;if (target == null) {//判斷該值是否為空,如果為空,則沒找到能響應的子View,那么直接調用super的dispatchTouchEvent,也就是View的dispatchTouchEventev.setLocation(xf, yf);return super.dispatchTouchEvent(ev);}//能走到這里來,說明已經有target,那也說明,這里不是DOWN事件,因為DOWN事件如果有target,已經在前面返回了,執行不到這里if (!disallowIntercept && onInterceptTouchEvent(ev)) {//如果有目標,又非要攔截,則給目標發送一個cancel事件final float xc = scrolledXFloat - (float) target.mLeft;final float yc = scrolledYFloat - (float) target.mTop;ev.setAction(MotionEvent.ACTION_CANCEL);//該為cancelev.setLocation(xc, yc);if (!target.dispatchTouchEvent(ev)) {//調用子View的dispatchTouchEvent,就算它沒有消費這個cancel事件,我們也無能為力了。}//清除目標mMotionTarget = null;//有目標,又攔截,自身也享受不了了,因為一個事件應該由一個View去完成return true;//直接返回true,以完成這次事件,好讓系統開始派發下一次}if (isUpOrCancel) {//取消或者UP的話,把目標賦值為空,以便下一次DOWN能重新找,此處就算不賦值,下一次DOWN也會先把它賦值為空mMotionTarget = null;}//又不攔截,又有目標,那么就直接調用目標的dispatchTouchEventfinal float xc = scrolledXFloat - (float) target.mLeft;final float yc = scrolledYFloat - (float) target.mTop;ev.setLocation(xc, yc);return target.dispatchTouchEvent(ev);//也就是說,如果是DOWN事件,攔截了,那么每次一次MOVE或者UP都不會再判斷是否攔截,直接調用super的dispatchTouchEvent//如果DOWN沒攔截,就是有其他View處理了DOWN事件,那么接下來的MOVE或者UP事件攔截了,那么給目標View發送一個cancel事件,告訴它touch被取消了,并且自身也不會處理,直接返回true//這是為了不違背一個Touch事件只能由一個View處理的原則。 }3.4 Move和Up事件
判斷事件是否被取消或者事件是否要攔截住,是的話,給Down事件找到的target發送一個取消事件。如果不取消,也不攔截,并且Down已經找到了target,則直接交給target處理,不再遍歷子View尋找合適的View了。這種處理事件是正確的,我們用手機經常可以體會到,當我手指按在一個拖動條上之后,在拖動的時候手指就算移出了拖動條,依然會把事件分發給拖動條控制它的拖動。
4. onInterceptTouchEvent
ViewGroup的方法,事件攔截,return true表示攔截觸摸事件,事件就不往下傳遞
子View可以調用getParent().requestDisallowInterceptTouchEvent( true ) 請求父控件不攔截touch事件
5. View的onTouchEvent
從View的dispatchTouchEvent可以看出,事件最終的處理無非是交給TouchListener的onTouch方法或者是交由onTouchEvent處理,由于onTouch默認是空實現,由程序員來編寫邏輯,那么我們來看看onTouchEvent事件。View只能響應click和longclick,不具備滑動等特性。
Down時,設置按壓狀態,發送一個延遲500毫秒的長按事件。
Move時,判斷是否移出了View,移出后移除按壓狀態,長按事件。
Up時,取消按壓,并判斷它是否可以通過觸摸獲取焦點,是的話設置焦點,判斷長按事件是否執行了,如果還沒執行,就刪除,并執行點擊事件。
從上面的代碼我們總結一下View對觸摸事件的處理:
1、是否為diabale,如果是,直接根據是否設置了click和longclick來返回。
2、是否設置了觸摸代理對象,如果有,把事件傳遞給觸摸代理對象,交由其處理,如果消費了,直接返回
3、是否為click或者longclick的,如果是,返回true,不是返回false。
而View對click和longclick的處理如下:
Down:
- 判斷是否可以觸摸上下文菜單。
- 是否在可以滑動的容器中,如果是先設置臨時按壓,再發送一個延遲消息把臨時按壓改為按壓,并發送一個延遲500毫秒的事件去執行長按代碼
- 如果不在滾動容器中,直接設置按壓狀態,并發送一個延遲500毫秒的事件去執行長按代碼。
Move:
- 取觸摸點坐標判斷是否在View中(額外增加了8像素的范圍)
- 如果在,不用做任何事。
- 如果不在,取消臨時按壓到按壓回調,取消長按延遲回調,設置為非按壓狀態
Up
- 判斷是否為按壓或者臨時按壓狀態
- 如果不是,不做任何處理
- 如果是先判斷其是否可以獲取焦點,然后請求焦點。
- 如果是臨時按壓狀態,設置臨時按壓狀態為按壓狀態。保證界面被繪制成按壓狀態,讓用戶可以看見。
- 如果長按回調還未觸發,取消長按回調,如果不是焦點狀態,觸發click事件。
- 如果是臨時按壓狀態,發送一個延遲取消按壓狀態的,保證按壓狀態持續一段時間,讓用戶可見。
- 如果不是臨時按壓狀態,直接發送消息取消按壓狀態。發送失敗,直接取消按壓狀態。
- 取消把臨時按壓設置按壓的回調。
從中我們知道View的onTouchEvent主要處理了click和longclick事件,當按下時,向消息機制發送一個延遲500毫秒的長按回調事件,當移動時候判斷是否移出了View的范圍,超出則取消事件。當離開時,判斷長按事件是否觸發了,如果沒觸發且不是焦點,就觸發click事件。
在這里最繞的就是臨時按壓和按壓狀態,臨時按壓是為了處理滑動容器的,讓處于滑動容器中,按下時,我們先設置的是臨時按壓,持續64毫秒,是為了判斷接下來的時間內是否發生了move事件,如果發生了,將不會再出發按壓狀態,這樣不會讓用戶看到listView滾動時,item還處于按壓狀態。在離開時,我們再次判斷是否處于臨時按壓,如果是在64毫秒內觸發了down和up,說明按壓狀態還沒來得急繪制,則強制設置為按壓狀態,保證用戶能看到,并在取消回調的方法上加上64毫秒的延遲
6. onTouch與onClick
ImageView iv_image = (ImageView) findViewById(R.id.iv_image); iv_image.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {System.out.println("iv_image---onTouch--" + event.getAction());return false;} });點擊ImageView的時候只會打印一次,因為onTouch()返回false,只傳遞down事件,不會傳遞up事件
System.out: iv_image---onTouch--0 // ImageView天生不能被點擊,沒有點擊事件 ImageView iv_image = (ImageView) findViewById(R.id.iv_image); iv_image.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {System.out.println("iv_image---onTouch--" + event.getAction());return true; // 把返回值改為true } });把onTouch()方法返回值改為true,點擊ImageView會打印兩次(down and up)
System.out: iv_image---onTouch--0 System.out: iv_image---onTouch--1 ImageView iv_image = (ImageView) findViewById(R.id.iv_image); iv_image.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {System.out.println("iv_image---onTouch--" + event.getAction());return true;} }); //添加click事件 iv_image.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {System.out.println("iv_image---onClick");} });還是打印兩次,onTouch()返回true,click事件并不會得到執行
ImageView iv_image = (ImageView) findViewById(R.id.iv_image); iv_image.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {System.out.println("iv_image---onTouch--" + event.getAction());return false;} });iv_image.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {System.out.println("iv_image---onClick");} });打印三次,兩次touch事件(down and up)和一次click事件
Button button = (Button) findViewById(R.id.button); button.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {System.out.println("button---onTouch--" + event.getAction());return false;} });點擊Button會打印兩次
Button button = (Button) findViewById(R.id.button); button.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {System.out.println("button---onTouch--" + event.getAction());return true;} });button.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {System.out.println("button---onClick");} });打印兩次,因為onTouch()返回true,不會執行onTouchEvent(),而click事件是在onTouchEvent()中執行,所以也不會執行click事件
Button button = (Button) findViewById(R.id.button); button.setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {System.out.println("button---onTouch--" + event.getAction());return false;} });button.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {System.out.println("button---onClick");} });打印三次
public boolean dispatchTouchEvent(MotionEvent event) {if (!onFilterTouchEventForSecurity(event)) {return false;}if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&mOnTouchListener.onTouch(this, event)) {return true;}return onTouchEvent(event); }a 判斷mOnTouchListener是否為null
b 判斷當前的控件是否可用
c 判斷view的onTouch。
d 如果以上一個返回為false。那么就會調用onTouchEvent
首先判斷mOnTouchListener不為null,并且view是enable的狀態,然后 mOnTouchListener.onTouch(this, event)返回true,這三個條件如果都滿足,直接return true ; 也就是下面的onTouchEvent(event)不會被執行了。如果我們設置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不會被執行了
onTouch是優先于onClick執行, onClick的調用在onTouchEvent(event)方法中
view的事件分發
Android的事件分發實例分析
7. ScrollView的onTouchEvent
普通的ViewGroup并沒有對onTouchEvent事件做處理,只有可以滾動的才有,我們可以分析一下ScrollView
Down時,判斷落點是否在子View中,不再就不處理,因為ScrollView只有一個子View。
Move時,通過對比本次手指的位置和上一次的位置的距離,計算出Y方向的差值,然后用scorllBy進行滾動視圖
Up時,通過速度進行fling,這里利用了兩個幫助類,一個是計算速度的幫助類VelocityTracker,一個是滾動的幫助類Scroller
通過以上分析,我們得出以下知識:
- 在down事件的時候先判斷觸摸是否處于邊緣,如果是,則不處理
- 在down事件中判斷落點是否在子View中,如果不在,不處理
- 在down事件中判斷是否仍在滑動,如果是,先停止
- 記錄第一個按下點的索引值
- 每次事件都記錄住當前的y值
- 在move事件中通過記錄的索引值找到對應的點,獲取y坐標
- 與上一次y坐標進行比對,scrollBy兩次的差值
- 在up事件的時候計算最后一秒鐘的速度,并且有最大速度進行限制,當計算的速度大于系統默認的最小速度時,只想fling
- up和cancel事件還原變量為默認值
- 如果為多點離開,進行多點離開的處理
- 該處理方式時:如果離開的是第一個按下的點,那么由第二個按下的點代替其進行y值偏移計算的基點,并清空速度計算的幫助類,重新記錄MotionEvnet
8. Layout和Scroll的區別
- Layout中設置的是自身在父View中的顯示區域
- Scroll是調整自己的顯示區域
- 當父View滾動或者layout變化后,自身在屏幕上的位置會發生變化。
當自身Scroll滾動后,在屏幕上的顯示位置是不變的,變的只是自身的顯示內容。 - Scroll滾動不會影響Layout,只是在draw的時候影響畫布偏移和觸摸時的坐標計算。
總結
以上是生活随笔為你收集整理的Android的事件分发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android开发常用第三方平台
- 下一篇: 未来之路—写在大二结束之前