android進階4step4:Android實戰開發——事件分發機制
Android事件分發機制
為什麼需要事件分發機制?
比如:上圖
Button(View)的ViewGroup是FrameLayout2
FragmeLayout2的ViewGroup是FragmeLayout1
當點選Button時,所觸發的事件到底是交給誰來處理呢?
常見的事件分發分為兩種
- 冒泡(自下而上的過程) View—>ViewGroup—>Activity
- 捕獲(自上而下的過程) Activity—>ViewGroup—>View
Android中的事件分發機制
注意:嚴格來說以下流程只是ACTION_DOWN的一種特殊的情況
程式碼實現:
MyFrameLayout.java 自定義的ViewGroup
重寫了以下三個方法
- public boolean dispatchTouchEvent(MotionEvent ev) 事件分發
- public boolean onInterceptTouchEvent(MotionEvent ev) 事件攔截(ViewGroup的方法)
- public boolean onTouchEvent(MotionEvent event) 事件處理(是否消費該事件)
/** * 自定義ViewGroup(FrameLayout本身就是一個ViewGroup)的MyFragmentLayout */ public class MyFrameLayout extends FrameLayout { private static final String TAG = "MyFrameLayout"; public MyFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { final 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; } return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onInterceptTouchEvent - ACTION_DOWN"); mLastY = ev.getAction(); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onInterceptTouchEvent - ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onInterceptTouchEvent - ACTION_UP"); break; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { final 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; } return super.onTouchEvent(event); } }
MyView.java 自定義View
重寫了以下兩個方法
- public boolean dispatchTouchEvent(MotionEvent ev) 事件分發
- public boolean onTouchEvent(MotionEvent event) 事件處理(是否消費該事件)
/**
* 自定義View MyView
*/
public class MyView extends View {
private static final String TAG = "MyView";
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
final 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;
}
return super.dispatchTouchEvent(event);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent - ACTION_DOWN");
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent - ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent - ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
}
TouchSystemActivity.java Activity
重寫了以下兩個方法
- public boolean dispatchTouchEvent(MotionEvent ev) 事件分發
- public boolean onTouchEvent(MotionEvent event) 事件處理(是否消費該事件)
public class TouchSystemActivity extends AppCompatActivity {
private static final String TAG = "TouchSystemActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_touch_system);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final 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;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final 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;
}
return super.onTouchEvent(event);
}
}
佈局檔案:
activity_touch_system.xml
<com.demo.android4step4.view.MyFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#728dff"
>
<com.demo.android4step4.view.MyView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:background="#ffc481"/>
</com.demo.android4step4.view.MyFrameLayout>
現在來操作:
當手指點選MyView時會執行 ACTION_DOWN ——>ACTION_MOVE
手指離開執行 ACTION_UP
那麼這幾個事件會怎麼進行分發呢?
注意:嚴格來說以下流程只是ACTION_DOWN的一種特殊的情況
前面4個階段是捕獲(自上而下)的事件分發模式
後面3個階段是冒泡(自下而上)的事件分發模式
當所有的View和ViewGroup都不消費該事件,那麼就會自動傳給當前Activity進行處理
之後的事件(直到下次點選開始,完一次點選事件的完成過程)都由它進行處理了。
可見:下面的MOVE 和UP都是由Activity的onTouchEvent方法進行(消費)處理的
E/TouchSystemActivity: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: onInterceptTouchEvent - ACTION_DOWN
E/MyView: dispatchTouchEvent - ACTION_DOWN
E/MyView: onTouchEvent - ACTION_DOWN
E/MyFrameLayout: onTouchEvent - ACTION_DOWN
E/TouchSystemActivity: onTouchEvent - ACTION_DOWN
E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: dispatchTouchEvent - ACTION_UP
E/TouchSystemActivity: onTouchEvent - ACTION_UP
對事件感興趣的View
上面講的是一個預設情況下,會交由Activity進行事件的處理
那View自身如何表明對事件感興趣呢?
最主要是View.dispatchOnTouchEvent()在 ACTION_DOWN的時候返回true。
但是一般情況下,我們主要重寫的方法是onTouchEvent, 所以要保證ACTION_DOWN返回true。
- 注:凡是clickable = true 或者 longClickable = ture的控 件,正常情況下View.onTouchEvent()一定返回true.
還是手指點選View的過程:DOWN-MOVE*-UP
如果在View中的OnTouchEvent方法中返回True 表明對該事件感興趣(消費該事件),進行相應的處理
只要ACTION_DOWN 中返回true其他的事件也是預設交由該View進行處理
那麼事件分發機制是以下的流程,虛線是不走的
有兩種方式返回true
- 直接在末尾返回true
- 在Action_Down的時候返回true,末尾執行父類的super.onTouchEvent也是可以
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction();
//Log.e(TAG,MotionEvent.actionToString(event.getAction()));
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent - ACTION_DOWN");
//getParent().requestDisallowInterceptTouchEvent(true);
//只要ACTION_DOWN 返回true其他的事件也是預設交由該View進行處理
//1。這裡返回true也可以,其他兩個預設返回false
return true ;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent - ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent - ACTION_UP");
break;
}
//2
return super.onTouchEvent(event);
}
log日誌:
E/TouchSystemActivity: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: onInterceptTouchEvent - ACTION_DOWN
E/MyView: dispatchTouchEvent - ACTION_DOWN
E/MyView: onTouchEvent - ACTION_DOWN
E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onInterceptTouchEvent - ACTION_MOVE
E/MyView: dispatchTouchEvent - ACTION_MOVE
E/MyView: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onInterceptTouchEvent - ACTION_MOVE
E/MyView: dispatchTouchEvent - ACTION_MOVE
E/MyView: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onInterceptTouchEvent - ACTION_MOVE
E/MyView: dispatchTouchEvent - ACTION_MOVE
E/MyView: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: dispatchTouchEvent - ACTION_UP
E/MyFrameLayout: dispatchTouchEvent - ACTION_UP
E/MyFrameLayout: onInterceptTouchEvent - ACTION_UP
E/MyView: dispatchTouchEvent - ACTION_UP
E/MyView: onTouchEvent - ACTION_UP
E/TouchSystemActivity: onTouchEvent - ACTION_UP
你會發現前面的Down 返回了true該事件經由View消費
但是當Move 和 Up操作時會到 Activity的OnTouchEvent 方法
為什麼呢?
在Activity中的dispatchTouchEvent(ev)的原始碼中
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
- 看第二個if 如果該事件轉發下去返回true則,直接返回true。因為在View的轉發Move和Up事件轉發是返回的事false,則activity會執行它本身的onTouchEvent的方法,這就是它會輸出的原因。
ViewGroup對事件進行攔截
攔截的目的是交給自己處理(onTouchEvent)
在MyFragmentLayout中的
onInterceptTouchEvent 返回true表示攔截該事件
onTouchEvent 中返回 true表示處理該事件
E/TouchSystemActivity: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: dispatchTouchEvent - ACTION_DOWN
E/MyFrameLayout: onInterceptTouchEvent - ACTION_DOWN
E/MyFrameLayout: onTouchEvent - ACTION_DOWN
E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: dispatchTouchEvent - ACTION_MOVE
E/MyFrameLayout: onTouchEvent - ACTION_MOVE
E/TouchSystemActivity: dispatchTouchEvent - ACTION_UP
E/MyFrameLayout: dispatchTouchEvent - ACTION_UP
E/MyFrameLayout: onTouchEvent - ACTION_UP
可以看到
第一次在Down中攔截事件後,事件就不再往下轉發給View,而是交給自己的onTouchEvent方法進行處理
之後的Move*-Up都不執行攔截操作了,預設之後的事件都交給該ViewGroup處理
以下是Down的分發過程:
下面是Move*-Up的分發過程
模擬真實攔截過程:
在ViewGroup中 onInterceptTouchEvent 中
如果點選的點到最後的觸控點的Y大於200dp(實際就是手指下滑200dp後觸發攔截事件)
一旦攔截,之後的所有事件都由ViewGroup處理
//記錄Down時的點的位置
private int mLastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onInterceptTouchEvent - ACTION_DOWN");
mLastY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onInterceptTouchEvent - ACTION_MOVE");
//如果當前的觸控點和我們之前down時候的點大於200dp
if (ev.getY() - mLastY > 200) {
Log.e(TAG, "down時候Y的位置" + mLastY);
Log.e(TAG, "up時候Y的位置 + ev.getY());
Log.e(TAG, "onInterceptTouchEvent - ACTION_MOVE - return true ");
return true;
}
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onInterceptTouchEvent - ACTION_UP");
break;
}
return super.onInterceptTouchEvent(ev);
// return true;
}
有沒有方法?讓ViewGroup不攔截子View要處理的事件呢?
getParent().requestDisallowInterceptTouchEvent(true);
請求父View不攔截這個事件
@TargetApi(Build.VERSION_CODES.KITKAT) @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getAction(); Log.e(TAG,MotionEvent.actionToString(event.getAction())); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onTouchEvent - ACTION_DOWN"); getParent().requestDisallowInterceptTouchEvent(true); case MotionEvent.ACTION_MOVE: Log.e(TAG, "onTouchEvent - ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onTouchEvent - ACTION_UP"); break; } //return super.onTouchEvent(event); return true ; }
View與ViewGroup相關方法職責
- dispatchTouchEvent ACTION_DOWN的時候:逆序遍歷子View,找出對該事件感興趣的View,標記為targetView,然後對於該手勢(DOWN-MOVE*-UP)的後續事件都傳給targetView。
- onInterceptTouchEvent 在ACTION_DWON,或者存在targetView的情況下,可以隨時對該事件進行攔截,交給自己處理。
- onTouchEvent 拿到事件後做針對當前View的相關操作。
自定義View中的事件分發
- 處理Touch事件
– 可以編寫setOnTouchListener(無需繼承View) – 複寫onTouchEvent
- 注意標明對事件感興趣
– 如果需要自己獲取touch事件進行處理,ACTION_DOWN必須 返回true,保證整個手勢的事件都能夠傳遞到該View。
• 包含上述View的所有事項
- • 攔截子View事件
– 可以在onInterceptTouchEvent()中子View的事件進行攔截, 交給自己的onTouchEvent進行處理。
– 注意:一旦攔截針對當然的手勢所有事件都將由當前的 ViewGroup處理。會傳遞一個ACTION_CANCEL交給當前的子 View,讓子View明白後續的事件不會到來了。
其他?
requestDisallowInterceptTouchEvent
來讓父佈局禁用攔截事件功能,從而父佈局忽略該事件之後的一切Action
ViewConfiguration 是系統中關於檢視的各種特性的常量記錄物件
• ViewConfiguration
- – getScaledTouchSlop()
getScaledTouchSlop是一個距離,表示滑動的時候,手的移動要大於這個距離才開始移動控制元件。如果小於這個距離就不觸發移動控制元件,如viewpager就是用這個距離來判斷使用者是否翻頁
- – getScaledMinimumFlingVelocity()
用於設定最小加速率
- – getLongPressTimeout()
長按事件閾值
- • OnScrollListener / View.onScrollChanged() 滑動監聽
- • GestureDetector 手勢檢測
- • ScaleGestureDetector 伸縮手勢
擴充套件學習
• Mastering the Android Touch System
– 視訊(中文字幕) http://v.youku.com/v_show/id_XODQ1MjI2MDQ0.html?from=s1.8- 1-1.2
– 英文(Google搜尋下)