TouchDelegate

可以通过设置TouchDelegate 给View的父类来实现点击事件的区域扩充(拦截父View的Touch事件)

   View child;   ViewGroup parent;   // 上下左右各扩充10px的点击范围   int sizeDifference =  10;   Rect delegateArea = new Rect();   delegateArea.right += sizeDifference;   delegateArea.bottom += sizeDifference;   delegateArea.left -= sizeDifference;   delegateArea.top -= sizeDifference;   TouchDelegate delegate = new TouchDelegate(bounds, child);   parent.setTouchDelegate(delegate);

系统提供的默认TouchDelegate有一定的局限性:

  • 一个ViewGroup下只能添加一个View的点击区域扩充
  • 点击区域扩充, 不能超出父View的显示区间

查看TouchDelegate的实现, 可以看到TouchDelegate的处理方式就是拦截Touch事件,然后判断事件范围是否是在扩充区域内,然后根据是否是点击,来决定事件坐标系是否转换成View内部的 并直接通过dispatchTouchEvent方法来触发delegateView的Click事件,来看下onTouchEvent中的实现:

    public boolean onTouchEvent(@NonNull MotionEvent event) {             int x = (int)event.getX();        int y = (int)event.getY();        boolean sendToDelegate = false;        boolean hit = true;        boolean handled = false;        switch (event.getActionMasked()) {                 case MotionEvent.ACTION_DOWN:               // 判断当前是否命中范围                mDelegateTargeted = mBounds.contains(x, y);                sendToDelegate = mDelegateTargeted;                break;            case MotionEvent.ACTION_POINTER_DOWN:            case MotionEvent.ACTION_POINTER_UP:            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_MOVE:                sendToDelegate = mDelegateTargeted;                if (sendToDelegate) {                         Rect slopBounds = mSlopBounds;                    // 判断是否是hit                    if (!slopBounds.contains(x, y)) {                             hit = false;                    }                }                break;            case MotionEvent.ACTION_CANCEL:                sendToDelegate = mDelegateTargeted;                mDelegateTargeted = false;                break;        }        if (sendToDelegate) {                 if (hit) {                    // 将Event坐标转换为View的中心位置并                // Offset event coordinates to be inside the target view                event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);            } else {                     // 将Event坐标转换到View的触控范围以外, 避免触发按下态                // 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 = mDelegateView.dispatchTouchEvent(event);        }        return handled;    }

为了解决多个View的问题我们可以将传入的bounds 和 delegateView 换成一个列表, 为了解决点击范围限制的问题, 我们可以将TouchDelegate 设置到RootView上,然后命中区间转换都转换成相对于RootView的相对坐标,而不是使用相对于父View的相对坐标。
首先我们先定义一个类用来存储delegateView 和 bounds信息:

    class TouchDelegateItem {             // 扩展点击范围        Rect appendRect;        // 扩展后的点击范围,相对于root        Rect clickBounds = new Rect();        // 扩展后的点击溢出范围, 相对于root        Rect slopBounds = new Rect();        // delegateView的弱引用        WeakReference<View> viewRef;        // 相对于Window的location 用于计算和root的相对坐标        int[] location = new int[2];    }

然后我们给TouchDelegateItem补充更新这些数据对应的方法:

        void updateClickBoundsAndSlop(int[] rootLocation, int slop) {                 if (viewRef == null || viewRef.get() == null) {                     return;            }            View view = viewRef.get();            view.getLocationInWindow(location);            clickBounds.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());            // 坐标转换 计算相对位置            clickBounds.offsetTo(location[0] - rootLocation[0], location[1] - rootLocation[1]);            // 追加扩展区域            clickBounds.left -= appendRect.left;            clickBounds.right += appendRect.right;            clickBounds.top -= appendRect.top;            clickBounds.bottom += appendRect.bottom;            // 设置溢出区域             slopBounds.set(clickBounds);            slopBounds.inset(-slop, -slop);        }        boolean contains(int x, int y) {                 return viewRef != null && viewRef.get() != null && clickBounds.contains(x, y);        }        boolean slopContains(int x, int y) {                 return viewRef != null && viewRef.get() != null && slopBounds.contains(x, y);        }

然后回头来看TouchDelegate的构造方法,我们改造一下, 只接受一个ViewGroup用作root 然后通过方法可以追加代理的View:

    public IVOSTouchDelegate(ViewGroup viewGroup) {             super(new Rect(), viewGroup);        mSlop = ViewConfiguration.get(viewGroup.getContext()).getScaledTouchSlop();        mRootView = viewGroup;        viewGroup.setTouchDelegate(this);    }

然后我们添加对TouchDelegateItem的增,删,查方法,因为涉及到View 所以我们要尽可能避免内存泄漏使用弱引用,并检查无效的数据及时清除:

    public void addDelegateItem(@NonNull View view, int left, int top, int right, int bottom) {             if (mDelegateItems == null) {                 mDelegateItems = new ArrayList<>();        }        // 避免重复添加        TouchDelegateItem item = removeDelegateItem(view);        if (item != null) {                 item.appendRect.set(left, top, right, bottom);        } else {                 item = new TouchDelegateItem(view, new Rect(left, top, right, bottom));        }        mDelegateItems.add(item);    }    public @Nullable    TouchDelegateItem removeDelegateItem(View view) {             if (mDelegateItems == null || mDelegateItems.isEmpty()) {                 return null;        }        for (int i = 0, n = mDelegateItems.size(); i < n; ) {                 TouchDelegateItem item = mDelegateItems.get(i);            if (item.viewRef.get() == null) {      // 清除无效数据                mDelegateItems.remove(i);            } else if (item.viewRef.get() == view) {                     return mDelegateItems.remove(i);            } else {                     i++;            }        }        return null;    }    private TouchDelegateItem getDelegateTarget(int x, int y, boolean needUpdate) {             if (mCurTouchDelegateItem != null && mCurTouchDelegateItem.contains(x, y)) {                 return mCurTouchDelegateItem;        }        if (mDelegateItems == null || mDelegateItems.isEmpty()) {                 return null;        }        mRootView.getLocationInWindow(mRootLocation);        for (int i = 0, n = mDelegateItems.size(); i < n; i++) {                 TouchDelegateItem item = mDelegateItems.get(i);            if (needUpdate) {                     item.updateClickBoundsAndSlop(mRootLocation, mSlop);            }            if (item.slopContains(x, y)) {                     return item;            }        }        return null;    }

然后我们再添加一个总的开关:

    public void doDelegate(boolean doDelegate) {             this.mDoDelegate = doDelegate;    }

然后我们复写onTouchEvent方法,将父类中的事件处理copy出来进行改造,将坐标范围判断,改为从添加的TouchDelegateItem的列表中查找:

    @Override    public boolean onTouchEvent(MotionEvent event) {             if (!mDoDelegate) {                 return false;        }        int x = (int) event.getX();        int y = (int) event.getY();        boolean hit = true;        boolean handled = false;        TouchDelegateItem cur = null;        switch (event.getActionMasked()) {                 case MotionEvent.ACTION_DOWN:                mCurTouchDelegateItem = getDelegateTarget(x, y, true);                cur = mCurTouchDelegateItem;                break;            case MotionEvent.ACTION_POINTER_DOWN:            case MotionEvent.ACTION_POINTER_UP:            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_MOVE:                cur = mCurTouchDelegateItem;                if (cur != null && cur.contains(x, y)) {                         if (!cur.slopContains(x, y)) {                             hit = false;                    }                }                break;            case MotionEvent.ACTION_CANCEL:                cur = mCurTouchDelegateItem;                mCurTouchDelegateItem = null;                break;            default:                break;        }        if (cur != null) {                 final View delegateView = cur.viewRef.get();            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;    }

更多相关文章

  1. listview中放Button 点击 长按事件
  2. Android事件分发机制解析
  3. Android事件处理(6)
  4. Android(安卓)GridView宫格视图(一) 运用--BaseAdapter
  5. Android(安卓)EditText回车不换行
  6. Android(安卓)4.0 事件输入(Event Input)系统
  7. 初学Android,图形图像之在指定点(坐标)播放动画(三十五)
  8. Android(安卓)自定义SeekBar 实现分段显示不同背景颜色
  9. Android中关于键盘管理,点击除editText外的区域收起键盘

随机推荐

  1. Android: Listen outgoing/incoming call
  2. android gpuimage显示的缩放和剪裁模式
  3. Android中的Parcelable接口
  4. Android弹窗
  5. Android版本对照
  6. 组件居中显示 安卓
  7. Eclipse 运行Android程序在虚拟机中,出现
  8. android源代码下载及编译
  9. Android图文布局【整理】
  10. Android(安卓)开源项目分类汇总