Android进阶 -事件冲突与解决方案大揭秘

1,133 阅读11分钟

Android高级进阶 -事件冲突与解决方案大揭秘

前言:

事件冲突在开发过程中经常碰到,比如说2个可以滑动的布局ViewPager和RecyclerView

这两个滑动的时候ViewPager左右滑动,RecyclerView上下滑动

本篇给大家由简单的冲突到常见的冲突,模拟滑动冲突进而跟进源码找到问题并解决问题!

事件分发流程

滑动冲突之前,先来温习一下事件分发流程吧~

在Android中当我们点击了屏幕首先会先执行到:

Activity.java

public boolean dispatchTouchEvent(MotionEvent ev) {
   	   ...
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

在这里getWindow()获取的是Window对象,在Android中只有一个PhoneWindow继承子Window,所以直接在PhoneWindow里面找superDispatchTouchEvent()

	PhoneWindow.java
	
 	private DecorView mDecor;
	@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

这里的DecorView 继承自 FrameLayout , FrameLayout继承自ViewGroup,所以最终点击事件会发送到ViewGroup的dispatchTouchEvent()来进行事件分发

简单的滑动冲突

同一个按钮,设置setOnClickListener和setOnTouchListener不同的效果

 	bt.setOnClickListener(v -> {
            Log.i("点击事件:", "OnClick");
        });

        bt.setOnTouchListener((v, event) -> {
                    Log.i("点击事件:", "OnTouchClick" + event.getAction());
                    return true;
                }
        );

结果图:


可以看出setOnTouchListener设置返回值为true后,setOnClickListener()事件则不会进行处理
why?
\

走近View的dispatchTouchEvent()方法来一探究竟!

View.java

public boolean dispatchTouchEvent(MotionEvent event) {
		...
		 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;
            }
		....
}

在这个方法中咋们先看看li.mOnTouchListener.onTouch(this, event)是什么

public interface OnTouchListener {
        boolean onTouch(View v, MotionEvent event);
    }

这个onTouch(this, event)就是绿框选中的new View.OnTouchListener();
(红框 和 绿框的值是一样的,红框使用的是java8的特性lambda表达式)
在这里插入图片描述
在来看看setOnTouchListener做了什么事情:

View.java

 public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }
    
	ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

看到这段代码,就应该明白,如果一旦调用了setOnTouchListener()那么传入的参数一定不为null,setOnClickListener()也是同样的道理

在来看一眼dispatchTouchEvent()方法

View.java

public boolean dispatchTouchEvent(MotionEvent event) {
		...
		 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;
            }
		....
}

在第一个if中判断都是用的&& ,既然onTouch()已经执行了,那么前面的条件一定是满足的.

如果li.mOnTouchListener.onTouch(this, event)返回为true 那么一旦执行了if 最终 result = true;

紧接着再来看第二个if判断,因为由第一个判断知道result = true那么第二个if这段代码就不会执行也包括onTouchEvent(event)

在来看看onTouchEvent(event)方法中做了什么事情:

View.java

 public boolean onTouchEvent(MotionEvent event) {
			 ....
		  if (!post(mPerformClick)) {
              performClickInternal();
            }
            ....
	}
View.java

private boolean performClickInternal() {
        notifyAutofillManagerOnClick();
        return performClick();
    }
View.java

public boolean performClick() {
		...
		if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        ....
        return result;
}

最终在performClick()方法中发现了onClick()

在回过头来顺一下思路:

当点击事件来临的时候,会执行到View.dispatchTouchEvent()

然后会先判断通过mOnTouchListener.onTouch(this, event)判断是否将result变量赋值为true

如果为true就不执行onTouchEvent()方法,

onTouchEvent()方法调用了performClickInternal(),

performClickInternal()调用了 performClick()

onClick就在performClick()方法中

常见滑动冲突

布局ViewPager包含RecyclerView():

在这个小案例里面,ViewPager包含RecyclerView

所以ViewPager是ViewGroup, RecyclerView是View

MyViewPager.java

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev) / true / false
    }

对应效果图:

