鸿洋版ViewGroup事件分发机制
View滑动冲突处理方法(外部拦截法、内部拦截法)
Android View 滑动冲突解决方式以及原理
案例
添加一个自定义的LinearLayout:
package com.example.zhy_event03;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class MyLinearLayout extends LinearLayout
{
private static final String TAG = MyLinearLayout.class.getSimpleName();
public MyLinearLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "onInterceptTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "onInterceptTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
{
Log.e(TAG, "requestDisallowInterceptTouchEvent ");
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
更改xml文件
<com.example.zhy_event03.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<com.example.zhy_event03.MyButton
android:id="@+id/id_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="click me" />
</com.example.zhy_event03.MyLinearLayout>
ViewGroup事件运行顺序为VeiwGroup的dispatchTouchEvent -> VeiwGroup的onInterceptTouchEvent ->View的dispatchTouchEvent ->View的onTouchEvent
源码
为什么会这样呢?我们来看一下ViewGroup的dispatchTouchEvent
源码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onFilterTouchEventForSecurity(ev)) {
return false;
}
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) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
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)) {
// offset the event to the view's coordinate system
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)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
} ....//other code omitted
代码比较长,决定分段贴出,首先贴出的是ACTION_DOWN事件相关的代码。
16行:进入ACTION_DOWN的处理
17-23行:将mMotionTarget置为null
26行:进行判断:if(disallowIntercept || !onInterceptTouchEvent(ev))
两种可能会进入IF代码段
1、当前不允许拦截,即disallowIntercept =true,
2、当前允许拦截但是不拦截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false ;
注:disallowIntercept 可以通过viewGroup.requestDisallowInterceptTouchEvent(boolean);进行设置,后面会详细说;而onInterceptTouchEvent(ev)可以进行复写。
36-57行:开始遍历所有的子View
41行:进行判断当前的x,y坐标是否落在子View身上,如果在,47行,执行child.dispatchTouchEvent(ev),就进入了View的dispatchTouchEvent代码中了,如果不了解请参考:Android View的事件分发机制,当child.dispatchTouchEvent(ev)返回true,则为mMotionTarget=child;然后return true;
ViewGroup的ACTION_DOWN分析结束,总结一下:
ViewGroup实现捕获到DOWN事件,如果代码中不做TOUCH事件拦截,则开始查找当前x,y是否在某个子View的区域内,如果在,则把事件分发下去。
如何拦截
复写ViewGroup的onInterceptTouchEvent方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
int action = ev.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
//如果你觉得需要拦截
return true ;
case MotionEvent.ACTION_MOVE:
//如果你觉得需要拦截
return true ;
case MotionEvent.ACTION_UP:
//如果你觉得需要拦截
return true ;
}
return false;
}
默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这要该事件就不会往子View传递了,并且如果你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。
原因很简单,当onInterceptTouchEvent(ev) return true的时候,会把mMotionTarget 置为null ;
如何不被拦截
如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;此时子View希望依然能够响应MOVE和UP时该咋办呢?
Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:
@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
getParent().requestDisallowInterceptTouchEvent(true);
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
getParent().requestDisallowInterceptTouchEvent(true); 这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。
总结
1、如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
2、可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
3、子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;
View滑动冲突处理方法(外部拦截法、内部拦截法)
1、外部拦截法 (子view代码无需修改)(符合view事件分发机制)
说明:需要在父ViewGroup,重写onInterceptTouchEvent方法,根据业务需要,判断哪些事件是父Viewgroup需要的,需要的话就对该事件进行拦截,然后交由onTouchEvent方法处理,若不需要,则不拦截,然后传递给子view或子viewGroup。
代码:
public boolean onInterceptTouchEvent(MotionEvent ev) {
int y= (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
yDown=y;
isIntercept=false;
break;
case MotionEvent.ACTION_MOVE:
yMove=y;
if (yMove-yDown<0){ //根据业务需求更改判断条件,判断是时候需要拦截
isIntercept=false;
}else if(yMove-yDown>0&&getChildAt(0).getScrollY()==0){
isIntercept=true;
}else if(yMove-yDown>0&&getChildAt(0).getScrollY()>0){
isIntercept=false;
}
break;
case MotionEvent.ACTION_UP:
isIntercept=false;
break;
}
return isIntercept; //返回true表示拦截,返回false表示不拦截
}
总结
- 外部拦截法主要是父容器去控制事件的拦截,若事件是父容器需要的,则进行拦截,不需要的则向下传递。
- 父容器不能拦截DOWN事件或者UP事件。
2.内部拦截法(父viewgroup需要重写onInterceptTouchEvent)(不符合view事件分发机制)
说明:顾名思义就是在子view中拦截事件,父viewGroup默认是不拦截任何事件的,所以,当事件传递到子view时,
子view根据自己的实际情况来,如果该事件是需要子view来处理的,那么子view就自己消耗处理,如果该事件不需要由子view来处理,那么就调用getParent().requestDisallowInterceptTouchEvent()方法来通知父viewgroup来拦截
这个事件,也就是说,叫父容器来处理这个事件,这刚好和view的分发机制相反。
代码: **(需要注意,要确保MotionEvent.ACTION_DOWN时不拦截)**
//子view的代码·
public boolean dispatchTouchEvent(MotionEvent ev) {
int y= (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
yDown=y;
break;
case MotionEvent.ACTION_MOVE:
yMove=y;
Log.e("mes",yMove+"!!!");
int scrollY = getScrollY();
if (scrollY == 0&&yMove-yDown>0) { //根据业务需求判断是否需要通知父viewgroup来拦截处理该事件
//允许父View进行事件拦截
Log.e("mes",yMove-yDown+"拦截");
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
总结
- 内部拦截法是将事件控制权交给子View,若子View需要事件,则对事件进行处理,不需要则将事件传递给父ViewGroup,让父ViewGroup处理。
- 子View通过调用父ViewGroup的requestDisallowInterceptTouchEvent来干预父ViewGroup对事件的拦截状况
- 父ViewGroup不能拦截DOWN事件,至于MOVE或者UP事件的拦截状态要根据具体的情景