上篇文章分析了AndroidUI事件传递,点击冲突,接下来用一个例子继续学习ViewGroup的事件分发。
ViewGroup?它和普通的View有什么区别?
ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是android中所有布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。ViewGroup继承结构示意图如下所示:
我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。
接下来,移步郭霖大神的文章看一下源码分析:
Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
用一个图总结一下:
好啦,这样就解决了这两个问题:
- ViewGroup中Item点击事件和子控件的冲突;
- onTouch与onClick之间的冲突;
常见的ListView中的Item点击事件和子控件的冲突或者item点击没有反应的解决办法:
此时这些子控件会将焦点获取到,所以常常当点击item时变化的是子控件,item本身的点击没有响应。
这时候就可以使用descendantFocusability来解决啦,API描述如下:
android:descendantFocusability
该属性是当一个为view获取焦点时,定义viewGroup和其子控件两者之间的关系。属性的值有三种:
beforeDescendants:viewgroup会优先其子类控件而获取到焦点
afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点
通常我们用到的是第三种,即在Item布局的根布局加上android:descendantFocusability=”blocksDescendants”的属性。
好了,接下来就结合上篇文章应该就能理解UI点击事件传递与点击冲突了。好了,接下来 view的滑动冲突
Android中的滑动冲突
只要在界面中内外两层同时滑动的时候,就会产生滑动。意即有一个占主导地位的View抢着去执行滑动操作,从而带来非常差的用户体验。
常见的滑动冲突场景分为如下三种:
外部滑动方向与内部滑动方向不一致,主要是将ViewPager和Fragment配合使用所形成的页面滑动效果。
在这个效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个Listview。这种情况下本来是很容易发生滑动冲突的,但ViewPager内部处理了这种滑动冲突,所以如果使用ViewPager,则无需担心这个问题。 但如果使用的是Scroller,则必须手动处理滑动冲突了。否则后果就是内外两层只能有一层能够滑动。 具体来说是:根据滑动的方向判断到底由什么来拦截事件。
外部滑动和内部滑动方向一致,比如ScrollView嵌套ListView,或者是ScrollView嵌套自己。表现在要么只能有一层能够滑动,要么两者滑动起来显得十分卡顿。
处理规则:从业务上寻找突破点,比如业务上有规定:当处于某种状态时需要外部View处理用户的操作,而处理另一种状态时则让内部View处理用户的操作。
上面两种情况的嵌套。
处理规则:同场景二
滑动冲突的解决方式:
针对场景一的滑动冲突,有两种处理滑动的解决方式:
1.外部拦截法:
所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这个方法需要重写父容器的onInterceptTouchEvent方法。伪代码如下所示:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted=false;
int x=(int)event.getX();
int y=(int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted=false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要当前点击事件){
intercepted=true;
}else {
intercepted=false;
}
break;
case MotionEvent.ACTION_UP:
intercepted=false;
break;
default:
break;
}
mLastXIntercept=x;
mLastYIntercept=y;
return intercepted;
}
2.内部拦截法:
内部拦截法是指父容器不拦截任何事件,所有的事件传递给子元素,如果子元素需要此事件就直接消耗掉,如果不需要则交由父容器处理。需要配合requestDisallowInterceptTouchEvent方法才能正常工作。伪代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x=(int)event.getX();
int y=(int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX=x-mLastX;
int deltaY=y-mLastY;
if(父容器需要当前点击事件){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX=x;
mLastY=y;
return super.dispatchTouchEvent(event);
}
另外,为了使父容器不接收ACTION_DOWN事件,我们需要对父类进行一下修改:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action=event.getAction();
if (action==MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
以上两种方式,是针对场景一而得出的通用的解决方法。对于场景二和场景三而言,只需改变相关的滑动规则的逻辑即可。
注意:因为内部拦截法的操作较为复杂,因此推荐采用外部拦截法来处理常见的滑动冲突。