触摸反馈与手势:GestureDetector、ScaleGestureDetector、滑动、双击、长按

触摸交互,是移动应用最核心的体验。说白了,用户跟你的App打交道,大部分时间就是手指在屏幕上划来划去。如果这个环节做得生硬、卡顿,用户会立刻觉得“这App不行”。

我个人习惯,在接手一个新项目时,会先看看它的手势处理是否优雅。很多新手容易把触摸事件写得一团乱麻,各种if-else嵌套,最后连自己都看不懂。今天我们就来聊聊Android里处理手势的几个利器:GestureDetectorScaleGestureDetector,以及它们背后的滑动、双击、长按逻辑。

1. 触摸事件的本质:MotionEvent

在聊手势之前,得先明白触摸事件的底层——MotionEvent。它记录了手指在屏幕上的每一个动作:按下、移动、抬起、多点触控等等。

每个MotionEvent都有一个action,常见的包括:

  • ACTION_DOWN:手指按下
  • ACTION_MOVE:手指移动
  • ACTION_UP:手指抬起
  • ACTION_POINTER_DOWN:第二个手指按下(多点触控)
  • ACTION_POINTER_UP:第二个手指抬起

嗯,这里要注意:ACTION_DOWN是每次触摸序列的起点,ACTION_UP是终点。中间可能夹杂着很多个ACTION_MOVE。如果你在onTouchEvent里直接处理这些原始事件,代码会变得非常啰嗦。

核心观点:GestureDetector 就是帮你把这些原始事件“翻译”成我们熟悉的手势语义——单击、双击、长按、滑动。

2. GestureDetector:你的手势翻译官

GestureDetector是我用得最多的类。它接收MotionEvent,然后回调给你各种手势事件。你只需要实现一个监听器即可。

常用的监听器有两种:

  • OnGestureListener:处理单击、长按、滑动、抛掷(Fling)
  • OnDoubleTapListener:处理双击、单击确认

来看一个典型的用法:

public class MyView extends View {

    private GestureDetector gestureDetector;

    public MyView(Context context) {
        super(context);
        gestureDetector = new GestureDetector(context, new MyGestureListener());
        // 别忘了设置双击监听器
        gestureDetector.setOnDoubleTapListener(new MyDoubleTapListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 把事件交给GestureDetector处理
        return gestureDetector.onTouchEvent(event);
    }

    private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onDown(MotionEvent e) {
            // 必须返回true,表示我们消费了这个事件
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            // 长按触发
            // 我在项目中遇到过,长按后如果手指不抬起,会继续触发onScroll,需要加标志位控制
            Log.d("Gesture", "长按了");
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // 滑动事件,e1是按下时的位置,e2是当前位置
            // distanceX是X轴上的滑动距离(负值表示向左滑)
            Log.d("Gesture", "滑动中: " + distanceX + ", " + distanceY);
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            // 快速抛掷,常用于翻页
            Log.d("Gesture", "抛掷: velocityX=" + velocityX);
            return true;
        }
    }

    private class MyDoubleTapListener extends GestureDetector.SimpleOnGestureListener {

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            // 双击
            Log.d("Gesture", "双击了");
            return true;
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            // 确认是单击(不是双击的一部分)
            Log.d("Gesture", "单击确认");
            return true;
        }
    }
}

小技巧onDown()必须返回true,否则后续的滑动、长按等事件都不会触发。这是很多新手容易踩的坑。

你可能会问:为什么要有onSingleTapConfirmed?直接onSingleTapUp不行吗?

原因在于:系统需要等待一段时间,确认用户没有进行第二次点击(即双击),才能确定这是一次单击。所以onSingleTapConfirmed会有延迟。而onSingleTapUp是手指抬起时立刻触发,但它无法区分单击和双击的前半段。

3. 滑动(Scroll)与抛掷(Fling)的区别

这两个概念容易混淆。我简单解释一下:

  • 滑动(Scroll):手指在屏幕上拖动,有持续接触。比如你慢慢拖动一张图片。
  • 抛掷(Fling):手指快速滑动后抬起,屏幕内容继续惯性滚动。比如快速翻页。

GestureDetector中,onScrollonFling是分开回调的。实际开发中,我通常会在onScroll里更新UI位置,在onFling里启动一个动画或惯性滚动效果。