super.onInterceptTouchEvent(ev)(默认)truefalse

咋们不管默认的,默认的google给处理好了,现在是模拟的冲突之后要如何解决

点击响应事件

首先要了解有那些事件onInterceptTouchEvent()会响应

ACTION_DOWN(0)ACTION_MOVE(1)ACTION_UP(2)ACTION_CANCEL(3)
按压滑动 (会响应多次)抬起被父容器拦截

首先执行的就是ACTION_DOWN事件,DOWN事件是一切事件的开始,如果没有DOWN事件,就没有其他的事件

众所周知onInterceptTouchEvent()是是否拦截事件,true为拦截事件 false为不拦截事件

那么问题就来了,为什么返回了true就不能上下滑动了?

为什么返回了false不能左右滑动了?它的原理是什么?

接下来一步一步带领大家通过看源码的方式找到问题并解决问题!

当一个事件来临的时候还是会经过activity…phoneWindow…最终走到ViewGroup的dispatchTouchEvent()中,会先执行DOWN事件,然后在执行MOVE事件等

DOWN事件源码流程

注:源码很枯燥,一遍绝对看不懂,我2个小时候的视频看了4遍才总结的这么点,希望大家要耐心耐心耐心😘😘

在ViewGroup的dispatchTouchEvent()中:

  • 首先会把ACTION_DOWN事件清空
ViewGroup.java

            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
  • 紧接着判断是否拦截事件
ViewGroup.java

			//判断事件是否拦截 为DOWN事件的时候 disallowIntercept为false
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //intercepted 为true 表示拦截事件  false表示不拦截事件
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); 
                } else {
                    intercepted = false;
                }

在这里需要注意的是onInterceptTouchEvent() 为true表示拦截事件 ,false表示不拦截事件

  • 拦截事件 ------ start ------

onInterceptTouchEvent() 返回true:

ViewGroup.java

		if (mFirstTouchTarget == null) {
                //是否处理事件
                //如果为false表示没有处理事件 true处理事件
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 

dispatchTransformedTouchEvent()如果为false表示没有处理事件 true处理事件

ViewGroup.java

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
		if (child == null) {
            //会走到View的dispatchTouchEvent ViewGroup真正处理还是交给View
            //handled 为false表示为处理事件 true表示处理事件
            handled = super.dispatchTouchEvent(transformedEvent);
        }
		return handled;
}

最终这个事件交给View的dispatchTouchEvent()处理,就走到了文章最开头的那块代码,先判断OnTouch然后再onCilck等等

结论:如果ViewPager设置onInterceptTouchEvent为 true ,那么就不会走到事件分发直接事件拦截自己(ViewPager)消费了,所以他的子View(RecyclerView)就不会接收到事件


不接受事件,他就不会滑动!!

  • 拦截事件 ------ stop ------
  • 不拦截事件 ------ start ------

不拦截事件表示onInterceptTouchEvent为 false

再来接着看ViewGroup.dispatchTouchEvent()方法

如果不拦截事件那么就分发事件:

ViewGroup.java

 public boolean dispatchTouchEvent(MotionEvent ev) {
				//判断事件是否拦截 为DOWN事件的时候 disallowIntercept为false
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //DOWN 事件一定可以进来 判断是否 拦截
                    intercepted = onInterceptTouchEvent(ev);
                }
                
			//如果 intercepted 为 false 则分发或处理事件
			if (!canceled && !intercepted) {
						...
						
						 //倒序取出
                        for (int i = childrenCount - 1; i >= 0; i--) {
							....
                            //child.canReceivePointerEvents()  判断View能否接收事件
                            // isTransformedTouchPointInView()  判断是否在点击范围内
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }
                     }
			}
	}

先来看看for循环里面的这个if

  • child.canReceivePointerEvents() 判断View能否接收事件
View.java
		protected boolean canReceivePointerEvents() {
		        //判断View能否接收事件
		        //1. 是否是VISIBLE状态
		        //2. 是否使用Animation移动了
		        return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
		    }
  • isTransformedTouchPointInView() 判断是否在点击范围内
