支持触摸拖动的 TouchDelegate

支持触摸拖动的 TouchDelegate

需求

最近有一个小需求,就是在界面上有一个预览图片的区域,这个区域用户可以双击缩放图片、双指自由缩放图片、触摸图片进行移动,对图片的局部区域进行查看,像这样:
0.gif

这个功能可以使用 github 中的开源库 PhotoView 实现,地址:https://github.com/Baseflow/PhotoView

同时另一个条件是当前这个图像预览区域较小,不像上图这样预览区域比较大,大概是 4 分之 1 左右,如果直接使用 PhotoView 设置图片,那么用户体验可能较差,因为图像展示区域较小,难以进行自由的图像移动预览操作,所以这个需求就是:扩大触摸区域,让用户在图像显示区域的外侧也可以自由的对图像进行移动预览操作。如下,白色区域是 PhotoView 的父控件的区域,用户触摸和双击这里时,可对图像进行预览操作:
1.gif

先把 PhotoView 放置在布局中,然后设置一张图片,PhotoView 本身会支持图片的双击和双指缩放与移动预览,如果要实现上述的需求,当时想到了两种解决方案:

  1. 将 PhotoView(图像预览控件)的父控件的触摸事件传递给 PhotoView,那么用户触摸在父控件上时,PhotoView 将会接收到触摸事件,从而控制内部图像的缩放和移动;
  2. 先将 PhotoView 本身尺寸变大,填满白色区域,再想办法将图片显示区域限制到需要的大小,需要手动使用代码调整图片显示到限制区域

显然第二种方法是需要对 PhotoView 控件进行额外处理和调整,与 PhotoView 紧密相连,第一种方法只需要将触摸事件传递给 PhotoView 即可,那么第一种方法更灵活和通用,所以优先选择第一种方案进行处理

采取第一种方案,通常可以自定义一个 ViewGroup,将它作为 PhotoView 的父控件,并重写它的 dispatchTouchEvent 方法,将事件全部传递给 PhotoView 处理。但这种方法不够灵活,因为需要创建一个特定的 ViewGroup 类型。那么还有没有其他的方法?发现 Android 提供了一个 TouchDelegate 的类,使用它可以扩大一个 View 的触摸区域,听起来挺符合当前需求的场景,那么计划使用 TouchDelegate 来实现这个需求。

首先看一下 TouchDelegae 的基本用法

TouchDelegate 用法

public static void expandViewTouchBounds(final View view, final View parent) {
   
    Rect bounds = new Rect();
    bounds.left = 0;
    bounds.top = 0;
    bounds.right = parent.getWidth();
    bounds.bottom = parent.getHeight();

    TouchDelegate touchDelegate = new TouchDelegate(bounds, view);
    parent.setTouchDelegate(touchDelegate);
}
// 扩展 photoView 的触摸区域到 parent 上
expandViewTouchBounds(photoView, parent);

使用方法很简单,首先创建一个 TouchDelegate 对象,然后设置给父控件,其中 bounds 为在父控件中扩展的触摸区域,坐标相对于父控件,示例中设置为整个父控件的区域。

Bug

本以为这样就可以了,通过测试发现在 PhotoView 外部的区域用单个手指触摸移动,图像预览区域根本无法跟着移动,但是却可以响应双击和双指的缩放事件。难道是用法的不对。

通过查看 TouchDelegate 的源代码发现,其实 TouchDelegate 是无法支持移动的触摸事件的响应的,只能支持 click 事件。关键代码如下:

/**
 * Forward touch events to the delegate view if the event is within the bounds
 * specified in the constructor.
 *
 * @param event The touch event to forward
 * @return True if the event was consumed by the delegate, false otherwise.
 */
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;
                if (!slopBounds.contains(x, y)) {
   
                    hit = false;
                }
            }
            break;
        case MotionEvent.ACTION_CANCEL:
            sendToDelegate = mDelegateTargeted;
            mDelegateTargeted = false;
            break;
    }
    if (sendToDelegate) {
   
        if (hit) {
   
            // Offset event coordinates to be inside the target view
            event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.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 = mDelegateView.dispatchTouchEvent(event);
    }
    return handled;
}

核心逻辑并不复杂,其中 mDelegateView 为被代理处理事件的 View,也就是 PhotoView。

主要关注下面 if (sendToDelegate) { . . . } 代码块中的逻辑,当触摸事件发生点的坐标落在 bounds 内,那么 hittrue,执行:

event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);

setLocation 的含义是重新设置 MotionEvent 的 x,y 坐标,也就是 getX()getY() 的返回值。

那么从这里看出来,无论触摸事件的 action 是什么,都会将触摸坐标设置为被代理 View 的中心点,自然用手指触摸 View 外部区域移动时图像并不会跟着移动了。不过双击事件还是没有问题的,可正常检测到。

按照需求,肯定需要支持触摸拖动预览图片,结合目前情况,只能自己修复 TouchDelegate 中的事件传递逻辑了。父控件比 PhotoView 的区域大,当用户触摸到父控件区域内时,应该怎样处理?通常一个简单的规则就是按比例映射,按照触摸点 x、y 坐标在父控件内所占父控件尺寸的比例,设置在相对于子 View 区域相同的宽高比例的坐标上。

改进 TouchDelegate

根据分析,那么需要改进 TouchDelegate 原始处理逻辑中将坐标固定的问题,代码如下:


                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值