Android事件
分发原理搞清楚
可以辅助我们解决很多实际项目中遇到的事件冲突
等问题
1.
进入正题之前,问大家几个事件相关的问题?
标签: dispatchTouchEvent()
Q1:
Android点击事件传递规则是怎样的?(下面几步仔细阅读2遍,有助于加深对事件传递的理解
)
A1:
Step1:
当点击事件产生后,会由 Activity
首先来处理
/**
* 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;
}
// 如果没有任何View处理触摸事件,则转交给Activity的onTouchEvent自己处理
return onTouchEvent(ev);
}
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
Step2:
上述步骤1传递给 PhoneWindow
的getWindow().superDispatchTouchEvent(ev)
,再传递给 DecorView
,最后传递给顶层的ViewGroup
。
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
...
...
// PhoneWindow 的 superDispatchTouchEvent 方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
// DecorView 的 superDispatchTouchEvent 方法
// DecorView 继承 FrameLayout (FrameLayout extends ViewGroup)
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
Step3:
对于根 ViewGroup
,点击事件首先传递给它的 dispatchTouchEvent()
方法
,如果该 ViewGroup
的 onInterceptTouchEvent()
方法返回true,则表示它要拦截这个事件,这个事件就会交给它的 onTouchEvent()
方法处理。如果 onInterceptTouchEvent()
方法返回 false
,则表示它不拦截这个事件,则这个事件会交给它的子元素的 dispatchTouchEvent()
来处理
// ViewGroup 中 dispatchTouchEvent 方法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
......
// 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;
}
......
// 如果该 `ViewGroup` 的 `onInterceptTouchEvent()` 方法返回true,则表示它要拦截这个事件,这个事件就会交给它的 `onTouchEvent()` 方法处理
if (!canceled && !intercepted) {
......
}
}
return handled;
}
Step4:
依步骤3传递,如此反复下去。如果传递给底层的View,View是没有子View的不再需要拦截了,就会调用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) {
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 我们平常设置的 setOnTouchListener方法优先级高于 onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
/**
* Interface definition for a callback to be invoked when a touch event is
* dispatched to this view. The callback will be invoked before the touch
* event is given to the view.
*/
public interface OnTouchListener {
/**
* Called when a touch event is dispatched to a view. This allows listeners to
* get a chance to respond before the target view.
*
* @param v The view the touch event has been dispatched to.
* @param event The MotionEvent object containing full information about
* the event.
* @return True if the listener has consumed the event, false otherwise.
*/
boolean onTouch(View v, MotionEvent event);
}
Step5:
一般情况下最终会调用View的 onTouchEvent()
方法
// 1. 一般View 长按监听在 点击监听之前被触发
// 2. 长按监听源代码是在点击按压超过500ms时,默认为长按事件
// 3.
/**
* Implement this method to handle touch screen motion events.
* <p>
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* {@link #performClick()}. This will ensure consistent system behavior,
* including:
* <ul>
* <li>obeying click sound preferences
* <li>dispatching OnClickListener calls
* <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
* accessibility features are enabled
* </ul>
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int action = event.getAction();
......
switch (action) {
case MotionEvent.ACTION_UP:
// 抬起,判断是否处理点击事件
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)) {
performClickInternal();
}
}
}
break;
case MotionEvent.ACTION_DOWN:
// 按下,处理长按事件
......
break;
case MotionEvent.ACTION_MOVE:
// 移动,检测触摸是否划出了控件区域,移除响应事件
break;
case MotionEvent.ACTION_CANCEL:
//
......
break;
}
}
Step6:
接下来讲解点击事件由下而上的传递。当点击事件传给底层的 View 时,如果其 onTouchEvent()
方法返回 true
,则事件由底层的View消耗并处理;如果返回false
则表示该View不做处理,则传递给父View的 onTouchEvent()
处理;若父View的 onTouchEvent()
仍旧返回false,则继续传递给该父View的父View处理,如此反复下去
事件分发小结
:
1.
一个事件序列,从手指触摸屏幕到手指离开屏幕,在这个过程中,以DOWN
事件开始,中间含有不定数的MOVE
事件,以UP
事件结束
2.
正常情况下,一个事件序列,只能被一个View拦截并且消耗
3.
某个View一旦决定拦截,那么这个事件序列都将由它的onTouchEvent
处理,并且它的onInterceptTouchEvent
不会再调用
4.
某个View一旦开始处理事件,如果它不消耗ACTION_DOWN
事件(onTouchEvent
返回false),那么同一事件序列中其他事件都不会再交给它处理。并且重新交由它的父元素处理(父元素onTouchEvent
被调用)
5.
事件传递过程是由外向内
的,即事件总是先传递给父元素
,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent
方法可以在子View中干预父元素的事件分发过程,但ACTION_DOWN
除外
6.
ViewGroup
默认不拦截任何事件,即onInterceptTouchEvent
默认返回false
。View
没有onInterceptTouchEvent
方法,一旦有点击事件,传递给它,那么它的onTouchEvent
方法就会被调用
7.
View
的onTouchEvent
默认会消耗事件(返回true
),除非它是不可点击的(clickable
和longClickable
同时为false
),View的longClickable
默认都为false,clickable
要分情况,比如Button的clickable默认为true,TextView的clickable默认为false
Q2:
Android事件分发类型有哪几个状态
A2:
onTouchDown,onTouchMove,onTouchCancel,onTouchUp
Q3:
requestDisallowInterceptTouchEvent属性的作用?
A3:
禁止或允许父View拦截自己的点击事件
// 不允许父View 拦截点击事件
// false则反之
getParent().requestDisallowInterceptTouchEvent(true);
2.
在琢磨以上几个问题之后,我们通过下面几段伪代码,来模式触摸事件的传递流程
以下通过Java层代码模拟Android中事件分发流程,有助于理解Android事件分发机制,希望能帮助到大家(创建6个java文件,然后以java工程运行打印日志学习)
如果你能静下心来,读完下面6个.java文件代码,我相信你能收获很多
2.1 假Activity
public class Activity {
public static void main(String[] arg) {
// 顶级容器ViewGroup(构造函数传递左上,右下坐标)
ViewGroup viewGroup = new ViewGroup(0, 0, 1080, 1920);
viewGroup.setName("顶级容器");
// 二级容器ViewGroup,也是定义两个坐标
ViewGroup viewGroup1 = new ViewGroup(0, 0, 500, 500);
viewGroup1.setName("第二级容器");
// 模拟初始化View以及放置在ViewGroup层级中
View view = new View(0, 0, 200, 200);
view.setName("子View");
viewGroup1.addView(view);
viewGroup.addView(viewGroup1);
viewGroup.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("顶级的OnTouch事件");
return false;
}
});
viewGroup1.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("第二级容器的OnTouch事件");
return false;
}
});
// view.setOnClickListener(new OnClickListener() {
// @Override
// public void onClick(View v) {
// System.out.println("子iew的onClick事件");
// }
// });
//
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
System.out.println("子view的OnTouch事件");
return false;
}
});
// 模拟事件分发(点击里面View坐标点为:(100,100))
MotionEvent motionEvent = new MotionEvent(100, 100);
motionEvent.setActionMasked(MotionEvent.ACTION_DOWN);
// 顶级容器分发
viewGroup.dispatchTouchEvent(motionEvent);
}
}
2.2 假ViewGroup
public class ViewGroup extends View {
// 子View个数
private View[] mChildren = new View[0];
public ViewGroup(int left, int top, int right, int bottom) {
super(left, top, right, bottom);
}
List<View> childList = new ArrayList<>();
public void addView(View view) {
if (view == null) {
return;
}
childList.add(view);
mChildren = childList.toArray(new View[childList.size()]);
}
private TouchTarget mFirstTouchTarget;
// 事件分发的入口
public boolean dispatchTouchEvent(MotionEvent event) {
//
System.out.println(name + " dispatchTouchEvent ");
boolean handled = false;
boolean intercepted = onInterceptTouchEvent(event);
// TouchTarget 模式 内存缓存 move up
TouchTarget newTouchTarget = null;
int actionMasked = event.getActionMasked();
if (actionMasked != MotionEvent.ACTION_CANCEL && !intercepted) {
// 不拦截情况下,开始处理Down事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
final View[] children = mChildren;
// 遍历ViewGroup中所有子View
for (int i = children.length - 1; i >= 0; i--) {
View child = mChildren[i];
// View能够接收到事件
if (!child.isContainer(event.getX(), event.getY())) {
continue;
}
// 能够接受事件 child 分发给他
if (dispatchTransformedTouchEvent(event, child)) {
// View[] 采取了 Message 的方式进行 链表结构
handled = true;
newTouchTarget = addTouchTarget(child);
break;
}
}
}
// 当前的ViewGroup dispatchTransformedTouchEvent
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(event, null);
}
}
return handled;
}
private TouchTarget addTouchTarget(View child) {
final TouchTarget target = TouchTarget.obtain(child);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
// 回收池策略·
private static final class TouchTarget {
public View child;//当前缓存的View
// 回收池(一个对象)
private static TouchTarget sRecycleBin;
private static final Object sRecycleLock = new Object[0];
public TouchTarget next;
// size
private static int sRecycledCount;
// up事件
public static TouchTarget obtain(View child) {
TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
target = sRecycleBin;
}
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
target.child = child;
return target;
}
public void recycle() {
if (child == null) {
throw new IllegalStateException("已经被回收过了");
}
synchronized (sRecycleLock) {
if (sRecycledCount < 32) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
}
}
}
}
//分发处理 子控件 View
private boolean dispatchTransformedTouchEvent(MotionEvent event, View child) {
boolean handled = false;
// 当前View消费了
if (child != null) {
handled = child.dispatchTouchEvent(event);
} else {
handled = super.dispatchTouchEvent(event);
}
return handled;
}
/**
* @param ev
* @return 是否拦截点击事件
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
}
2.3 假View
public class View {
public String name;
@Override
public String toString() {
return "" + name;
}
public void setName(String name) {
this.name = name;
}
private int left;
private int top;
private int right;
private int bottom;
private OnTouchListener mOnTouchListener;
private OnClickListener onClickListener;
public void setOnTouchListener(OnTouchListener mOnTouchListener) {
this.mOnTouchListener = mOnTouchListener;
}
public void setOnClickListener(OnClickListener onClickListener) {
this.onClickListener = onClickListener;
}
public View() {
}
public View(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
/**
* @param x
* @param y
* @return 是否处于View点击区域内
*/
public boolean isContainer(int x, int y) {
if (x >= left && x < right && y >= top && y < bottom) {
return true;
}
return false;
}
// 接受分发的代码
public boolean dispatchTouchEvent(MotionEvent event) {
System.out.println(name + " dispatchTouchEvent ");
// 消费
boolean result = false;
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
private boolean onTouchEvent(MotionEvent event) {
System.out.println(name + " onTouchEvent ");
if (onClickListener != null) {
onClickListener.onClick(this);
return true;
}
return false;
}
}
2.4 假MotionEvent
public class MotionEvent {
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;
private int actionMasked;
private int x;
private int y;
public MotionEvent() {
}
public MotionEvent(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getActionMasked() {
return actionMasked;
}
public void setActionMasked(int actionMasked) {
this.actionMasked = actionMasked;
}
}
2.5 假OnClickListener
public interface OnClickListener {
void onClick(View v);
}
2.6 假OnTouchListener
public interface OnTouchListener {
boolean onTouch(View v, MotionEvent event);
}
3.
通过以上伪代码,我们熟悉了事件传递流程,在实际开发中,我们通常通过以下2种策略来解决事件冲突
3.1
外部拦截法
// 重写ViewGroup的以下方法,
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downPoint.x = ev.getX();
downPoint.y = ev.getY();
intercepted = false; // 必须为false,否则后续的MOVE,UP不在传递给子View
break;
case MotionEvent.ACTION_MOVE:
CLog.i(TAG, "onTouchEvent ACTION_MOVE ev.getX(): " + ev.getX());
if (父容器需要当前点击事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false; // 必须为false,否则会影响子View的onClick是否被触发
break;
}
return intercepted;
}
// 大家可以参照ScrollView的源码来进一步熟悉外部拦截法的实际应用(以下这段为ScrollView源码片段)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (super.onInterceptTouchEvent(ev)) {
return true;
}
/*
* Don't try to intercept touch if we can't scroll anyway.
*/
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
......
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
......
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
/* Release the drag */
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
stopNestedScroll();
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
return mIsBeingDragged;
}
3.2
内部拦截法
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downPoint.x = ev.getX();
downPoint.y = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
CLog.i(TAG, "onTouchEvent ACTION_MOVE ev.getX(): " + ev.getX());
if (TextUtils.equals("0", mPos) && downPoint.x < ev.getX()) {
return true;
}
}
return super.dispatchTouchEvent(ev);
}