避坑指南:我曾经在做一个图片查看器时,发现onFlingonScroll会同时触发。原因是手指在抬起前的一瞬间,系统既检测到了滑动,又检测到了抛掷。解决方案是:在onFling里设置一个标志位,阻止onScroll的后续处理。

4. ScaleGestureDetector:双指缩放

双指缩放是图片查看、地图浏览的标配。ScaleGestureDetector专门处理这个。它的用法和GestureDetector类似,也是接收MotionEvent,然后回调缩放事件。

public class ZoomableView extends View {

    private ScaleGestureDetector scaleGestureDetector;
    private float scaleFactor = 1.0f;

    public ZoomableView(Context context) {
        super(context);
        scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 先交给ScaleGestureDetector处理
        scaleGestureDetector.onTouchEvent(event);
        return true;
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            // detector.getScaleFactor() 返回缩放比例,大于1表示放大,小于1表示缩小
            scaleFactor *= detector.getScaleFactor();
            // 限制缩放范围,防止缩得太小或太大
            scaleFactor = Math.max(0.5f, Math.min(scaleFactor, 5.0f));
            invalidate(); // 重绘
            return true;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            // 缩放开始,可以在这里做一些初始化
            return true; // 返回true表示接受缩放
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            // 缩放结束
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.scale(scaleFactor, scaleFactor, getWidth() / 2f, getHeight() / 2f);
        // 绘制你的内容...
    }
}

这里有个细节:onScaleBegin必须返回true,否则缩放不会生效。另外,ScaleGestureDetector内部已经处理了多点触控的复杂逻辑,你不需要自己去追踪第二个手指。

个人经验:我在做一个涂鸦App时,需要同时支持单指绘画和双指缩放。这时候onTouchEvent里需要同时调用GestureDetectorScaleGestureDetector。我的做法是:先判断手指数量,如果只有一根手指,交给GestureDetector;如果有两根手指,交给ScaleGestureDetector。这样就不会冲突了。

5. 知识体系总览

下面这张图,帮你理清触摸事件与手势检测的整体脉络:

触摸反馈与手势检测知识体系 MotionEvent GestureDetector ScaleGestureDetector onDown / onShowPress onSingleTapUp / onSingleTapConfirmed onDoubleTap / onDoubleTapEvent onLongPress onScaleBegin onScale (getScaleFactor) onScaleEnd

6. 实战中的组合使用

在实际项目中,你往往需要同时处理多种手势。比如一个图片查看器,需要支持:

  • 单指滑动:移动图片
  • 双指缩放:放大缩小
  • 双击:放大/缩小切换
  • 长按:弹出菜单

这时候,你需要在onTouchEvent里协调多个手势检测器。我的做法是:

@Override
public boolean onTouchEvent(MotionEvent event) {
    // 先交给ScaleGestureDetector处理(它内部会判断手指数量)
    scaleGestureDetector.onTouchEvent(event);
    
    // 再交给GestureDetector处理
    gestureDetector.onTouchEvent(event);
    
    return true;
}

但这样有个问题:当双指缩放时,GestureDetector也会收到事件,可能会触发onScroll。所以最好加一个判断:

@Override
public boolean onTouchEvent(MotionEvent event) {
    // 如果正在缩放,就不处理滑动
    if (isScaling) {
        scaleGestureDetector.onTouchEvent(event);
        return true;
    }
    
    gestureDetector.onTouchEvent(event);
    scaleGestureDetector.onTouchEvent(event);
    return true;
}

其中isScaling可以在onScaleBegin设为true,在onScaleEnd设为false。

小建议:如果你觉得GestureDetectorScaleGestureDetector配合起来有点麻烦,可以考虑使用View.onTouchEvent配合VelocityTracker自己实现手势识别。但说实话,除非你有非常特殊的需求,否则用现成的检测器更省心。

7. 总结一下

触摸反馈和手势处理,是Android开发中“看起来简单,做起来容易出bug”的环节。记住几个关键点:

  • GestureDetector帮你把原始触摸事件翻译成语义化的手势
  • ScaleGestureDetector专门处理双指缩放
  • onDown()必须返回true,否则后续事件收不到
  • 多点触控时,注意协调不同手势检测器的优先级

嗯,差不多就这些。下次你在写触摸事件时,不妨先想想:用户会怎么操作?然后选择合适的手势检测器,而不是自己手写一堆switch-case。相信我,这样代码会清爽很多。


公众号:蓝海资料掘金营,微信deep3321