ViewGroup.java
protected boolean isTransformedTouchPointInView(float x, float y, View child,
                                                    PointF outLocalPoint) {
       		.....
        return isInView;
    }

什么是判断是否在点击范围内?

假设现在点击红色区域,应该响应的是红色区域,而不是绿色区域

在这个分发事件的时候,首先判断你是否是DOWN事件,

然后判断你是否有子View如果有然后倒叙排序(倒叙的原因是为了让最上层的View为第一个响应)

如果当前的View是VISIBLE(隐藏)状态或者通过Animation移动到了其他的位置,或者不再范围之内,那么就不分发

如果满足条件那么还是通过dispatchTransformedTouchEvent()分发

 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){...}
  • 判断是否在 分发处理事件 范围之内
  • dispatchTransformedTouchEvent 如果为false继续循环出下一个
  • dispatchTransformedTouchEvent 会询问他的子类是否需要处理事件
  • 如果为false表示没有处理事件 true处理事件
  • dispatchTransformedTouchEvent的关键在于第三个参数(child)
 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
			
			if (child == null) {
	            ....
	        } else {
	            final float offsetX = mScrollX - child.mLeft;
	            final float offsetY = mScrollY - child.mTop;
	            transformedEvent.offsetLocation(offsetX, offsetY);
	            if (!child.hasIdentityMatrix()) {
	                transformedEvent.transform(child.getInverseMatrix());
	            }
	
	            //如果子View 是ViewGroup 则会会继续执行ViewGroup的dispatchTouchEvent  相当于递归
	            //如果子View 是 View 则分发事件进行onTouchEvent处理
	            handled = child.dispatchTouchEvent(transformedEvent);
	        }
}

最终通过child.dispatchTouchEvent(transformedEvent);进行分发

  • 如果子View 是ViewGroup 则会会继续执行ViewGroup的dispatchTouchEvent 相当于递归
  • 如果子View 是 View 则分发事件进行onTouchEvent处理

结论:如果onInterceptTouchEvent()返回false,那么事件就会通过dispatchTransformedTouchEvent()全部分发给子View(RecyclerView)

  • 不拦截事件 ------ stop ------

MOVE事件源码流程

执行完DOWN事件之后,紧接着就是执行MOVE事件,MOVE不分发事件

还是继续来看dspatchTouchEvent()方法

ViewGroup.java

public boolean dispatchTouchEvent(MotionEvent ev) {
			...
			boolean alreadyDispatchedToNewTouchTarget = false;
			
              //是否处理事件
			if (mFirstTouchTarget == null) {
					...
			} else {
				 TouchTarget target = mFirstTouchTarget;
					//这里while子循环一次
                while (target != null) {
                		//第一次的时候next = null 因为 target.next 在addTouchTarget()中赋值为null
                   		 final TouchTarget next = target.next;
							if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
							...
							 } else {
							 		//最终在这里分发给子View事件
									if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {...}
									else{
											...
										if (predecessor == null) {
		                              		  mFirstTouchTarget = next;
			                            } else {
			                                  predecessor.next = next;
			                            }
	                            }
					     	}
					   }
              	  }
			}
}
  • alreadyDispatchedToNewTouchTarget默认为false,走到分发才会变为true,MOVE事件不分发事件
  • whlie只会循环一次,因为target在addTouchTarget()中赋值为null,当第二次执行MOVE的时候就会被赋值,这里whlie循环的目的是为了多点MOVE事件
    在这里插入图片描述

mFirstTouchTarget是一个链表 在这next第一次执行MOVE事件的时候

因为MOVE事件不走分发事件 next 是在分发事件中的 addTouchTarget()方法执行的

所以这个next = null = mFirstTouchTarget(红色部分)

当第二个MOVE事件来临的时候,就直接走(绿色部分) if(mFirstTouchTarget == null)的事件分发了

解决思路

一切的拦截与分发,都是通过onInterceptTouchEvent()方法来改变的,那么如果可以不执行onInterceptTouchEvent()即可有办法解决

ViewGroup.java

