Android手势控制---双指缩放和单指移动

本文介绍了在Android中实现双指缩放和单指移动功能的思路。通过自定义ViewGroup并重写onTouchEvent,利用MotionEvent的ACTION_DOWN、ACTION_UP、ACTION_POINTER_DOWN等回调处理触摸事件。双指缩放通过计算双指间的距离变化调整子view大小,单指移动则根据手指移动距离改变子view位置。文中强调了防止无限缩小的限制,并提到在特定条件下直接调用设备控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

作为一个非科班程序员在Android行业已经摸爬滚打5年多了,一直待在小公司,自我感觉能力应该在中上甚至可以达到初高的水平了.前段时间的一次面试彻底给我打回了原形.面试官当时问我觉得自己对那块最熟悉,然后讲解一下.当时脑袋突然就一片空白,这些年大部分时间都是在独立开发,所有的东西都有涉及,但是没有哪块是自己真正深入了解的.反思自己的经历,所有的东西似乎都还停留在会用阶段.向下深入发现自己基础不够牢靠.重新深入学习Android和我个人规划又不相符,所以只能将日后的遇到的问题在此记录下来,主要记录思路,拒绝代码复制粘贴.

起因

公司最近在做一套智能监控系统,需要用到远程实时预览和摄像头控制功能.领导希望在实时预览的同时可以对视频进行双指缩放单指移动,类似米家的摄像头查看功能.大概在网上搜索了一遍,支持直播/缩放/手势的播放器没找到.手势控制绝大多数都是基于图片的.所以决定自己写一个ViewGroup将视频播放器包裹,拦截手势去处理.

实现思路

首先我们确定需要实现的功能有 双指缩放/放大后单指的拖动/原始比例下单指方向控制

实现这个功能我先自定义了ViewGroup,重写了onTouchEvent方法,返回true,对触摸事件进行了消费处理.如果子view有事件冲突可以在ViewGroup中拦截事件分发.
主要代码集中在onTouchEvent中,用到回调有:

  • MotionEvent.ACTION_DOWN (第一个触摸点按下)
  • MotionEvent.ACTION_UP (最后一个触摸点离开)
  • MotionEvent.ACTION_POINTER_DOWN / MotionEvent.ACTION_POINTER_2_DOWN (其他触摸点按下.注:ACTION_POINTER_2_DOWN方法已经废弃,使用ACTION_POINTER_DOWN代替,但是我在小米手机上测试还是回调ACTION_POINTER_2_DOWN,如果有其他触摸点可以使用ACTION_POINTER_3_DOWN等)
  • MotionEvent.ACTION_POINTER_UP (其他触摸点离开)
  • MotionEvent.ACTION_MOVE (触摸点移动)

通过getX() getY()记录原始触摸点坐标

 case MotionEvent.ACTION_DOWN:
                point_num = 1;
                x_position = event.getX();
                y_position = event.getY();
                break;
  
  case MotionEvent.ACTION_POINTER_DOWN:
  case MotionEvent.ACTION_POINTER_2_DOWN:
                point_num += 1;
                oldDis = (float) spacing(event);
                if (point_num == 2) {
                    x1_position = event.getX();
                    y1_position = event.getY();
                }

在MotionEvent.ACTION_MOVE方法中判断是单指还是双指

 case MotionEvent.ACTION_MOVE:
     if (point_num == 1) {
         //单指滑动
         moveView(event);
     } else if (point_num == 2) {
         //双指缩放
         scaleView(event);
     }
     break;

也可以使用getPointerCount()方法判断触摸点个数

因为单指移动涉及到放大后的拖动,所以先处理双指缩放问题,原理是通过双指的距离变化不断修改子view的LayoutParams,通过invalidate()方法重绘UI.
事件在ACTION_MOVE回调中处理,所以我们可以实时获取到双指的位置,通过getX(int pointerIndex)方法取到两个触摸点的位置,然后通过勾股定理计算出双指的距离

float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return Math.sqrt(x * x + y * y);

因为手指滑动后不离开屏幕ACTION_MOVE方法一直在回调,所以我们不能直接使用计算出来的双指距离,需要定义一个临时变量记录上一次的双指距离,每次的缩放都是相对于上一次回调而不是触摸时候的回调.
每次回调我们都修改子view的宽高为 原始值+将相对缩放值,为了保证缩放中心位置,同时修改leftMargin和topMargin

