我们通过一个示例来分析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()方法,又根据点击图片等坐标🈶️分发到了第二个LinearLayout,接着有调用了ViewGroup的dispatchTouch()方法,又根据点击图片的坐标分发到了ImageView,然后调用了View的dispatchTouch()方法。ImageView设置setOnTouchListener方法和setOnclickListener方法,如果setOnTouchListener方法返回了false,接着调用了onTouchEvent()方法,从而onClick方法调用,onTouchEvent返回true,消耗了此事件。否则dispatchTouch()方法直接返回了true,消耗此事件。
ViewGroup的onInterceptTouchEvent()方法默认返回false,默认不拦截。
END.
网友评论
问:父布局是如何找到能够接受该点击事件的子 View 的?
答:假设在父布局中的事件坐标是 (x, y)
1)首先要转换成相对子 View 中的坐标,转换的步骤如下:
a) 首先要考虑父布局是否发生滚动,用(x', y')表示转换后的坐标:
x' = x + scrollX;
y' = y + scrollY;
b) 其次要转换成相对子 View 的坐标,转换后的坐标为(x'', y''):
x'' = x' - child.left;
y'' = y' - child.top;
c) 如果子 View 发生了平移,那么还要发生一次转换,假设转换后的坐标为 (x''', y'''):
x''' = x'' - translationX;
y''' = y'' - translationY;
2)然后根据相对子 View 的坐标判断点 (x''', y''') 是否落在子 View 的范围内。判断的方法是:x''' < width && y''' < height && x''' >= 0 && y''' >= 0
源码中的代码不完全和我说一样,但是原理相同,主要表现为:在源码中,第三步中对平移后的 View 的坐标转化不是和我写的那样通过 - translationX 和 -translationY 来进行的,而是采用 Matrix 来变换,但都是一样的道理。