实话实说,之前并不知道有TouchDelegate,直到最近查看view的源码时候才发现这个新大陆.
在view中有一个私有的TouchDelegate变量:
private TouchDelegate mTouchDelegate = null;
在view中的公共方法setTouchDelegate可以给这个view设置一个TouchDelegate对象,源码如下:
public void setTouchDelegate(TouchDelegate delegate) { mTouchDelegate = delegate; }
到了这里,你可能会问:view中的mTouchDelegate到底可以做什么?为了回答这个问题,下面分三步来解释.
第一步:首先来看看TouchDelegate类的一段原文说明,在文件TouchDelegate.java有如下说明:
Helper class to handle situations where you want a view to have a larger touch area than its
actual view bounds. The view whose touch area is changed is called the delegate view. This
class should be used by an ancestor of the delegate.
其大意是: TouchDelegate是一个工具类,其目的是让一个view在一个特定的位置拥有比自己实际的触摸区域更大的可触摸的区域.触摸区域被更改的view被称作"delegate view".这个工具类应该被"delegate view"的父view使用.
通过上面的意思,我们可以明白其实TouchDelegate的主要目的就是来扩大一个view的触摸区域的.
第二步:要深刻理解,我觉得需要查看源码,看看他是如何实现的.
查看TouchDelegate可以知道,TouchDelegate是一个很简单的类,主要有四个变量和两个方法:
/** * View that should receive forwarded touch events */ private View mDelegateView;//需要扩大触摸区域的view /** * Bounds in local coordinates of the containing view that should be mapped to the delegate * view. This rect is used for initial hit testing. */ private Rect mBounds;//定义了这个扩大的触目区域 /** * mBounds inflated to include some slop. This rect is to track whether the motion events * should be considered to be be within the delegate view. */ private Rect mSlopBounds;//这是一个相对于mBounds溢出的触摸区域:实际上就是比mBounds大一点的区域(宽高分别大8),其目的时消除触摸误差. public TouchDelegate(Rect bounds, View delegateView) {//构造函数:bounds 表示触摸的区域;delegateView 需要扩大触摸区域的view mBounds = bounds; mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();//这里获取的是触摸滑动距离的判断:就是触摸滑动距离为mSlop时候裁判为是在触摸滑动move.其默认值为8 mSlopBounds = new Rect(bounds); mSlopBounds.inset(-mSlop, -mSlop);//这里扩大触摸滑动区域(宽高扩大8),其目的时消除触摸时候的误差.达到一个触摸安全处理. mDelegateView = delegateView; //需要扩大触摸区域的view(需要改变触摸区域的view) } public boolean onTouchEvent(MotionEvent event) {//这是核心代码:判断当前触摸是否在这个区域:mBounds.如果在是则会让这次的触摸事件真的在mDelegateView真实的区域内. int x = (int)event.getX(); int y = (int)event.getY();//记录这次触摸点位置 boolean sendToDelegate = false;//标记这次触摸是否有效(应该传递给mDelegateView) boolean hit = true; //标记这次触摸是否在这个区域内(mBounds) boolean handled = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Rect bounds = mBounds; if (bounds.contains(x, y)) {//ACTION_DOWN是否在这个区域内 mDelegateTargeted = true;//标记这一次触摸事件在这个区域(在mDelegateView内) 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) {//这次触摸有效:则把触摸事件传递给 mDelegateView final View delegateView = mDelegateView; //模拟这次触摸真的在mDelegateView区域内:从新计算event的触摸点,以保证这次的event事件的触摸点在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);//把计算后的触摸事件传递给mDelegateView的dispatchTouchEvent使其相应触摸事件. } return handled; }
第三步:最后来看看view里面是何时使用mTouchDelegate.
上面说了,在view里面定义来一个变量mTouchDelegate来保存当前这个view的TouchDelegate对象,其目的是确保view有这样的功能:原本在自己区域的触摸事件实际上相应的却是别的地方(别的区域)的view.
那view是如何实现的呢? 查看源码可以知道,在view的onTouchEvent方法里面,首先就会去判断自己是否有可用的TouchDelegate对象,如果有,那么onTouchEvent方法里面首先会去执行mTouchDelegate的
onTouchEvent方法.下面是源码:
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if (DBG_MOTION) { Xlog.d(VIEW_LOG_TAG, "(View)onTouchEvent 1: event = " + event + ",mTouchDelegate = " + mTouchDelegate + ",enable = " + isEnabled() + ",clickable = " + isClickable() + ",isLongClickable = " + isLongClickable() + ",this = " + this); } if ((viewFlags & ENABLED_MASK) == DISABLED) { /// M: we need to reset the pressed state or remove prepressed callback either up or cancel event happens. final int action = event.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } else if ((mPrivateFlags & PFLAG_PREPRESSED) != 0) { Xlog.d(VIEW_LOG_TAG, "View onTouch event, if view is DISABLED & PFLAG_PREPRESSED, remove callback mPrivateFlags = " + mPrivateFlags + ", this = " + this); removeTapCallback(); } } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } if (mTouchDelegate != null) {//判断是否有TouchDelegate对象 if (mTouchDelegate.onTouchEvent(event)) {执行onTouchEvent return true;//如果这个触摸事件在TouchDelegate设置的区域内,这返回.不会执行其他 } } .....
最后,来看看一个实例.下面是一个父view夸大自己view触摸区域的方法:
public static void enlargeBtnTouchScope(View parent, Button btn, Rect rect, float width, float height){ rect.top = btn.getTop(); rect.bottom = btn.getBottom(); rect.left = btn.getLeft(); rect.right = btn.getRight(); rect.top -= height; rect.bottom += height; rect.left -= width; rect.right += width; parent.setTouchDelegate(new TouchDelegate(rect, btn)); }
上面的代码可以使得在parent_view的button可以更好地被触摸到(被点击到).