Android下Touch事件的分發機制
我們通過一個示例來分析Touch事件的分發過程。
示例:
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rootview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.maimingliang.test.view.TestTouchActivity">
<TextView
android:id="@+id/txt"
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="55dp"
android:text="textView"/>
<ImageView
android:id="@+id/img"
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher"/>
</LinearLayout>
Activity:
public class TestTouchActivity extends AppCompatActivity {
private static final String TAG = "TestTouchActivity";
@Bind(R.id.txt)
TextView tv;
@Bind(R.id.img)
ImageView img;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_touch);
ButterKnife.bind(this);
initView();
}
private void initView() {
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG,"-------> tv Onclick");
}
});
tv.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "-------> tv onTouch");
return false;
}
});
img.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "--------> img onClick");
}
});
img.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "--------> img onTouch");
return true;
}
});
}
點擊圖片,現象
這里寫圖片描述
可以看到onTouch事件比onClick事件優先級高。
再看看把setOnTouchListener事件的返回值改為true:
這里寫圖片描述
可以看到onClick事件沒有了。這是為什么?我們透過源碼來看看這個現象。
事件分發機制源碼分析
當我們觸摸屏幕上的某個控件時,底層的設備硬件傳遞給InputManager經過一 定的處理后,傳遞給AmS,再經過AmS的處理后就傳遞到我們的Activity,接著傳遞Window,最后傳遞到頂級View。
觸摸事件的分發過程有三個重要的方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用來分發事件的,如果當前事件能傳遞到該View,該 方法一定調用,View的onTouchEvent方法會調用,而該方法的返回值所onTouchEvent影響。
public boolean onInterceptHoverEvent(MotionEvent event)
用來攔截事件的,如果返回值為true,表示攔截。否則不攔截。
public boolean onTouchEvent(MotionEvent event)
處理當前事件的。如果返回值為true表示消耗該事件。否則無法再接收同一個序列的事件。
同一個序列的事件是;DOWN事件--》多個MOVE事件--》UP事件。
Activity觸摸事件分發過程
當觸摸事件傳遞到Activity,Activity的dispatchTouchEvent()方法就會調用,我們去看看:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如果當前事件是DOWN事件,調用了onUserInteraction方法,該方法是一個空方法,我們可以重載該方法,在DOWN事件做一些處理。接著就把事件傳遞給Window來處理該事件。如果返回true,表示有View處理該事件,onTouch Event()方法返回了true,整個事件處理完成。否則Activity的onTouchEvent方法就會被調用。
Window觸摸事件的分發過程
Window類是abstract的,唯一的具體實現類是PhoneWindow類,我們去看看PhoneWindow的superDispatchTouchEvent()方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
private DecorView mDecor;
DecorView類繼承于FrameLayout:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {....}
因此就是調用了DecorView的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
可以看到,其實就是調用了父類的dispatchTouchEvent()方法。DecorView繼承于FrameLayout,FrameLayout繼承于ViewGroup。因此就是調用了ViewGroup的dispatchTouchEvent()方法。
DecorView就是我們的頂層View,當我們通過setContentView()方法設置的是頂層View的一個子View。DecorView組成為:
這里寫圖片描述
可以看出,事件傳遞的大概過程:
Activity--》Window--》View。某個View的onTouchEvent()方法被調用。如果返回true,傳遞會Window,Window再傳遞會Activity,事件處理結束。否則返回false,再同樣的傳遞會Activity。
頂層View事件分發的過程
DecorView繼承與FrameLayout,是一個ViewGroup,ViewGroup繼承于View,繼承圖:
這里寫圖片描述
ViewGroup重載了dispatchTouchEvent()方法。那我們去看看該方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
....
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
1.
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
2.
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
....
3.
if (!canceled && !intercepted) {
....
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
....
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
....
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
....
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
....
}
}
....
}
}
....
return handled;
}
這個方法很長我們分幾部分來分析。代碼中標有1.2.3.....。
1.ViewGroup對DOWN事件重置狀態的操作。
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
標志FLAG_DISALLOW_INTERCEPT可以通過requestDisallowInterceptTouchEvent方法設置。因此在DOWN事件該方法不影響該標志,簡單來說,就是不影響ViewGroup處理DOWN事件的操作。
2.判斷是否攔截事件。
首先判斷是否DOWN事件或者mFirstTouchTarget != null。
mFirstTouchTarget的意思是,如果ViewGroup的有子元素成功處理,mFirstTouchTarget就會指向該元素。
如果當前事件是DOWN:FLAG_DISALLOW_INTERCEPT不影響ViewGroup對DOWN事件的處理,因此調用了onInterceptTouchEvent()方法。是否攔截取決于該方法的返回值。
如果onInterceptTouchEvent()返回true,說明ViewGroup攔截事件,mFirstTouchTarget為null,同一序列的事件都由它處理,onInterceptTouchEvent也不會再調用了,因為actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null條件都不滿足。如果子 View調用了requestDisallowInterceptTouchEvent()方法后,ViewGroup將無法攔截除DOWN事件以外的其他事件。該方法不影響ViewGroup的DOWN事件。
3.如果ViewGroup不攔截,ViewGroup遍歷所有的子View,判斷子View是否滿足當前的事件。滿足的條件有兩個:子View是否播放動畫和事件的坐標是否在子View的區域。
如果滿足條件,調用了dispatchTransformedTouchEvent()方法。去看看:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
....
}
其實就是調用了子View的dispatchTouchEvent()方法。如果返回了true,就會通過addTouchTarget()方法對mFirstTouchTarget賦值并停止遍歷子View。
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
可以看到,mFirstTouchTarget是一個單鏈表的數據結構。
如果遍歷全部的子View都沒有成功處理的,mFirstTouchTarget成員變量為null,當該成員變量為null,就會調用:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
因為第三個參數為null,就會調用super.dispatchTouchEvent()方法,調用到了View的dispatchTouchEvent()方法。
View的事件分發過程
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 (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
....
return result;
}
從上面的代碼可以看出,判斷了是否設置了setOnTouchListener,是否為ENABLED,onTouch是否返回了true。
ENABLED對這個判斷沒有影響。
但onTouch返回true,onTouchEvent方法就不會執行了。而onClick的方法是在onTouchEvent()方法執行的。因此onTouch事件的優先級比onClick事件高,而且還當onTouch方法返回了true,onClick事件就不會調用了。說明了上面的示例的現象。
我們去看看onClick事件是否在onTouch Event方法中執行的。
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_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 (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
.....
.....
}
return true;
}
return false;
}
從上述代碼看到,判斷了viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE判斷是否可點擊或者長點擊。只要有一個為true,就會返回true,表示消耗此事件。
CLICKABLE和LONG_CLICKABLE的值可以在清單文件中通過android:clickable和 android:longClickable屬性設置,也可以通過setOnclickListener()和setLongClickListener()方法設置。
當設置了點擊事件調用了performClick()方法:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
可以看到回調了我們設置的onClick方法。由此看出onClick事件是在onTouch Event方法執行的。
這就是事件分發的大概流程。
我們根據上面的示例走一下整個觸摸事件的分發流程。
我們從頂View開始分析:
整個View樹的結構如下:
這里寫圖片描述
上面的示例,我們點擊的圖片。
首先由頂層View(FrameLayout)的dispatchTouch()方法根據點擊圖片等坐標首先分發到第一個LinearLayout的,然后調用了ViewGroup的dispatchTouch()方法,又根據點擊圖片等坐標:u6709:?分發到了第二個LinearLayout,接著有調用了ViewGroup的dispatchTouch()方法,又根據點擊圖片的坐標分發到了ImageView,然后調用了View的dispatchTouch()方法。ImageView設置setOnTouchListener方法和setOnclickListener方法,如果setOnTouchListener方法返回了false,接著調用了onTouchEvent()方法,從而onClick方法調用,onTouchEvent返回true,消耗了此事件。否則dispatchTouch()方法直接返回了true,消耗此事件。
ViewGroup的onInterceptTouchEvent()方法默認返回false,默認不攔截。
END.
來自:http://www.jianshu.com/p/1e2d439487f0