通过《Android事件分发(一)》我们了解了Android的事件分发机制,不熟悉的,可以回头再去看一遍。
有了这方面的知识基础,我们来解决实际研发的过程中,老生常谈的事件冲突问题。
解决这类问题,其实是有方法的。下面先介绍这两种方法,然后再结合以上三种问题,分别进行讲解
方法一:外部拦截法
顾名思义,外部拦截法,就是指在外部进行拦截,让事件不传递下去。其实就是对外部的dispatchTouchEvent和onInterceptTouchEvent动手脚
方法二:内部拦截法
事件冲突大致分为三种
1.方向一致
我们用ScrollView嵌套ListView,同为上下方向的滑动。
我们假设一个需求:
1.listView向下滑时,如果还没有滑到listView的头部的话,listView继续滑动;否则ScrollView滑动。
2.listView向上滑时,如果还没有滑到listView的底部的话,listView继续滑动;否则ScrollView滑动。
<com.example.weights.CustomScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#0000ff"
android:text="头部" />
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0000ff"
app:adapter="@{da}" />
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#0000ff"
android:text="尾部" />
1.1 外部拦截法
重写ScrollView的onInterceptTouchEvent
int tempY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;//表示是否拦截
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
tempY = y;//记录按下时的Y坐标
intercept = super.onInterceptTouchEvent(ev);//这里返回的是false。不然,不会调用其他Action方法
break;
case MotionEvent.ACTION_MOVE:
if(null != listView){
if(listView.getFirstVisiblePosition() == 0 && y > tempY){//list显示第一个,而且是向下滑
intercept = true;//ScrollView拦截
break;
}else if(listView.getLastVisiblePosition() == listView.getCount() - 1 && y < nowY){//list显示最后一个,而且是向上滑
intercept = true;
break;
}
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
break;
}
return intercept;
}
所谓外部拦截法,其实就是外部父容器做处理。在这里,当父容器ScrollView拦截事件后,就会调用父容器ScrollView自己的onTouchEvent,即ScrollView处理滚动,否则交给ListView消费。
2.1 内部拦截法
int nowY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {//内部拦截法
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
nowY = y;
getParent().requestDisallowInterceptTouchEvent(true);//禁止父容器拦截事件
break;
case MotionEvent.ACTION_MOVE:
View topView = getChildAt(0);
View bottomView = getChildAt(getChildCount() - 1);
if(y > nowY && getFirstVisiblePosition() == 0 && null != topView && topView.getTop() == 0){//往下滑,并且是第一项,父容器拦截
getParent().requestDisallowInterceptTouchEvent(false);
}else if(y < nowY && getLastVisiblePosition() == getCount() - 1 && null != bottomView && bottomView.getBottom() == getHeight()){//往上滑,并且是第后一项,父容器拦截
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(ev);
}
所谓内部拦截法,其实就是在内部子容器里,通过requestDisallowInterceptTouchEvent方法,控制父容器是否拦截事件。requestDisallowInterceptTouchEvent是ViewParent接口的一个抽象方法,实现是在ViewGroup里面
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;//注意这里
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
注意mGroupFlags这个变量,回想下,在ViewGroup的dispatchTouchEvent方法里面有这么一段代码
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;
}
这里说明一点,在使用内部拦截法的时候,需要重写父容器的onInterceptTouchEvent,
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){//这里需要返回false,因为如果这里返回true,事件就不会往下传递,导致子View无法接收事件
return false;
}else{//其他事件拦截。已配合子容器调用requestDisallowInterceptTouchEvent使用
return true;
}
}
这里的ScrollView没有重写,是因为ScrollView本身已经重写了onInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (super.onInterceptTouchEvent(ev)) {
return true;
}
...
return mIsBeingDragged;
}
2.方向不一致
典型的例子,就是ViewPager和ListView。不过,ViewPager已经做了处理,有兴趣的可以看看ViewPager的源码。
解决套路还是外部拦截和内部拦截,在判断条件上,需要做些调整。归根结底还是根据X和Y的滑动距离来判断是否消费事件。
3.多层嵌套,方向不一致
解决套路还是外部拦截和内部拦截,根据需要,把之拆开成以上两种情况,然后解决,这里就不多做介绍了,有兴趣的朋友,可以自行尝试。
胖子总结
- 自己写个demo,试一试,比看十遍文章都有用
- 温馨提示:点成线,线成面,切勿贪心,否则一脸懵逼
- 胖子有什么理解错误的,欢迎大家指出来,一起讨论、学习、进步
- 期待胖子为了巩固、加深基础的《Java虚拟机(JVM)》