事件分发机制与NestedScrolling机制
一、事件分发机制
1.理论分析
事件分发涉及的是View
和ViewGroup
,相关事件:dispatchTouchEvent
、onInterceptTouchEvent
、OnTouchEvent
,其中onInterceptTouchEvent
只有ViewGroup才有这个方法。
当一个Touch事件到来时,它会从Activity向下依次分发,分发的过程是通过调用ViewGroup或View的dispatchTouchEvent方法实现的。详细的说就是根ViewGroup遍历包含的子view,如果子view处于事件触发范围内,调用每个子View的dispatchTouchEvent,当这个View为ViewGroup的时候,又会进行遍历其内部子view继续调用子view的dispatchTouchEvent。该方法有boolean类型的返回值,当返回true时,事件分发会中断,交给返回true的view的onTouchEvent处理事件,返回false才会执行下一个view的dispatchTouchEvent,如果子view的dispatchTouchEvent都返回false(其实就是view内部的onTouchEvent返回false),那么就会执行到父view的onTouchEvent。
下面是该过程分析的伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
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;
}
//如果是DOWN 则会遍历子view, 找出消耗事件的子view
//mFirstTouchTarget指向找到的子view, 并且子view处理事件
if(mFirstTouchTarget == null){
onTouchEvent() //执行自身OnTouchEvent
}else{
if(intercepted){
//给子view分发cancel事件,并把mFirstTouchTarget置为null
//下次move事件才会真正给自己处理
} else{
mFirstTouchTarget.child.dispatchTouchEvent() //子view处理事件
}
}
下面是简单的事件分发流程图:(针对down事件分析)
总结:其实就是dipatchTouchEvent层层向下分发,分发的过程中涉及onInterceptTouchEvent和OnTouchEvent的调用。对于ViewGroup,当其下面的所有子view都不处理事件的时候,才会调用到自己的onTouchEvent,对于View,dispatchTouchEvent里面就直接调用了onTouchEvent,然后根据返回值决定是否处理该事件。
如果某个父view的onInterceptTouchEvent返回false,之后每次事件都会询问是否拦截 如果onInterceptTouchEvent返回true,那么后续的move up事件将不再询问是否拦截,直接交给自己onTouchEvent处理
如果某个父view的onInterceptTouchEvent返回true,事件不会向下继续分发,会回调自己的onTouchEvent 如果自己的onTouchEvent返回false,则回调给父ViewGroup的onTouchEvent,如果自己的onTouchEvent返回true,那么后续的move up事件都给他
如果ACTION_DOWN遍历了所有view都没有找到处理事件的view,那么MOVE,UP事件将不会分发,即事件存在没有被消费的情况
2.事件冲突解决方法
1.外部拦截法
即父view根据需要对事件进行拦截,重写onInterceptTouchEvent
伪代码为:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if(满足父容器拦截要求){
intercept = true;
}else{
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
return intercept;
}
有几点需要注意:
1.ACTION_DOWN一定要返回false,否则如果返回true,那么后续的move up事件就会都交给父view处理,事件没有机会到达子view
2.ACTION_MOVE中在父view的需要的时候返回true,然后父view进行事件处理
3.原则上ACTION_UP也需要返回false,因为如果在move中没达到父view的拦截条件,up的时候返回true,那子view就收不到up事件,onClick等事件就无法触发。如果在move中达到了父view的拦截条件,那么up返回什么都无所谓了,因为都会直接交给父view处理。
2.内部拦截法
即子view根据实际情况允许或不允许父view拦截。
伪代码为:
//父view的实现
//父view除了ACTION_DOWN,其他都默认拦截 ACTION_DOWN不拦截主要是为了让子view能收到事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
//子child
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//不允许父view进行拦截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
//在不需要自己处理的时候,允许父容器进行拦截
if(不需要自己处理){
getParent().requestDisallowInterceptTouchEvent(false);
}else{
//在这里做自己的事情
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return true;
}
二、NestedScrolling机制
NestedScrolling机制是为了解决现在事件分发同一个事件中只能有一个view响应事件的问题,在这个机制中,父view和子view可以配合滚动,子view滚动之前会先询问父view是否要进行嵌套滚动,并且如果嵌套滚动的话,消耗多少,剩余的交给子view继续滚动。
现在android中很多组件已经实现了嵌套滚动的机制,比如RecyclerView和NestedScrollView,CoordinatorLayout等,当然我们也可以自定义,如果自定义的话我们首先需要了解下面的几个类:
- NestedScrollingChild 实现嵌套滚动的子view需要实现的接口
- NestedScrollingChildHelper 对实现嵌套滚动的子view提供的嵌套滚动工具类
- NestedScrollingParent 实现嵌套滚动的父view需要实现的接口
- NestedScrollingParentHelper 对实现嵌套滚动的父view提供的嵌套滚动工具类
public class MyNestedScrollingChild extends LinearLayout implements NestedScrollingChild {
private NestedScrollingChildHelper mNestedScrollingChildHelper;
private int[] offset = new int[2];
private int[] consumed = new int[2];
private int lastY;
public MyNestedScrollingChild(Context context) {
this(context, null);
}
public MyNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
}
public MyNestedScrollingChild(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
}
//需要重写onTouchEvent在需要的时候将滚动传给父容器,问他要消耗多少,剩下自己用多少
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = (int) event.getRawY();
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_VERTICAL;
//down的时候开始嵌套滚动
startNestedScroll(nestedScrollAxis);
break;
case MotionEvent.ACTION_MOVE:
int y = (int) (event.getRawY());
int dy = y - lastY;
lastY = y;
//滑动中
if (dispatchNestedPreScroll(0, dy, consumed, offset)) {
int remain = dy - consumed[1];
dispatchNestedScroll(0, consumed[1], 0, remain, offset);
} else {
scrollBy(0, -dy);
}
break;
case MotionEvent.ACTION_UP:
stopNestedScroll();
break;
}
return true;
}
/**
* 设置嵌套滑动是否能用
*
* @param enabled
*/
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
/**
* 判断嵌套滑动是否可用
*
* @return
*/
@Override
public boolean isNestedScrollingEnabled() {
return true;
}
/**
* 开始嵌套滑动,会查找有没有嵌套滑动的父view,如果有调用父view的onStartNestedScroll和onNestedScrollAccepted
*
* @param axes
* @return
*/
@Override
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}
/**
* 停止嵌套滑动 会回调父view的onNestStopScroll
*/
@Override
public void stopNestedScroll() {
mNestedScrollingChildHelper.stopNestedScroll();
}
/**
* 判断是否有父view支持嵌套滑动
*
* @return
*/
@Override
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}
/**
* 在开始滑动后,调用该方法通知父view滑动的距离,会回调父view的onNestPreScroll和onNestScroll方法
*
* @param dx
* @param dy
* @param consumed
* @param offsetInWindow
* @return
*/
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
/**
* 子view于父view后滑动
*
* @param dxConsumed
* @param dyConsumed
* @param dxUnconsumed
* @param dyUnconsumed
* @param offsetInWindow
* @return
*/
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
/**
* 在开始滑行后,调用该方法通知父view滑动的距离,会回调父view的onNestPreFling和onNestFling方法
*
* @param velocityX
* @param velocityY
* @return
*/
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
/**
* 子view于父view后滑行
*
* @param velocityX
* @param velocityY
* @param consumed
* @return
*/
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
}
public class MyNestedScrollingParent extends LinearLayout implements NestedScrollingParent {
private NestedScrollingParentHelper mNestedScrollingParentHelper;
public MyNestedScrollingParent(Context context) {
this(context, null);
}
public MyNestedScrollingParent(Context context, @Nullable AttributeSet attrs) {
super(context, attrs, 0);
}
public MyNestedScrollingParent(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
//这里需要根据实际情况进行消耗滑动的距离
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
//这里需要根据实际情况进行消耗滑动的距离
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
}
}