拦截的艺术
先来看接口ViewParent中的一个函数
/**
* Called when a child does not want this parent and its ancestors to
* intercept touch events with
* {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
*
* <p>This parent should pass this call onto its parents. This parent must obey
* this request for the duration of the touch (that is, only clear the flag
* after this parent has received an up or a cancel.</p>
*
* @param disallowIntercept True if the child does not want the parent to
* intercept touch events.
*/
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
参数disallowIntercept的意思就是childView告诉父容器要不要进行拦截
true :告诉所有父控件不要拦截,事件交由childrenView处理
Code 1
//该代码展示的是站在View的角度,在子View的dispatchTouchEvent()中解决冲突的实战代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
/**
* 子View的所有父ViewGroup都会跳过onInterceptTouchEvent的回调
* 相当于ViewGroup的流程是dispatchTouchEvent() --> onTouchEvent()
*/
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {//水平滑动,使得父类可以执行onInterceptTouchEvent
/**
* 不让所有的父容器跳过onInterceptTouchEvent()
* 相当于ViewGroup的流程是:dispatchTouchEvent() --> onInterceptTouchEvent() --> onTouchEvent()
* 也就是ViewGroup拦截以后交给自己的onTouchEvent()来处理
*/
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
false:告诉所有父控件拦截。在父控件的onInterceptTouchEvent()中可能类似这样的处理
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
if(event.getAction() == MotionEvent.ACTION_DOWN){
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
}else{
//如果是非down事件,说明子View并没有拦截父类的onInterceptTouchEvent
//说明该事件交由父类处理,所以不需要再传递给子类,返回true
return true;
}
}
然后接下来的事情交给父控件的onTouchEvent()来处理
Ok,咱们回到拦截的艺术,主要分两个角度
解决冲突的两个角度
ChildView的角度
- 在View的onTouch()或者子控件的dispatchTouchEvent中,根据具体的业务逻辑来告诉父容器要不要拦截。两种思路下代码都以给出,参见Code 1 , 2
Code 2
// //该代码展示的是站在View的角度,在子View的onTouchEvent()中解决冲突的实战代码
mListView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
mLastY = event.getY();
}
if(action == MotionEvent.ACTION_MOVE) {
int top = mListView.getChildAt(0).getTop();
float nowY = event.getY();
if(!isSvToBottom) {
// 允许scrollview拦截点击事件, scrollView滑动
mScrollView.requestDisallowInterceptTouchEvent(false);
} else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
// 允许scrollview拦截点击事件, scrollView滑动
mScrollView.requestDisallowInterceptTouchEvent(false);
} else {
// 不允许scrollview拦截点击事件, listView滑动
mScrollView.requestDisallowInterceptTouchEvent(true);
}
}
return false;
}
});
ViewGroup角度
- 在onInterceptTouchEvent()中根据具体业务逻辑判断要不要拦截事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//down事件不拦截,否则无法传给子元素
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
//水平滑动则拦截
if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//不拦截,否则子元素无法收到
intercepted = false;
break;
}
...
return intercepted;
}
冲突解决实战
上下滑动与上下滑动的冲突型
ScrollView嵌套ListView
-
网上的解决思路分析:
- 根据展示数据的个数乘以每一个Item的高度,计算出ListView的总体高度,然后动态的设置ListView的高度
- 复写ListView的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,让ListView完全展开
这两种方式最突出的缺陷在于ListView的子控件不能复用,容易造成OOM。
分析冲突点:嵌套之后ListView的高度无法完全显示,且两者存在事件冲突
-
解决思路:无法显示,复用的问题我们需要在布局中明确的设置ListView的高度。冲突根据ScrollView的状态和ListView的状态共同决定,冲突的解决选择在子View的onTouch()中解决。
- ScrollView在未滑动到底部时候,如果点击并滑动ListView时候,ListView是不能滑动的,不禁止。
- 如果ScrollView滑动到底部,且ListView已经到顶部,继续下拉ListView,其实会拉动ScrollView,不禁止。
- 如果ScrollView滑动到底部,用户向上滑,ListView滑动,禁止ScrollView截断点击事件能力
对于ScrollView的滑动到底部的监听我们可以这样做
boolean isSvToBottom = false;
scroll_view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 判断 scrollView 当前滚动位置在顶部
if(scroll_View.getSrollY == 0){
isSvToBottom = true;
}
// 判断scrollview 滑动到底部
// scrollY 的值和子view的高度一样,这人物滑动到了底部
if (scroll_view.getChildAt(0).getHeight() - scroll_view.getHeight()
== scroll_view.getScrollY()){
isSvToBottom =false;
}
return false;
}
});
mListView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
mLastY = event.getY();
}
if(action == MotionEvent.ACTION_MOVE) {
int top = mListView.getChildAt(0).getTop();
float nowY = event.getY();
if(!isSvToBottom) {
mScrollView.requestDisallowInterceptTouchEvent(false);
} else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
mScrollView.requestDisallowInterceptTouchEvent(false);
} else {
mScrollView.requestDisallowInterceptTouchEvent(true);
}
}
return false;
}
});
子控件部分接管型
场景:SwipeRefreshLayout --> ScrollView --> ViewPager,这种布局场景很常见,多见于应用的首页,如美团。
分析冲突点:ScrollView的上下滑动与ViewPager的左右滑动原本是不冲突的,但是加入了SwipeRefreshLayout之后,事件场景就变得复杂。当滑动ViewPager的时候,很容易就触发了SwipeRefreshLayout的下来刷新。
解决思路:因为ViewPager只涉及X轴方向上的滑动,所以我们在MotionEvent_ACTION_MOVE的时候去动态的判断X周方向上是否超过了我们设定的阈值,这个值我们可以自定义,也可以去拿系统的这个值
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
mViewPager.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
mLastX = event.getX();
}
if(action == MotionEvent.ACTION_MOVE) {
if(Math.abs(event.getX() - mLastX) > 60f) {
mRefreshLayout.setEnabled(false);
mScrollView.requestDisallowInterceptTouchEvent(true);
}
}
if(action == MotionEvent.ACTION_UP) {
mScrollView.requestDisallowInterceptTouchEvent(false);
mRefreshLayout.setEnabled(true);
}
return false;
}
});
子控件完全接管事件型
ScrollView嵌套Map(第三方地图控件)
分析冲突点:外层套了ScrollView的Map就接受不到事件了,但Map的应用场景是只要用户在Map上触摸,也就是MotionEvent_ACTION_DOWN时候,说明就是要操作Map,就跟ScrollView没有关系。
解决思路:直接在子View的onTouch中告诉ScrollView(父控件)要不要拦截
mMapView.getChildAt(0).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_UP){
mScrollView.requestDisallowInterceptTouchEvent(false);
}else{
mScrollView.requestDisallowInterceptTouchEvent(true);
}
return false;
}
});
总结
本文重要从子View的角度(内部),和ViewGroup(外部)的两种角度去解决事件冲突的思路,并给出了一些常见的冲突场景以及解决案例,具体的冲突点需要具体分析,主要是分两点来确定
- 确定冲突的有关控件
- 找准冲突发生的点
- 确定是用内部还是外部的方式
注意:内部解决优先于外部解决思路