if (event.getPointerCount() == 2) {
    float scale = (float) ((spacing(event) - oldDis));
    float xScale = scale;
    float yScale = scale;
    params = (LinearLayout.LayoutParams) childView.getLayoutParams();
    if (widht + scale <= ScreenUtils.getScreenWidth()) {
        xScale = -(widht - ScreenUtils.getScreenWidth());
        params.leftMargin = 0;
    }
    if (height + scale <= ScreenUtils.getScreenWidth() / 16 * 9) {
        yScale = -(height - ScreenUtils.getScreenWidth() / 16 * 9);
        params.topMargin = 0;
    }
    params.height = (int) (height += yScale);
    params.width = (int) (widht += xScale);
    params.leftMargin = params.leftMargin == 0 && xScale < 0 ? 0 : params.leftMargin - (int) (xScale / 2);
    params.topMargin = params.topMargin == 0 && yScale < 0 ? 0 : params.topMargin - (int) (yScale / 2);
    childView.setLayoutParams(params);
    invalidate();
    oldDis = (float) spacing(event);
}

需要注意的是无限缩小的问题,我在这里限制如果缩小值+当前子view的宽高小于屏幕的宽高时,不允许继续缩小.暂时对放大没有限制.

单指拖动主要是在画面放大后对子view的位置移动,我们通过当前手指的位置-初始手指的位置来获得移动距离.

int slidLeft = (int) ((currentX_postion - x_position) * 0.03);
int slidTop = (int) ((currentY_position - y_position) * 0.03);

其中0.03是为了控制拖动速度
当子view的宽高和父view的宽高一致时,我们直接回调设备控制,不进行拖动

if ((widht <= ScreenUtils.getScreenWidth() + 5 && currentLeft == 0 && currentTop == 0) &&
        height <= ScreenUtils.getScreenWidth() / 16 * 9 + 5 && currentLeft == 0 && currentTop == 0) {
    return;
}

具体的逻辑由业务实现,可以通过回调告知业务处理

拖动时候需要注意view边界的问题.需要考虑上下左右各个方向

if (slidLeft > 0) {
    if (slidLeft + currentLeft > 0) {
        slidLeft = Math.abs(currentLeft);
    }
} else if (slidLeft < 0) {
    if (Math.abs(slidLeft + currentLeft) > widht - ScreenUtils.getScreenWidth()) {
        slidLeft = (int) (widht - ScreenUtils.getScreenWidth() + currentLeft);
    }
}
if (slidTop > 0) {
    if (slidTop + currentTop > 0) {
        slidTop = Math.abs(currentTop);
    }
} else if (slidTop < 0) {
    if (Math.abs(slidTop + currentTop) > height - ScreenUtils.getScreenWidth() / 16 * 9) {
        slidTop = (int) (height - ScreenUtils.getScreenWidth() / 16 * 9 + currentTop);
    }
}

在限定范围内修改滑动值以后,修改子view LayoutParams 的leftMargin和topMargin

params.leftMargin = (int) (currentLeft + slidLeft);
params.topMargin = (int) (currentTop + slidTop);
childView.setLayoutParams(params);
invalidate();

结束

只是一个简单的思路介绍,代码优化空间很大.

