我們通過(guò)一個(gè)示例來(lái)分析Touch事件的分發(fā)過(guò)程。
示例:
布局文件:
<?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;
}
});
}
點(diǎn)擊圖片,現(xiàn)象
可以看到onTouch事件比onClick事件優(yōu)先級(jí)高。
再看看把setOnTouchListener事件的返回值改為true:
可以看到onClick事件沒(méi)有了。這是為什么?我們透過(guò)源碼來(lái)看看這個(gè)現(xiàn)象。
事件分發(fā)機(jī)制源碼分析
當(dāng)我們觸摸屏幕上的某個(gè)控件時(shí),底層的設(shè)備硬件傳遞給InputManager經(jīng)過(guò)一 定的處理后,傳遞給AmS,再經(jīng)過(guò)AmS的處理后就傳遞到我們的Activity,接著傳遞Window,最后傳遞到頂級(jí)View。
觸摸事件的分發(fā)過(guò)程有三個(gè)重要的方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用來(lái)分發(fā)事件的,如果當(dāng)前事件能傳遞到該View,該 方法一定調(diào)用,View的onTouchEvent方法會(huì)調(diào)用,而該方法的返回值所onTouchEvent影響。
public boolean onInterceptHoverEvent(MotionEvent event)
用來(lái)攔截事件的,如果返回值為true,表示攔截。否則不攔截。
public boolean onTouchEvent(MotionEvent event)
處理當(dāng)前事件的。如果返回值為true表示消耗該事件。否則無(wú)法再接收同一個(gè)序列的事件。
同一個(gè)序列的事件是;DOWN事件--》多個(gè)MOVE事件--》UP事件。
Activity觸摸事件分發(fā)過(guò)程
當(dāng)觸摸事件傳遞到Activity,Activity的dispatchTouchEvent()方法就會(huì)調(diào)用,我們?nèi)タ纯矗?/p>
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如果當(dāng)前事件是DOWN事件,調(diào)用了onUserInteraction方法,該方法是一個(gè)空方法,我們可以重載該方法,在DOWN事件做一些處理。接著就把事件傳遞給Window來(lái)處理該事件。如果返回true,表示有View處理該事件,onTouch Event()方法返回了true,整個(gè)事件處理完成。否則Activity的onTouchEvent方法就會(huì)被調(diào)用。
Window觸摸事件的分發(fā)過(guò)程
Window類(lèi)是abstract的,唯一的具體實(shí)現(xiàn)類(lèi)是PhoneWindow類(lèi),我們?nèi)タ纯碢honeWindow的superDispatchTouchEvent()方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
private DecorView mDecor;
DecorView類(lèi)繼承于FrameLayout:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {....}
因此就是調(diào)用了DecorView的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
可以看到,其實(shí)就是調(diào)用了父類(lèi)的dispatchTouchEvent()方法。DecorView繼承于FrameLayout,F(xiàn)rameLayout繼承于ViewGroup。因此就是調(diào)用了ViewGroup的dispatchTouchEvent()方法。
DecorView就是我們的頂層View,當(dāng)我們通過(guò)setContentView()方法設(shè)置的是頂層View的一個(gè)子View。DecorView組成為:
可以看出,事件傳遞的大概過(guò)程:
Activity--》Window--》View。某個(gè)View的onTouchEvent()方法被調(diào)用。如果返回true,傳遞會(huì)Window,Window再傳遞會(huì)Activity,事件處理結(jié)束。否則返回false,再同樣的傳遞會(huì)Activity。
頂層View事件分發(fā)的過(guò)程
DecorView繼承與FrameLayout,是一個(gè)ViewGroup,ViewGroup繼承于View,繼承圖:
ViewGroup重載了dispatchTouchEvent()方法。那我們?nèi)タ纯丛摲椒ǎ?/p>
@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;
}
這個(gè)方法很長(zhǎng)我們分幾部分來(lái)分析。代碼中標(biāo)有1.2.3.....。
1.ViewGroup對(duì)DOWN事件重置狀態(tài)的操作。
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
標(biāo)志FLAG_DISALLOW_INTERCEPT可以通過(guò)requestDisallowInterceptTouchEvent方法設(shè)置。因此在DOWN事件該方法不影響該標(biāo)志,簡(jiǎn)單來(lái)說(shuō),就是不影響ViewGroup處理DOWN事件的操作。
2.判斷是否攔截事件。
首先判斷是否DOWN事件或者mFirstTouchTarget != null。
mFirstTouchTarget的意思是,如果ViewGroup的有子元素成功處理,mFirstTouchTarget就會(huì)指向該元素。
如果當(dāng)前事件是DOWN:FLAG_DISALLOW_INTERCEPT不影響ViewGroup對(duì)DOWN事件的處理,因此調(diào)用了onInterceptTouchEvent()方法。是否攔截取決于該方法的返回值。
如果onInterceptTouchEvent()返回true,說(shuō)明ViewGroup攔截事件,mFirstTouchTarget為null,同一序列的事件都由它處理,onInterceptTouchEvent也不會(huì)再調(diào)用了,因?yàn)閍ctionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null條件都不滿足。如果子 View調(diào)用了requestDisallowInterceptTouchEvent()方法后,ViewGroup將無(wú)法攔截除DOWN事件以外的其他事件。該方法不影響ViewGroup的DOWN事件。
3.如果ViewGroup不攔截,ViewGroup遍歷所有的子View,判斷子View是否滿足當(dāng)前的事件。滿足的條件有兩個(gè):子View是否播放動(dòng)畫(huà)和事件的坐標(biāo)是否在子View的區(qū)域。
如果滿足條件,調(diào)用了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;
}
....
}
其實(shí)就是調(diào)用了子View的dispatchTouchEvent()方法。如果返回了true,就會(huì)通過(guò)addTouchTarget()方法對(duì)mFirstTouchTarget賦值并停止遍歷子View。
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
可以看到,mFirstTouchTarget是一個(gè)單鏈表的數(shù)據(jù)結(jié)構(gòu)。
如果遍歷全部的子View都沒(méi)有成功處理的,mFirstTouchTarget成員變量為null,當(dāng)該成員變量為null,就會(huì)調(diào)用:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
因?yàn)榈谌齻€(gè)參數(shù)為null,就會(huì)調(diào)用super.dispatchTouchEvent()方法,調(diào)用到了View的dispatchTouchEvent()方法。
View的事件分發(fā)過(guò)程
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;
}
從上面的代碼可以看出,判斷了是否設(shè)置了setOnTouchListener,是否為ENABLED,onTouch是否返回了true。
ENABLED對(duì)這個(gè)判斷沒(méi)有影響。
但onTouch返回true,onTouchEvent方法就不會(huì)執(zhí)行了。而onClick的方法是在onTouchEvent()方法執(zhí)行的。因此onTouch事件的優(yōu)先級(jí)比onClick事件高,而且還當(dāng)onTouch方法返回了true,onClick事件就不會(huì)調(diào)用了。說(shuō)明了上面的示例的現(xiàn)象。
我們?nèi)タ纯磑nClick事件是否在onTouch Event方法中執(zhí)行的。
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判斷是否可點(diǎn)擊或者長(zhǎng)點(diǎn)擊。只要有一個(gè)為true,就會(huì)返回true,表示消耗此事件。
CLICKABLE和LONG_CLICKABLE的值可以在清單文件中通過(guò)android:clickable和 android:longClickable屬性設(shè)置,也可以通過(guò)setOnclickListener()和setLongClickListener()方法設(shè)置。
當(dāng)設(shè)置了點(diǎn)擊事件調(diào)用了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;
}
可以看到回調(diào)了我們?cè)O(shè)置的onClick方法。由此看出onClick事件是在onTouch Event方法執(zhí)行的。
這就是事件分發(fā)的大概流程。
我們根據(jù)上面的示例走一下整個(gè)觸摸事件的分發(fā)流程。
我們從頂View開(kāi)始分析:
整個(gè)View樹(shù)的結(jié)構(gòu)如下:
上面的示例,我們點(diǎn)擊的圖片。
首先由頂層View(FrameLayout)的dispatchTouch()方法根據(jù)點(diǎn)擊圖片等坐標(biāo)首先分發(fā)到第一個(gè)LinearLayout的,然后調(diào)用了ViewGroup的dispatchTouch()方法,又根據(jù)點(diǎn)擊圖片等坐標(biāo)???分發(fā)到了第二個(gè)LinearLayout,接著有調(diào)用了ViewGroup的dispatchTouch()方法,又根據(jù)點(diǎn)擊圖片的坐標(biāo)分發(fā)到了ImageView,然后調(diào)用了View的dispatchTouch()方法。ImageView設(shè)置setOnTouchListener方法和setOnclickListener方法,如果setOnTouchListener方法返回了false,接著調(diào)用了onTouchEvent()方法,從而onClick方法調(diào)用,onTouchEvent返回true,消耗了此事件。否則dispatchTouch()方法直接返回了true,消耗此事件。
ViewGroup的onInterceptTouchEvent()方法默認(rèn)返回false,默認(rèn)不攔截。
END.