Android记一次debug——TouchDelegate的坑
bug
黑色是button,最外部的是ViewGroup,给ViewGroup设置了一个TouchDelegate以此扩大button的点击区域。但是发现点击了额外多出来的点击区域后,ViewGroup就再也响应不了点击事件了。
我一开始猜测是业务的问题,压根没想到Delegate会有坑。于是看了好久的业务,实在定位不到原因所在。
最后不得已回顾了Delegate的源码,果然有问题
入口
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
点进去
public boolean onTouchEvent(MotionEvent event) { int x = (int)event.getX(); int y = (int)event.getY(); boolean sendToDelegate = false; boolean hit = true; boolean handled = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Rect bounds = mBounds; if (bounds.contains(x, y)) { mDelegateTargeted = true; sendToDelegate = true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_MOVE: sendToDelegate = mDelegateTargeted; if (sendToDelegate) { Rect slopBounds = mSlopBounds; if (!slopBounds.contains(x, y)) { hit = false; } } break; case MotionEvent.ACTION_CANCEL: sendToDelegate = mDelegateTargeted; mDelegateTargeted = false; break; } if (sendToDelegate) { final View delegateView = mDelegateView; if (hit) { // Offset event coordinates to be inside the target view event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2); } else { // Offset event coordinates to be outside the target view (in case it does // something like tracking pressed state) int slop = mSlop; event.setLocation(-(slop * 2), -(slop * 2)); } handled = delegateView.dispatchTouchEvent(event); } return handled; }
注意这段代码
if (bounds.contains(x, y)) { mDelegateTargeted = true; sendToDelegate = true; }
点击button自然是不会把事件流转到view group的onTouchEvent里。点击额外的部分,这个时候就会把事件流转到view group里了,然后进行delegate的判断。进去之后,只要成功符点击了额外区域,也就是上面这段代码,一个成员变量mDelegateTargeted就会被保存未true。
继续看这段代码
sendToDelegate = mDelegateTargeted; if (sendToDelegate) { Rect slopBounds = mSlopBounds; if (!slopBounds.contains(x, y)) { hit = false; } }
这里会对sendToDelegat赋值为true。
最下面一段
if (sendToDelegate) { final View delegateView = mDelegateView; if (hit) { // Offset event coordinates to be inside the target view event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2); } else { // Offset event coordinates to be outside the target view (in case it does // something like tracking pressed state) int slop = mSlop; event.setLocation(-(slop * 2), -(slop * 2)); } handled = delegateView.dispatchTouchEvent(event); } return handled;
这个判断就会进去了。
handled = delegateView.dispatchTouchEvent(event);
然后就把点击事件交给孩子,看看它要不要。我们这里没有对button进行特殊复写,而且他天生就是可点击的(事实上,大部分 view默认都是不可点击的,在设置了点击事件之后,就可点击了)。如果你仔细看过view的onTouchEvent的源码,你就会发现不管是ENABLE还是DISABLE,他都会返回clickable,所以可点击,就会返回true了。因此return handled等价于return true。
回到入口
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
第二个if里的判断成功返回true了,所以view(这个view group)的onTouchEvent就返回true了,成功消费(不过这是一次 特殊的事件转移,算view group消费还是button消费,都可以)。