如何理解MotionEvent對象,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
一、MotionEvent對象
當用戶觸摸屏幕時,將創建一個MontionEvent對象。MotionEvent包含了關于發生觸摸的位置和時間的信息,以及觸摸事件的其他細節。
獲取MontionEvent對象的方法有:
1.重載Activity中的onTouchEvent(MotionEvent event)方法;
2.View對象調用View.setOnTouchListener接口實現onTouch(View v, MotionEvent event)方法;
獲得MontionEvent對象后,可以通過以下常用方法進一步獲取觸控事件的具體信息:
event.getAction() //獲取觸控動作比如ACTION_DOWN event.getPointerCount(); //獲取觸控點的數量,比如2則可能是兩個手指同時按壓屏幕 event.getPointerId(nID); //對于每個觸控的點的細節,我們可以通過一個循環執行getPointerId方法獲取索引 event.getX(nID); //獲取第nID個觸控點的x位置 event.getY(nID); //獲取第nID個點觸控的y位置 event.getPressure(nID); //LCD可以感應出用戶的手指壓力,當然具體的級別由驅動和物理硬件決定的 event.getDownTime() //按下開始時間 event.getEventTime() // 事件結束時間 event.getEventTime()-event.getDownTime()); //總共按下時花費時間
觸控對象中的主要相關常量:
/** * 用于多點觸控進行操作 */ public static final int ACTION_MASK = 0xff; /** * 手指按下時 */ public static final int ACTION_DOWN = 0; /** * 手指放開時 */ public static final int ACTION_UP = 1; /** * 移動操作時 */ public static final int ACTION_MOVE = 2; /** * 用戶無規則的操作時可能觸發. 此操作用于表明,一個觸摸序列在未發生任何實際操作的情況下結束. */ public static final int ACTION_CANCEL = 3; /** * 觸摸操作發生在窗口之外,但仍然能夠找到該操作的特殊情況下設置. */ public static final int ACTION_OUTSIDE = 4; /** * */ public static final int ACTION_POINTER_DOWN = 5; /** * */ public static final int ACTION_POINTER_UP = 6; /** * */ public static final int ACTION_HOVER_MOVE = 7; /** * Android3.1開始引入的常量,來自于輸入設備(如鼠標),而非觸摸屏. */ public static final int ACTION_SCROLL = 8;
二、MotionEvent對象處理過程
范例視圖如下:
activity_main.xml代碼如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:orientation="vertical" > <LinearLayout android:id="@+id/layout1" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical" android:tag="true_Layout" > <com.example.d_motionevent.TrueButton android:id="@+id/trueButton1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="true_Btn_上" android:text="true_Btn_上" /> <com.example.d_motionevent.FalseButton android:id="@+id/falseButton1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="false_Btn_上" android:text="false_Btn_上" /> </LinearLayout> <LinearLayout android:id="@+id/layout2" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="#ff00ff" android:orientation="vertical" android:tag="false_Layout" > <com.example.d_motionevent.TrueButton android:id="@+id/trueButton2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="true_Btn_下" android:text="true_Btn_下" /> <com.example.d_motionevent.FalseButton android:id="@+id/falseButton2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:tag="false_Btn_下" android:text="false_Btn_下" /> </LinearLayout> </LinearLayout> </RelativeLayout>
自定義Button類代碼如下:
package com.example.d_motionevent; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.Button; /** * * @author zeng */ public class BooleanButton extends Button { public BooleanButton(Context context, AttributeSet attrs) { super(context, attrs); } protected boolean myValue() { return false; } @Override public boolean onTouchEvent(MotionEvent event) { String myTag = this.getTag().toString(); Log.e(myTag, "===========" + "Button 的 onTouchEvent 方法" + "==========="); Log.e(myTag, MainActivity.describeEvent(this, event)); Log.e(myTag, "父類 onTouchEvent() = " + super.onTouchEvent(event)); Log.e(myTag, "該button touch = " + myValue()); return myValue(); } }
TrueButton類代碼:
package com.example.d_motionevent; import android.content.Context; import android.util.AttributeSet; /** * onTouchEvent返回true的Button. * @author zeng */ public class TrueButton extends BooleanButton { public TrueButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected boolean myValue() { return true; } }
FalseButton類代碼:
package com.example.d_motionevent; import android.content.Context; import android.util.AttributeSet; /** * onTouchEvent返回false的Button. * @author zeng */ public class FalseButton extends BooleanButton { public FalseButton(Context context, AttributeSet attrs) { super(context, attrs); } }
MainActivity.class代碼如下:
package com.example.d_motionevent; import android.os.Bundle; import android.app.Activity; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.widget.Button; /** * 地址:http://glblong.blog.51cto.com/3058613/1557683 * @author zeng */ public class MainActivity extends Activity implements OnTouchListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); View layout1 = findViewById(R.id.layout1); layout1.setOnTouchListener(this); Button trueButton1 = (Button) findViewById(R.id.trueButton1); trueButton1.setOnTouchListener(this); Button falseButton1 = (Button) findViewById(R.id.falseButton1); falseButton1.setOnTouchListener(this); View layout2 = findViewById(R.id.layout2); layout2.setOnTouchListener(this); Button trueButton2 = (Button) findViewById(R.id.trueButton2); trueButton2.setOnTouchListener(this); Button falseButton2 = (Button) findViewById(R.id.falseButton2); falseButton2.setOnTouchListener(this); } @Override public boolean onTouch(View v, MotionEvent event) { String myTag = v.getTag().toString(); Log.e(myTag, "===========" + "Activity 的 onTouch 方法" + "==========="); Log.e(myTag, "被點擊的View = " + myTag); Log.e(myTag, describeEvent(v, event)); if ("true".equals(myTag.substring(0, 4))) { Log.e(myTag, " == true"); return true; } else { Log.e(myTag, " == false"); return false; } } @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } protected static String describeEvent(View view, MotionEvent event) { StringBuilder sb = new StringBuilder(300); sb.append("Action: ").append(event.getAction()).append("\n");// 獲取觸控動作比如ACTION_DOWN sb.append("相對坐標: ").append(event.getX()).append(" * ").append(event.getY()).append(" "); sb.append("絕對坐標: ").append(event.getRawX()).append(" * ").append(event.getRawY()).append("\n"); if (event.getX() < 0 || event.getX() > view.getWidth() || event.getY() < 0 || event.getY() > view.getHeight()) { sb.append("未點擊在View范圍內"); } sb.append("Edge flags: ").append(event.getEdgeFlags()).append(" ");// 邊緣標記,但是看設備情況,很可能始終返回0 sb.append("Pressure: ").append(event.getPressure()).append(" ");// 壓力值,0-1之間,看情況,很可能始終返回1 sb.append("Size: ").append(event.getSize()).append("\n");// 指壓范圍 sb.append("Down time: ").append(event.getDownTime()).append("ms "); sb.append("Event time: ").append(event.getEventTime()).append("ms "); sb.append("Elapsed: ").append(event.getEventTime() - event.getDownTime()).append("ms\n"); return sb.toString(); } }
點擊過程分析:
1.點擊上部的【true_Btn_上】,運行日志如下:
此時執行的是Activity上的onTouch方法并返回了True。
onTouch()方法返回true,因為編碼TrueButton的目的是為返回true。返回true會告訴Android,MotionEvent對象已經被使用,不能將它提供給其他方法。
它還告訴Android,繼續將此觸摸序列的觸摸事件發送到此方法。這就是為什么我們在ACTION_DOWN事件后還會看到ACTION_UP或ACTION_MOVE等其他事件。
當觸摸【true_Btn_上】時,按鈕并沒有高亮顏色變化。這是因為,onTouch()是在調用任何按鈕方法之前調用的,而且onTouch()方法返回了true,所以Android就不會再調用【true_Btn_上】按鈕的onTouchEvent()方法了。
如果在onTouch()方法中,在返回true的行之前添加一行"v.onTouchEvent(event);",那么將會看到觸摸時按鈕會有顏色變化了。
2.點擊上部的【false_Btn_上】,運行日志如下:
點擊后,false_btn按鈕會處于常亮狀態。效果如圖:
Android接收到MotionEvent對象中的ACTION_DOWN事件,將它傳遞給MainActivity類中的onTouch()方法。onTouch()執行后返回false。
這個過程告訴Android,onTouch()方法未使用該事件,所以Android尋找要調用的下一個方法,也就是范例中的FalseButton類中重寫的onTouchEvent()方法。參見BooleanButton.java中的onTouchEvent()方法,先執行父類的onTouchEvent()方法,然后再返回了false。此時打印出來的Logcat日志與之前的完全一樣,因為我們仍然在同一個View對象FalseButton中。
根據日志可以看到,父類希望從onTouchEvent()返回true,但是FalseButton的該方法返回了false。通過返回false,再次告訴Android我們未使用此事件,所以Android不會將ACTION_UP事件發送到我們的按鈕,以至于該按鈕不知道手指是否已離開了觸摸屏。這也是為什么FalseButton在被按下時一直停留在被按下的顏色狀態。
簡而言之,每次從收到MotionEvent對象的UI對象返回false時,Android就會停止將MotionEvent對象繼續發送到該UI對象,同時還會不斷的查找另一個UI對象來使用MotionEvent對象。
接著看日志,Android兩次嘗試找到ACTION_DOWN事件的使用者但都失敗了,現在它前進道應用程序中下一個可能接收該事件的View,也就是按鈕底層的布局true_Layout。
此時,Android再次調用了MainActivity類中的onTouch()方法,但是現在接收事件的對象變成了true_Layout。接收到的MotionEvent對象的所有信息也與之前相同,只有Y坐標除外,因為點擊位置的Y坐標相對于布局左上角要比相對于按鈕的左上角的距離來得大。因為true_Layout的onTouch()方法返回true,所以Android將觸摸事件的剩余信息發送到了布局。
3.點擊上部的【true_Btn_上】按鈕不放,同時觸摸其他區域。
觸摸【true_Btn_上】按鈕,在離開按鈕之前,其他手指在屏幕上其他區域移動。此時,Logcat將顯示接收觸摸事件的對象都是【true_Btn_上】按鈕。甚至手指離開【true_Btn_上】按鈕,而另一手指仍然在屏幕上移動時,接收到觸摸事件的仍然還是【true_Btn_上】按鈕。
當從onTouch()返回true時,觸摸事件序列將與【true_Btn_上】按鈕相關聯。這告訴了Android,它可以停止查找用來接收MotionEvent對象的UI對象了,只需將此觸摸序列的所有未來MotionEvent對象發送給我們。即使在拖動手指時遇到了另一個視圖,我們仍然會綁定到此序列的原始視圖。
4.點擊下部的【false_Btn_下】按鈕,運行日志如下:
原理與前幾點相同。此時若按住按鈕的手指離開前,其他手指繼續觸摸屏幕,則將不再繼續輸出日志,因為Android找不到接收MotionEvent對象并返回true結果的UI對象,直到開始下一個新觸摸序列。
范例源碼見附件。地址:http://glblong.blog.51cto.com/3058613/1557683
三、MotionEvent回收
MotionEvent類中有個recycle()方法,但是不要通過此方法對MotionEvent對象進行回收。如果回調方法沒有使用MotionEvent對象,并且返回了false,MotionEvent對象可能會被傳遞到其他某個方法、視圖或我們的活動。
MotionEvent類中還有個obtain()方法,通過此方法可以創建一個MotionEvent的副本或者全新的MotionEvent,這個副本或全新的事件對象是在完成之后應該回收的對象。
四、小結
1.onTouch()方法與View類中的onTouchEvent()方法的區別
onTouch()是View.setOnTouchListener后調用的方法,如果一個View對象setOnTouchListener后,同時又重寫了View類自身的onTouchEvent()方法,那么當屏幕觸摸時會先調用哪個方法呢?
每當事件產生時,都要先經由dispatchTouchEvent方法來分發??聪耉iew類中的dispatchTouchEvent()源碼,如下:
/** * Pass the touch screen motion event down to the target view, or this view * if it is the target. * * @param event * The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { // 用于測試目,直接忽略 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } // 上面代碼的第2個標注。 未被其他窗口遮蓋 if (onFilterTouchEventForSecurity(event)) { // noinspection SimplifiableIfStatement if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { // 如果有監聽器,執行監聽器的,不在執行當前視圖的onTouchEvent方法 return true; } // 執行當前視圖的onTouchEvent方法 if (onTouchEvent(event)) { return true; } } // 用于測試目,直接忽略 if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }
dispatchTouchEvent()方法里先是判斷了是否有監聽器,然后才會去判斷onTouchEvent(event)。因此會先執行onTouch()方法,如果監聽器的onTouch()返回false,則繼續執行View的onTouchEvent()。
2.觸摸事件的傳遞過程
1).觸摸事件最先被最頂層的UI對象(比如范例中的Btn)接收,如果該View對象接收后返回false,則繼續傳向其上一層UI對象(比如范例中的layout),依此類推,直到最底層的Activity。
2).同一個View對象,接收到觸摸事件時,如果有設置監聽器,先執行View.setOnTouchListener里的onTouch(),然后執行View類中的onTouchEvent(event)方法。
3).當一個UI對象接收到觸摸事件并返回true時,同時多指觸摸的其他事件序列都會被該UI對象接收,直至下一個新的觸摸事件序列產生。
4).當各層的UI對象接收觸摸事件都返回false時,此刻同時多指觸摸,由此產生的其他事件序列將找不到接收的UI對象。因為當前原始的接收UI對象返回false,系統會一直查找下一個可能接收事件并返回true的UI對象但卻又無法找到。
看完上述內容,你們掌握如何理解MotionEvent對象的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。