public boolean dispatchTouchEvent(MotionEvent ev) {

		 //这里还有一个清除DOWN的方法

		  final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //DOWN 事件一定可以进来 判断是否 拦截
                    //intercepted为true 表示拦截事件  false表示不拦截事件
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
} 

但是咋们可以通过改变disallowIntercept 的值从而控制onInterceptTouchEvent()

disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

requestDisallowInterceptTouchEvent()介绍:

 @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
		...
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
		...
    }
  • requestDisallowInterceptTouchEvent(true) 如果现在有事件存在,不让父容器拿到事件
  • requestDisallowInterceptTouchEvent(false); 给父容器事件

然后还需要注意的是:

ViewGroup.java

public boolean dispatchTouchEvent(MotionEvent ev) {
			//如果是DOWN事件 则吧之前状态清空
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
}

因为我们都知道,DOWN是所有事件的开始,如果没有DOWN事件,则没有MOVE事件,没有UP事件

都没有按压,如何移动,如何抬起?

所以在处理事件冲突的时候,在ViewGroup时,不拦截DOWN事件,将DOWN事件传递给子View,拦截MOVE事件

结论:ViewGroup不能拦截DOWN事件,事件冲突解决可以在子View的MOVE事件中进行

解决办法

内部解决法:

	RecyclerView.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case ACTION_DOWN://按压
                //不给父容器事件
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case ACTION_MOVE://移动
                int moveX = x - mLastX;
                int moveY = y - mLastY;
                if (Math.abs(moveX) > Math.abs(moveY)) {
                    //给父容器事件
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    Log.i("ACTION_MOVE", "垂直滑动");
                }
                break;
            case ACTION_UP: //抬起
                break;
            case ACTION_CANCEL://被父容器拦截
                Log.i("ACTION_CANCEL", "RecyclerView");
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
ViewPager.java

   //是否拦截事件 true 拦截事件  false不拦截事件
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(ev);
            return false;
        }

        //在move事件的时候,拦截事件
        return true;
    }

外部拦截法

ViewPager.java

//外部拦截发
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        if (ev.getAction() == ACTION_DOWN) {
            mX = (int) ev.getX();
            mY = (int) ev.getY();
        }

        if (ev.getAction() == ACTION_MOVE) {
            int moveX = mX - x;
            int moveY = mY - y;
            if (Math.abs(moveX) < Math.abs(moveY)) {
                Log.i("ACTION_MOVE", "垂直滑动");
                //如果是垂直滑动就不拦截
                return false;
            } else {
                Log.i("ACTION_MOVE", "水平滑动");
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

总结

  • 1.先判断是否拦截 拦截走第三步,不拦截走第二步
  • 2.不拦截,就分发 如果子view响应事件就走第四步
  //如果没有进入分发 mFirstTouchTarget 会一直为null
            //newTouchTarget = mFirstTouchTarget = null
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
           alreadyDispatchedToNewTouchTarget = true;
  • 3.拦截或分发处理
  if (mFirstTouchTarget == null) {
             // No touch targets so treat this as an ordinary view.
             //是否处理事件
              //如果为false表示没有处理事件 true处理事件
              handled = dispatchTransformedTouchEvent(ev, canceled, null,
                      TouchTarget.ALL_POINTER_IDS);
            }

4。不拦截:

所有子View都不拦截事件,最终走到第三步(拦截或分发处理)

重点:

  • 通过 dispatchTransformedTouchEvent()分发给子View事件 如果为 false表示没有处理事件 true处理事件 false即子View的onTouchEvent返回false,ture也是同理
  • onInterceptTouchEvent() 是否拦截事件
  • mFirstTouchTarget是一个链表的TouchTarget
  • addTouchTarget()是当前事件的触摸目标(X,Y坐标)
  • MOVE不分发事件
  • 处理事件冲突,只能在MOVE事件时处理
  • getParent().requestDisallowInterceptTouchEvent(true); 允许父容器拿到事件
  • getParent().requestDisallowInterceptTouchEvent(false); 不允许父容器拿到事件

完整代码

去博主主页

原创不易,您的点赞就是对我最大的支持~