事件分发
事件分发就是用户触摸屏幕所产生的一系列事件的传递。
常见的事件类型
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_POINTER_DOWN = 5;
public static final int ACTION_POINTER_UP = 6;
public static final int ACTION_CANCEL = 3;
public static final int ACTION_OUTSIDE = 4;
MotionEvent常用的方法
getAction()
getX()
getY()
getRawX()
getRawY()
getActionMasked()
getActionIndex()
getPointerCount()
getPointerId(int pointerIndex)
findPointerIndex(int pointerId)
getX(int pointerIndex)
getY(int pointerIndex)
详情文章:MotionEvent详解
requestDisallowInterceptTouchEvent() - 阻止父布局拦截事件
子View为了防止父布局在onInterceptTouchEvent() 中拦截掉事件。
可以通过在自己的dispatchTouchEvent() 内调用getParent().requestDisallowInterceptTouchEvent(true); ,来让父布局不再进入onInterceptTouchEvent() 方法,从而阻止事件被父布局拦截。
事件分发机制
事件分发相关方法
dispatchTouchEvent():用来进行事件的分发,如果MotionEvent(点击事件)能够传递给该View,那么该方法一定会被调用。返回值由 本身的onTouchEvent() 和 子View的dispatchTouchEvent()的返回值 共同决定。
返回值为true,则表示该点击事件被本身或者子View消耗。
返回值为false,则表示该ViewGroup没有子元素,或者子元素没有消耗该事件。
onInterceptTouchEvent():在dispatchTouchEvent()中调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中不会再访问该方法。
onTouchEvent():在dispatchTouchEvent()中调用,返回结果表示是否消耗当前事件,如果不消耗(返回false),则在同一个事件序列中View不会再次接收到事件。
事件分发流向
事件分发从上往下依次是Activity、ViewGroup、View。如果事件不被中断整个事件流向是一个类U型图

消费事件的总结:在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。
ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。
详情文章:图解 Android 事件分发机制
伪代码看事件分发方法的关系
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent (ev) ;
} else {
consume = child.dispatchTouchEvent (ev) ;
}
return consume;
}
事件分发源码
直接看这文章简单易懂。大佬真的讲得很好,膜拜!!!
Android事件分发机制详解:史上最全面、最易懂
View的OnTouchListener解析
没用重写View或者ViewGroup的话,可以通过setOnTouchListener() 方法来监听触摸事件。
OnTouchListener 优先于内部onTouchEvent 执行,并且通过onTouch 回调用通过返回值来控制事件传递,让View的onTouchEvent 不执行。
...
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;
滑动冲突与解决
解决滑动冲突的方法只有两个,总结就是外敷或内服。
例子:父布局的左右滑动与子视图的左右滑动起冲突,最明显的现象是在子视图区域进行滑动,父布局跟着发生了滑动。
外敷 - 外部拦截
解决冲突,从父布局入手。核心:找出冲突区域按需分配
上面例子用外部拦截的方案解决的话。父布局中要重写onInterceptTouchEvent 、onTouchEvent 方法,在按下事件时获取子视图区域。如果手指在子视图区域内,那么就不拦截事件传给子视图并且禁止自己的滑动逻辑的执行。反之手指不在子视图区域直接拦截事件给自己。
private boolean isIntercept;
private Rect childViewRect;
private Rect getChildViewRectFromId(@IdRes int id){
View view = this.findViewById(id);
if (view != null){
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);
return rect;
}
return null;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
childViewRect = getChildViewRectFromId(R.id.a_view);
break;
case MotionEvent.ACTION_MOVE:
if (childViewRect != null){
isIntercept = !childViewRect.contains((int) event.getRawX(), (int) event.getRawY());
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
childViewRect = null;
break;
}
return isIntercept;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isIntercept) return true;
}
内服 - 内部拦截
解决冲突,从子视图入手。核心:阻止事件拦截,让事件给我消费
上面例子用内部拦截的方案解决的话。当事件分发到给自己的时候,调用requestDisallowInterceptTouchEvent 方法,阻止父布局拦截事件,然后事件让自己消费掉。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
super.dispatchTouchEvent(ev);
requestDisallowInterceptTouchEvent(true);
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
return true;
}
实战 - ViewPage2 + DrawerLayout + RecyclerView冲突惨案
上下翻页的全屏ViewPage2,ViewPage2的item内有个从右往左拉出的DrawerLayout,DrawerLayout内有个上下滑动的RecycleView。
冲突1:拉出DrawerLayout会经常触发到ViewPage2的翻页
冲突2:RecycleView上下滑也会经常触发ViewPage2的翻页
解决方法:因为ViewPage2是final的无法继承。所以引入中间布局,专门处理这三者的冲突。通过禁止ViewPage2的滚动来解决冲突。
public class ListenTouchLayout extends LinearLayout {
private float touchDownX, touchDownY;
private Rect childViewRect;
@Setter
private ViewPager2 viewPager;
public ListenTouchLayout(Context context) {this(context, null, -1);}
public ListenTouchLayout(Context context, @Nullable AttributeSet attrs) {this(context, attrs, -1);}
public ListenTouchLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void setViewPagerEnable(boolean isEnable){
if (viewPager != null){
viewPager.setUserInputEnabled(isEnable);
}
}
private Rect getChildViewRectFromId(@IdRes int id){
View view = this.findViewById(id);
if (view != null){
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);
return rect;
}
return null;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
touchDownX = event.getRawX();
touchDownY = event.getRawY();
childViewRect = getChildViewRectFromId(R.id.recycler_view);
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getRawX();
float moveY = event.getRawY();
if (Math.abs(touchDownX - moveX) > Math.abs(touchDownY - moveY)){
setViewPagerEnable(false);
}else {
if (childViewRect != null){
setViewPagerEnable(!childViewRect.contains((int)moveX, (int)moveY));
}else {
setViewPagerEnable(true);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setViewPagerEnable(true);
touchDownX = 0;
touchDownY = 0;
break;
}
return super.dispatchTouchEvent(event);
}
}
将这个ListenTouchLayout放到ViewPager2 item布局中作为根布局就好了。
|