View的事件分发机制小结
事件的分发原理图:
- 对于一个root viewgroup来说,如果接受了一个点击事件,那么首先会调用他dispatchTouchEvent方法.
- viewgroup的onInterceptTouchEvent 返回true,那就代表要拦截这个事件.接下来这个事件就给viewgroup自己处理了,从而viewgroup的onTouchEvent方法就会被调用.
- viewgroup的onInterceptTouchEvent返回false就代表我不拦截这个事件,然后就把这个事件传递给自己的子元素,然后子元素的dispatchTouchEvent就会被调用,就是这样一个循环直到 事件被处理.
我们可以看下事件分发的原理图.
图1:
简单的说只要各事件不消费,返回false,分发就会一直走下去:
dispatchTouchEvent(false) -> onInterceptTouchEvent(false) -> onTouchEvent(false) - 事件结束
重要的事情说一遍:
也就是说在任何View或者ViewGrop中只要它想消费Touch事件,就直接onInterceptTouchEvent(true)
,这样它就不会把事件传下去给孩子view了,而是自己消费.
知其人先知其心,我们继续进一步了解下其他事件分发的api.
- dispatchTouchEvent 分发事件
return false; //不是目标对象,则分发,默认false;
return true; // 是目标view,则不分发;
dispatchTouchEvent作用是将touch事件向下传递直到遇到被触发的目标view.
我们可以通过返回的boolean
对touch事件的分发进行处理,是否要向下分发寻找目标view,当然这个方法也可以被重载,手动分配事件.
- onInterceptTouchEvent 拦截事件
return false; //表示不拦截,默认false;
return true; // 表示拦截;
拦截是相当于它的孩子,也就是说不会拦截自己.
如果拦截,则TouchEvent
会传给他自己,而它孩子是接收不了.
如果不拦截会继续往他的孩子递归是否onInterceptTouchEvent
需要拦截.
- onTouchEvent 触摸事件
return false; //表示不消费,默认false;
return true; // 表示消费;
当onInterceptTouchEvent 确认拦截,会问自己是否要消费
TouchEvent
,如果拦截了又不消费则,那么Touch结束.
- invalidate 重新绘制
让整个view失效,这样view会被重新调用, 配合onDraw()使用.
下面是调用流程:
- 当invalidate时会重新调用draw方法;
- draw会调用onDraw,而在draw内还会调用computeScroll();
- 此时如果想让computeScroll()循环被调用可以在computeScroll()内自己调用postInvaildate()重新绘制;
invalidate刷新UI步骤: draw() -> onDraw() -> computeScroll()
computeScroll() 源码是空实现,具体实现由自己来写.
开发中事件分发的常见问题(重点)
-
view的onTouchEvent,OnClickListerner和OnTouchListener的onTouch方法 三者优先级如何?
答:
onTouchListener
优先级最高,也就是说如果onTouch方法返回 true ,那么事件结束,反之如果返回false,那么onTouchEvent
讲会被调用,至于OnClickListerner
优先级是最低的.
优先级如下:
OnTouchListener > onTouchEvent > OnClickListerner -
点击事件的传递顺序如何?
答: -
Activity > Window > View.从上到下依次传递.
-
如果你最低的那个view
onTouchEvent
返回false 那就说明他不想处理 那就再往下抛,都不处理的话最终就还是让Activity自己处理了。 -
举个例子,pm下发一个任务给leader,leader自己不做 给架构师a,小a也不做 给程序员b,b如果做了那就结束了这个任务。b如果发现自己搞不定,那就找a做,a要是也搞不定 就会不断向上发起请求,最终可能还是pm做。
-
总结下流程: view的事件分发会从上往下,只要在子不消费的情况,又会接着从下往上,最后结束.
//activity的dispatchTouchEvent 方法 一开始就是交给window去处理的
//win的superDispatchTouchEvent 返回true 那就直接结束了 这个函数了。返回false就意味
//这事件没人处理,最终还是给activity的onTouchEvent 自己处理 这里的getwindow 其实就是phonewindow
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//来看phonewindow的这个函数 直接把事件传递给了mDecor
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//devorview就是 我们的rootview了 就是那个framelayout 我们的setContentView里面传递的那个layout
//就是这个decorview的 子view了
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
- enable是否影响view的onTouchEvent返回值?
我们知道其实enable
的优先级高于cliable
,当enable=false
时会屏蔽view的点击事件.而事实上enable=false
并不会影响onTouchEvent返回true.
答:
- 不影响,只要clickable和longClickable有一个为真,那么onTouchEvent就返回true。
- 设置了enable为false的话,onClick事件是完全屏蔽的,而clickable属性就要看设置属性和设置OnClicListener的先后顺序了.
我们可以看下面demo
xml代码:
android:clickable="true"
android:enabled="false"
android代码:
//我在XML布局中设置了enabled="false",虽然是屏蔽了点击事件,但是在自定义Button中,实现的`onTouchEvent`方法还是会返回true.
public class MyButton extends Button {
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
System.out.println("onTouchEvent:"+super.onTouchEvent(event));
return super.onTouchEvent(event);
}
}
输出结果:
onTouchEvent:true
- 滑动冲突问题如何解决思路是什么?
答: - 让谁消费滑动:
要解决滑动冲突 其实最主要的就是有一个核心思想。你到底想在一个事件序列中,让哪个view 来响应你的滑动?比如 从上到下滑,是哪个view来处理这个事件,从左到右呢? - 拦截内外滑动:
用业务需求来想明白以后剩下的其实就很好做了。
核心的方法就2个:- 外部拦截
也就是父拦截.(重写父控件的onInterceptTouchEvent即可). - 内部拦截
也就是子view拦截方法.
- 外部拦截
学会这2种,基本上所有的滑动冲突.都是这2种的变种,而且核心代码思想都一样,下面是两种情况是示例代码:
- 外部拦截法:思路就是重写父控件的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因为这和android自身的事件处理机制 逻辑是一模一样的.
父控件示例代码:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
//down事件肯定不能拦截 拦截了后面的就收不到了
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (你的业务需求) {
//如果确定拦截了 就去自己的onTouchEvent里 处理拦截之后的操作和效果 即可了
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//up事件 我们一般都是返回false的 一般父控件都不会拦截他。 因为up是事件的最后一步。这里返回true也没啥意义
//唯一的意义就是因为 父元素 up被拦截。导致子元素 收不到up事件,那子元素 就肯定没有onClick事件触发了,这里的
//小细节 要想明白
intercepted = false;
break;
default:
break;
}
return intercepted;
}
- 内部拦截法:内部拦截法稍微复杂一点,就是事件到来的时候,父控件不管,让子元素自己来决定是否处理。如果消耗了就最好,没消耗自然就转给父控件处理了。
子控件示例代码:
@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:
if (如果父控件需要这个点击事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}//否则的话 就交给自己本身view的onTouchEvent自动处理了
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
PS: 父控件代码也要修改一下,其实就是保证父控件别拦截down:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
事件分发的项目
需求:仿QQ侧滑菜单
- google给我们提供了
DrawerLayout
控件来作为侧滑菜单控件,但是侧滑菜单一般都是覆盖到主页面顶部的. - 我们需求在向右拖拉滑动时可以侧滑出菜单,并且要求菜单不可覆盖到主页面上,需求主页面跟着侧滑菜单的滑动和位移.
图2:
img-w230
上面红色框区域的结构可以这样设计ScrollView+多TextView.使用scrollView的原因是当菜单的item增加时可以滚动,当然listview,rv也可以做到.
问题分析:
- 当点击它任意一个孩子(TextView)时,如果ScrollView不进行onInterceptTouchEvent ,则它就不可以在菜单上进行左右滑动.
- 但是如果拦截了全部,则它的孩子又会消费不了TouchEvent.
问题解决:
- 只有左右移动的时候进行拦截,这样父控件就拥有了TouchEvent,可在菜单上继续左右滑动.
- 上下移动或静止的时候就不拦截,这样孩子又有了TouchEvent,那么孩子就可以点击了.
实例代码:
/**
* 当滑动的时候,需要拦截TouchEvent时间,让scrollView消化,否则会分发到孩子去;
* 当不滑动的停止的时候,不拦截,则会分发到孩子去,也就是TexView;
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
// 只有水平滑动时才拦截touch
case MotionEvent.ACTION_DOWN:
startX = (int) (ev.getRawX() + 0.5f);
startY = (int) (ev.getRawY() + 0.5f);
break;
case MotionEvent.ACTION_MOVE:
int newX = (int) (ev.getRawX() + 0.5f);
int newY = (int) (ev.getRawY() + 0.5f);
int dx = Math.abs(startX - newX);
int dy = Math.abs(startY - newY);
if (dx > dy) {
// 水平滑动,只有水平滑动才会拦截事件
return true;
}
startX = (int) ev.getRawX();// 初始化当前位置
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}