重叠view事件响应问题
工作当中遇到一个需求:有两个重叠并且全屏的Framelayout,交互逻辑是点击上层的button使得上层消失,下层可见;再点击下层使得下层消失上层出现。但是发现当点击上层非button区域时,下层也会响应点击事件,但是点击button区域则不会。这意味着点击事件穿过上层到达下层。对于安卓事件机制一知半解的我无从下手,上网搜了一下发现在上层FrameLayout下增加:
android:clickable="true"
这样的确可以保证上层点击事件不会到达下层,可是这是为什么呢?最初步的猜想是当上层FrameLayout的clickable属性为true时,点击事件已经被上层拦截了。这个和当你点击button时响应onclick()时的原理是不是一样?这就需要从源码分析了。
安卓事件分发机制基础
分析源码前先大致了解一下安卓事件分发机制。
- 当点击事件到达一个viewgroup,首先会调用dispatchTouchEvent。如果该viewgroup的onInterceptTouchEvent返回true,表明拦截该事件,接着事件就会交给viewgoup来处理,意味着它的onTouchEvent会进行事件的处理。如果返回false,表明不拦截当前事件。这时事件会继续传递给它的子view,遍历调用子view的dispatchTouchEvent,直到事件被某个子view处理。如果所有子view都没有处理该事件,则事件会传递给父容器的onTouchEvent。以此类推,如果所有元素都不处理该事件,事件最终会传递给Activity处理,即Activity的onTouchEvent;
- 当一个view调用dispatchTouchEvent进行事件处理时,由于没有子元素无法进行向下传递只能自己处理。首先会判断是否有设置OnTouchListener,如果OnTouchListener的onTouch返回true,那么onTouchEvent就不会调用。
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
- 当view调用onTouchEvent处理事件时,从代码中可以看出只要view的Clickable和long_clickable中有一个为true,那么它就会消耗这个事件,即onTouchEvent返回true.
public boolean onTouchEvent(MotionEvent event) {
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
..
}
结论
综合上述对安卓事件分发的源码分析知道,当在上层FrameLayout中将clickable设置为true时,其实就是
当点击事件到达上层framelayout时,在onTouchEvent中对事件进行拦截,使得点击事件不会继续向下传递。当然,除了这个方法以外,也可以重写FrameLayout的onInterceptTouchEvent,在将这个函数的返回值为true,也可以实现拦截事件的作用。
网友评论