文章目录
Android 事件分发
阅读完之后,你可以学到以下知识
- 事件分发原理
- 解决view之间交互冲突
1、事件组成以及传递顺序
1.1、触摸事件的组成
- 1个down
- n个move
- 1个up
- 0|1 个cancel
1.2、传递顺序
Activity —> PhoneWindow —> DecorView —> ViewGroup —>… —> View
使用的是责任链设计模式,又上层往下层传递,如果有下层组建消费任务则结束,如果没有消费则交给上层自己处理
1.3、涉及的核心方法
- dispatchTouchEvent 分发事件
- onInterceptTouchEvent 拦截事件
- li.mOnTouchListener.onTouch 由开发者来处理事件
- onTouchEvent 处理用户产生的事件
- requestDisallowInterceptTouchEvent 控制父view拦截功能
其中1,2,3,4方法会返回一个值
true : 代表事件被处理,不会继续传递
false : 事件继续往下传递
2、View的事件分发
dispatchTouchEvent -> li.mOnTouchListener.onTouch -> onTouchEvent -> li.mOnClickListener.onClick
2.1、dispatchTouchEvent
分发触摸事件
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;
}
第7行到第9行4个条件决定了result的值,result控制onTouchEvent方法调用
-
li != null(mListenerInfo不为空)
用户设置了事件(比如点击事件) - 条件为true
-
li.mOnTouchListener != null (view设置了OnTouchListener)
设置了触摸事件 - 条件为true
-
(mViewFlags & ENABLED_MASK) == ENABLED
控件不可用(默认可用)- 条件为true
-
li.mOnTouchListener.onTouch(this, event)
设置触摸事件onTouch方法返回true - 条件为true
2.2、mOnTouchListener.onTouch
触摸事件
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);
}
2.3、onTouchEvent
控件对触摸行为处理
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
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)) {
performClickInternal();
}
}
}
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;
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
return false;
}
54,55行 case MotionEvent.ACTION_UP 经过一些处理到达performClickInternal方法
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
在看performClick
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
第9行执行了li.mOnClickListener.onClick通知上层控件被点击了
补下OnClickListener源码
public interface OnClickListener {
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
场景1:button注册点击事件,触摸事件
action -> 点击按钮 ,代码执行流程如下
1、上层li.mOnTouchListener.onTouch返回为true:
dispatchTouchEvent -> li.mOnTouchListener.onTouch
2、上层li.mOnTouchListener.onTouch返回为false:
dispatchTouchEvent -> li.mOnTouchListener.onTouch -> li.mOnClickListener.onClick
总结
- 点击事件是在onTouchEvent中触发的
- onTouch触摸事件被消费,则不会产生li.mOnClickListener.onClick 事件
- 发现按钮点击事件不响应看dispatchTouchEvent 在看 onTouch
3、ViewGroup事件分发
dispatchTouchEvent->onInterceptTouchEvent-> li.mOnTouchListener.onTouch -> onTouchEvent
li.mOnTouchListener.onTouch,onTouchEvent跟View的事件传递是一直的
下面主要分析一下
- dispatchTouchEvent
- onInterceptTouchEvent
3.1、dispatchTouchEvent
ViewGroup重写了view的dispatchTouchEvent方法
主要做了一下几件事情
- 拦截事件分发
- 事件传递给子view
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
mMotionTarget = null;
}
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
final View target = mMotionTarget;
if (target == null) {
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
mMotionTarget = null;
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
第13行 有两个条件disallowIntercept || !onInterceptTouchEvent(ev)
-
disallowIntercept
是否禁用掉事件拦截的功能,默认是false。可通过requestDisallowInterceptTouchEvent修改
-
onInterceptTouchEvent
拦截控件的触摸功能
源码执行流程
-
disallowIntercept 为false 尝试事件拦截,进入onInterceptTouchEvent
-
disallowIntercept 为true 不需要尝试事件拦截,不执行onInterceptTouchEvent
3.2、onInterceptTouchEvent
onInterceptTouchEvent源码
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
-
onInterceptTouchEvent返回true代表拦截触摸事件
此后事件将交给当前控件来处理,调用super.dispatchTouchEvent() 也就是View.dispatchTouchEvent()
-
onInterceptTouchEvent返回false代表不拦截触摸事件
匹配子view,匹配到之后传递给子view,调用child.dispatchTouchEvent(ev)
最后走的是View的事件传递:
dispatchTouchEvent -> li.mOnTouchListener.onTouch -> onTouchEvent -> li.mOnClickListener.onClick
4、事件冲突解决方法
一个布局中有多个可滑动的控件,就会存在滑动冲突的问题
举个栗子:
首页页面组成:Banner、功能菜单、列表item
- 主view为ListView
- Banner为ViewPager
- ListView里面header是一个ViewPager
交互
- ListView交互是上下滑动
- ViewPager交互是左右滑动
用户想滑动ViewPager,ListView也滑动了 导致体验很不好
这个案例中ListView是父View,ViewPager是子View
事件传递顺序
ListView -> ViewPager
因为是滑动冲突,我们只要关注滑动事件move即可
下面说下两种解决方法
- 内部拦截法
- 外部拦截法
4.1、内部拦截法
左右滑动是触发ViewPager
内部指的是ViewPager
dispatchTouchEvent方法 中进行处理冲突
private float mInitialTouchX;
private float mInitialTouchY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mInitialTouchY = event.getY();
mInitialTouchX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX() - mInitialTouchX;
float moveY = event.getY() - mInitialTouchY;
if (Math.abs(moveX) - Math.abs(moveY) < 0) {
//核心逻辑
getParent().requestDisallowInterceptTouchEvent(true);
}
mInitialTouchY = event.getY();
mInitialTouchX = event.getX();
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
核心逻辑:x轴移动距离大于y轴则代表用户操作的是上下滑动。
第16行 getParent().requestDisallowInterceptTouchEvent(true);
禁用父view(ListView)拦截,将事件传递给ViewPager来处理
这样处理之后move事件,一旦符合view的滑动规则,子view告诉父view后续产生的的move事件都交给自己来处理,所以此时只有一个view(ViewPager)能拿到move事件
4.2、外部拦截法
上下滑动是触发ListView
这里外部指的就是ListView
也是在dispatchTouchEvent方法处理
private float mInitialTouchX;
private float mInitialTouchY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mInitialTouchY = event.getY();
mInitialTouchX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX() - mInitialTouchX;
float moveY = event.getY() - mInitialTouchY;
mInitialTouchY = event.getY();
mInitialTouchX = event.getX();
if (Math.abs(moveX) - Math.abs(moveY) > 0) {
//核心逻辑
return true;
}
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
核心逻辑: y轴移动距离大于x轴则代表用户操作的是上下滑动
第18行 return true;
事件拦截后,交给自己处理。事件将不会分发到ViewPager
使用哪种方案合适
- 看view冲突是子view还是父view
- 滑动意图是希望父view滑动,则采用外部拦截法
- 滑动意图是希望子view滑动,则采用内部拦截法
文章有什么不对的地方,请大家斧正