【Android】结合View的事件分发机制解决滑动冲突
一、常见的滑动冲突类型及处理规则
常见的滑动冲突类型
1.外部滑动方向和内部方向不一致
2.外部滑动方向和内部方向一致
3.上面两种类型的嵌套
处理规则
场景1:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件
根据坐标得到滑动方向的方式:
1.依据滑动路径和水平方向形成的夹角
2.依据水平方向和竖直方向的距离差
3.依据水平和竖直方向的速度差
场景2和场景3:依据业务需求得出相应的处理规则
二、简单了解View的事件分发机制
MotionEvnet的传递规则
事件分发机制的核心就是对MotionEvnet(点击事件)的传递,而这个过程由以下三个重要方法来完成。
public boolean dispatchTouchEvent(MotionEvent ev)//用来进行事件的分发,如果事件能够传递给当前View,此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件
public boolean onInterceptTouchEvent(MotionEvent ev)//用来判断是否拦截某个事件,如果当前View拦截了此事件,在同一事件序列中只会调用一次此方法,返回结果表示是否拦截当前事件
public boolean onTouchEvent(MotionEvent ev)// dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在用一个时间序列中,当前View无法再次接收到事件
由此我们可以总结出事件分发机制的过程:对于一个ViewGroup来说,点击事件产生后,首先传给它,调用它的dispatchTouchEvent,如果onInterceptTouchEvent返回true,表示拦截该事件,即调用onTouchEvent;如果onInterceptTouchEvent返回false,表示不拦截该事件,事件继续向下传递,调用子元素的dispatchTouchEvent,直到事件被处理。
特殊情况:
通过requestDisallowInterceptTouchEvent 设置FLAG_DISALLOW_INTERCEPT标记位,设置后ViewGroup无法拦截除ACTION_DOWN之外的其他事件
事件分发机制有以下几点需特别注意:
1.一个事件序列只能被一个View拦截并消耗,但可以通过特殊手段做到分别由两个View同时处理
2.某个View处理事件时如果不消耗ACTION_DOWN事件(onTouchEvent返回false),同一事件序列中的其他事件不会再交给他处理,并调用父元素的onTouchEvent
3.如果View不消耗ACTION_DOWN以外事件,那么这个点击事件消失,并且当前View可以接收后续事件,最终消失的事件交给Activity处理
4.ViewGroup默认不拦截任何事件
5.View没有onInterceptTouchEvent方法,直接调用onTouchEvent
6.View的onTouchEvent默认消耗事件,View的longClickable默认为false
7.事件传递过程从外到内,除ACTION_DOWN事件外,子元素可以通过requestDisallowInterceptTouchEvent方法干预父元素事件分发过程
View对点击事件的处理(不包含ViewGroup)
首先判断是否定义了onTouchListener,根据onTouch的返回结果,判断是否调用onTouchEvent
在onTouchEvent中,即使View处于不可用状态,依旧会消耗点击事件
只要View的clickable和long_clickable有一个为true,就会消耗事件
当发生ACTION_UP事件时,会触发performClick,如果设置了OnClickListener就会调用onClick方法
当我们设置setOnClickListener/setOnLongClickListener时,会自动将View的clickable/long_clickable设为true
三、解决滑动冲突的两种方法
根据上面对View事件分发机制的了解,我们可以知道,View的事件分发是从父容器逐层向内传递的,由此,我们可以分别通过内部拦截和外部拦截两种方式解决滑动冲突。
内部拦截
自定义控件继承原来的控件,并重写onInterceptTouchEvent方法
父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要就消耗掉,否则交给父容器处理
重点:
子视图通过判断是否需要消费此事件,如果需要,通过设置getParent().requestDisallowInterceptTouchEvent(true)取消父容器拦截,并在子视图消费;如果不需要,设置getParent().requestDisallowInterceptTouchEvent(false)让父容器拦截。
public class HorizontalScrollRecyclerView extends RecyclerView {private float startX, startY;private boolean isScrolling = false;public HorizontalScrollRecyclerView(Context context) {super(context);}public HorizontalScrollRecyclerView(Context context, AttributeSet attrs) {super(context, attrs);}public HorizontalScrollRecyclerView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public boolean onInterceptTouchEvent(MotionEvent e){switch (e.getAction()){case MotionEvent.ACTION_DOWN://按下startX = e.getX();startY = e.getY();isScrolling = false;boolean intercepted = super.onInterceptTouchEvent(e);//先交给父类让他自己判断是否拦截,防止影响父视图正常纵向滑动if (intercepted) {getParent().requestDisallowInterceptTouchEvent(true);}break;case MotionEvent.ACTION_MOVE:float endX = e.getX();float endY = e.getY();float distanceX = Math.abs(endX - startX);float distanceY = Math.abs(endY - startY);if (distanceX > distanceY) {//如果是横向滚动,取消父视图拦截isScrolling = true;getParent().requestDisallowInterceptTouchEvent(true);}else {getParent().requestDisallowInterceptTouchEvent(false);}break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:break;}return super.onInterceptTouchEvent(e);}
}
注意:父容器不能拦截ACTION_DOWN事件,因为此事件不受FLAG_DISALLOW_INTERCEPT标记位控制,父容器一旦拦截,所有事件都无法传递到子元素中
父容器也需要修改为默认拦截除ACTION_DOWN以外事件,保证当子元素设置
getParent().requestDisallowInterceptTouchEvent(false);
时,父元素能正常拦截所需事件。
外部拦截
指点击事件都先经过父容器拦截处理
从父视图下手,重写onInterceptTouchEvent方法,在父视图需要拦截时拦截事件,否则返回false
import android.content.Context;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;public class MySwipeRefreshLayout extends SwipeRefreshLayout{private float startX;private float startY;private float mTouchSlop;public MySwipeRefreshLayout(Context context) {super(context);mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}public MySwipeRefreshLayout(Context context, AttributeSet attrs) {super(context, attrs);mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {switch (ev.getAction()){case MotionEvent.ACTION_DOWN:startX = ev.getX();startY = ev.getY();break;case MotionEvent.ACTION_MOVE:float distanceX = Math.abs(ev.getX() - startX);float distanceY = Math.abs(ev.getY() - startY);if(distanceX > mTouchSlop && distanceX > distanceY){ //判断为横向滑动return false;}break;}return super.onInterceptTouchEvent(ev);}
}
注意:ACTION_DOWN事件必须返回false,否则后续事件就会直接交给父容器处理;
ACTION_UP必须返回false,否则子元素无法接收此事件,无法响应onClick