<think>我们参考上述引用,特别是引用[2]引用[3]中提到的实现方法。在移动端实现图片的双指缩放,通常需要监听touch事件(touchstart, touchmove, touchend),并通过计算两个手之间的距离变化来确定缩放比例,同时还要考虑缩放中心点的位置,以调整图片的位置(使用transform的scaletranslate)。 步骤: 1. 监听touchstart事件:记录初始的两个手的位置,计算初始距离,并记录初始的缩放状态(包括当前的缩放比例偏移量)。 2. 监听touchmove事件:当两个手移动时,计算新的距离,得到缩放比例(新距离/初始距离)。同时,计算两个手的中心点,这个中心点将作为缩放的中心。然后根据缩放比例中心点位置,计算出图片应该进行的缩放位移,并应用到图片的transform属性上。 3. 注意:在缩放过程中,为了保持中心点始终在图片的同一位置,我们需要调整图片的位置(即平移)。平移的计算需要根据缩放中心的变化来调整。 平移调整原理: 假设初始时,中心点在图片上的位置为(相对于图片左上角)(startX, startY)。在缩放过程中,这个点在图片上的位置应该保持不变。但是,由于图片进行了缩放,这个点在屏幕上的坐标会变化。我们需要通过平移来补偿这个变化。 具体计算: 设初始缩放比例为scale0,当前缩放比例为scale,初始平移为(translateX0, translateY0),当前平移为(translateX, translateY)。 在缩放过程中,中心点在屏幕上的位置也会移动(从(startCenterX, startCenterY)移动到(currentCenterX, currentCenterY)),但我们希望的是图片上的点(即中心点对应的图片位置)在屏幕上的位置保持不变(除了用户移动的情况)。因此,我们需要调整平移量。 一个常用的方法是:在每次touchmove事件中,根据当前中心点与初始中心点的偏移,以及缩放比例的变化,来更新平移量。 公式推导(简化): 设初始状态: 图片的变换:transform: translate(tx0, ty0) scale(s0); 中心点(屏幕坐标):(cx0, cy0) 该点在图片上的局部坐标(相对于图片左上角)为:(localX, localY) = ( (cx0 - tx0) / s0, (cy0 - ty0) / s0 ) 缩放后,比例变为s,我们希望同一个局部点(localX, localY)在屏幕上的位置为当前中心点(cx, cy)?但实际上,我们希望这个点相对于图片的位置不变,但由于缩放,这个点在屏幕上的位置会变化,所以我们需要调整平移,使得该点现在在屏幕上的位置为: x = tx + localX * s y = ty + localY * s 同时,由于中心点移动到了(cx, cy),我们希望这个点就是原来的点,所以: tx + localX * s = cx ty + localY * s = cy 因此,我们可以解出txty: tx = cx - localX * s ty = cy - localY * s 而localXlocalY由初始状态确定: localX = (cx0 - tx0) / s0 localY = (cy0 - ty0) / s0 所以: tx = cx - ( (cx0 - tx0) / s0 ) * s ty = cy - ( (cy0 - ty0) / s0 ) * s 但是,注意:我们每次touchmove事件中,当前的变换状态是上一次事件结束时的状态(即tx0, ty0, s0是上一次的状态)。而每次事件中,我们记录的是初始状态(即touchstart时的状态),然后根据当前的手位置计算新的变换。 然而,在实际操作中,我们通常会在touchstart时记录初始的变换状态(tx0, ty0, s0)初始的中心点(cx0, cy0)。然后在touchmove中,根据当前中心点(cx, cy)当前计算出的缩放比例s(= 当前距离/初始距离 * s0)来更新平移。 但是,上面的公式中,我们使用的是初始状态(touchstart时的状态)来计算localXlocalY,然后根据当前的中心点(cx, cy)当前比例s来计算新的平移。 因此,新的平移量: tx = cx - ( (cx0 - tx0) / s0 ) * s ty = cy - ( (cy0 - ty0) / s0 ) * s 注意:这里s = (当前距离 / 初始距离) * s0 这样,每次touchmove事件中,我们计算出s当前中心点(cx, cy),然后根据上述公式计算txty,然后应用transform: translate(tx, ty) scale(s)。 但是,我们还需要考虑边界情况,比如最小缩放最大缩放限制,以及单指拖动(平移)的情况。此外,为了流畅体验,还需要注意性能优化(如使用requestAnimationFrame)防止页面滚动(使用preventDefault)。 实现步骤: 1. HTML结构:一个容器包含图片,并设置overflow:hidden,图片使用transform进行变换。 2. 初始化:记录初始的变换状态(scaletranslate)。 3. 事件监听:touchstart, touchmove, touchend。 4. 在touchstart中,如果触点数量为2,则记录两个触点的位置,计算初始距离中心点,并记录当前的缩放比例位移(作为初始状态)。 5. 在touchmove中,如果触点数量为2,则计算当前两个触点的距离中心点,然后计算缩放比例s,再根据上述公式计算新的位移,然后应用变换。 6. 在touchend中,重置状态。 另外,我们还可以添加击事件来放大/缩小。 代码结构示例(使用原生JavaScript): 注意:由于移动端touchmove事件频繁触发,我们需要使用requestAnimationFrame来优化渲染,并且注意在touchmove事件中阻止默认行为(防止页面滚动)。 以下是一个简化的实现代码:</think>在H5移动端实现图片的双指缩放功能,主要依靠监听触摸事件CSS3变换。以下是核心实现步骤代码示例: ### 一、实现原理 1. **事件监听**:通过`touchstart`、`touchmove`、`touchend`监听手触摸 2. **距离计算**:用勾股定理计算间距 $$ \text{distance} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} $$ 3. **缩放中心**:以中点作为缩放基准点 4. **变换控制**:通过CSS3的`transform: matrix()`实现平滑缩放 ### 二、完整代码实现 ```html <div class="container"> <img id="zoomImg" src="your-image.jpg"> </div> <script> const img = document.getElementById('zoomImg'); let startDistance = 0; let currentScale = 1; let startScale = 1; let originX = 0, originY = 0; // 计算两点距离 function getDistance(p1, p2) { return Math.sqrt(Math.pow(p2.pageX - p1.pageX, 2) + Math.pow(p2.pageY - p1.pageY, 2)); } // 计算中心点 function getMidpoint(p1, p2) { return { x: (p1.pageX + p2.pageX) / 2, y: (p1.pageY + p2.pageY) / 2 }; } img.addEventListener('touchstart', e => { if (e.touches.length === 2) { e.preventDefault(); // 记录初始距离缩放中心 startDistance = getDistance(e.touches[0], e.touches[1]); const midpoint = getMidpoint(e.touches[0], e.touches[1]); originX = midpoint.x - img.offsetLeft; originY = midpoint.y - img.offsetTop; startScale = currentScale; } }); img.addEventListener('touchmove', e => { if (e.touches.length === 2) { e.preventDefault(); // 计算当前缩放比例 const currentDistance = getDistance(e.touches[0], e.touches[1]); currentScale = startScale * (currentDistance / startDistance); // 应用变换矩阵 img.style.transform = `matrix(${currentScale}, 0, 0, ${currentScale}, 0, 0)`; img.style.transformOrigin = `${originX}px ${originY}px`; } }); img.addEventListener('touchend', () => { // 限制缩放范围 (0.5x - 5x) currentScale = Math.min(5, Math.max(0.5, currentScale)); }); </script> <style> .container { overflow: hidden; touch-action: none; /* 禁用默认触摸行为 */ } #zoomImg { transition: transform 0.1s; transform-origin: center; } </style> ``` ### 三、关键优化点 1. **缩放中心修正**: - 通过`transformOrigin`动态设置缩放中心点 - 计算公式:`originX = midpoint.x - img.offsetLeft` 2. **性能优化**: - 使用CSS3硬件加速:`transform`代替`width/height`修改 - 添加`touch-action: none`防止页面滚动冲突[^2] 3. **边界处理**: - 设置最小/最大缩放比例限制 - 添加过渡动画提升体验 ### 四、注意事项 1. **坐标系问题**:需考虑页面滚动偏移量,建议使用`pageX/pageY`而非`clientX/clientY` 2. **手势冲突**:需要单独处理单指拖动事件(平移功能) 3. **移动端兼容**:iOSAndroid的touch事件存在细微差异,建议用`Hammer.js`等库简化实现[^2][^3] ### 五、推荐方案 对于复杂场景,建议使用成熟手势库: ```javascript // 使用Hammer.js实现 import Hammer from 'hammerjs'; const mc = new Hammer(img); mc.get('pinch').set({ enable: true }); mc.on('pinchstart', e => { startScale = currentScale; }); mc.on('pinchmove', e => { currentScale = startScale * e.scale; img.style.transform = `scale(${currentScale})`; }); ``` > 示例代码库:http://pangyongsheng.github.io/imgPreview/[^1] > Hammer.js文档:https://hammerjs.github.io/[^2] --- ### 相关问题 1. 如何实现移动端图片的击放大功能? 2. 图片缩放时如何保持清晰度(避免模糊)? 3. 如何处理多手势并发(缩放+旋转)? 4. 移动端图片查看器如何实现惯性滑动效果? 5. 如何限制图片在容器内拖动的边界?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值