11、摇一摇功能:实现经典的摇一摇交互

摇一摇,这个功能大家再熟悉不过了。微信摇一摇、网易云音乐摇一摇换歌,甚至有些游戏里的彩蛋也是靠摇出来的。说实话,这个交互看起来简单,但真正实现起来,有不少细节需要打磨。

我个人习惯把摇一摇拆成三个核心问题:怎么检测摇动?怎么判断摇动了?摇动之后怎么响应? 今天我们就一个一个来解决。

11.1 摇一摇的核心原理

摇一摇的本质,就是监听加速度传感器的数据变化。你想想看,手机静止时,加速度计输出的值基本稳定在 (0, 0, -9.8) 左右(重力方向)。当你用力摇晃手机时,三个轴上的加速度值会剧烈波动。

所以,检测摇动的关键就是:计算加速度的变化量,看它是否超过某个阈值

核心公式:

摇动强度 = |当前加速度 - 上次加速度| 的平方和,再开根号

说白了,就是求两个采样点之间的「变化幅度」。

11.2 基础实现:监听加速度数据

我们先从最基础的代码开始。注册一个 SensorEventListener,监听 TYPE_ACCELEROMETER 的数据。

public class ShakeDetector implements SensorEventListener {

    private static final float SHAKE_THRESHOLD = 12.0f;  // 摇动阈值
    private static final int SHAKE_SKIP_TIME = 500;      // 防抖时间(毫秒)
    private static final int SHAKE_COUNT = 3;            // 连续触发次数

    private OnShakeListener listener;
    private long lastShakeTime = 0;
    private int shakeCounter = 0;

    private float lastX = 0, lastY = 0, lastZ = 0;
    private boolean isFirst = true;

    public interface OnShakeListener {
        void onShake();
    }

    public void setOnShakeListener(OnShakeListener listener) {
        this.listener = listener;
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) return;

        float x = event.values[0];
        float y = event.values[1];
        float z = event.values[2];

        if (isFirst) {
            lastX = x;
            lastY = y;
            lastZ = z;
            isFirst = false;
            return;
        }

        // 计算变化量
        float deltaX = Math.abs(x - lastX);
        float deltaY = Math.abs(y - lastY);
        float deltaZ = Math.abs(z - lastZ);

        // 计算摇动强度
        float shakeStrength = (float) Math.sqrt(
            deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ
        );

        // 更新上一次的值
        lastX = x;
        lastY = y;
        lastZ = z;

        // 判断是否达到阈值
        if (shakeStrength > SHAKE_THRESHOLD) {
            long now = System.currentTimeMillis();
            if (now - lastShakeTime > SHAKE_SKIP_TIME) {
                shakeCounter++;
                lastShakeTime = now;

                if (shakeCounter >= SHAKE_COUNT) {
                    shakeCounter = 0;
                    if (listener != null) {
                        listener.onShake();
                    }
                }
            }
        } else {
            shakeCounter = 0;  // 强度不够,重置计数
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // 一般不需要处理
    }
}

💡 我的经验:阈值 SHAKE_THRESHOLD 设成 12.0 是我在多个项目里试出来的「甜点值」。太小了容易误触,太大了用户摇到手酸都没反应。你可以根据实际场景微调,比如游戏里可以设小一点,社交应用可以设大一点。

11.3 防抖与连续触发

你可能会问:为什么还要加 SHAKE_SKIP_TIME 和 SHAKE_COUNT?

嗯,这里要注意。加速度传感器的采样频率很高(通常几十到几百赫兹),如果你每次检测到超过阈值就触发,那用户轻轻晃一下手机,可能就触发了十几次。这显然不是我们想要的。

所以,我设计了两个机制:

  • 时间防抖:两次触发之间至少间隔 500 毫秒。避免一次摇晃触发多次。
  • 计数防抖:连续检测到 3 次有效摇动,才真正触发回调。这样可以过滤掉偶然的抖动。

我曾经在一个社交 App 里遇到过一个问题:用户把手机放在桌上,有人走路震动了一下桌子,结果摇一摇被触发了。后来加了计数防抖,这个问题就解决了。

11.4 在 Activity 中使用

写好了检测器,接下来就是在 Activity 里注册和注销了。这个步骤很容易被忽略,但非常重要。

public class ShakeActivity extends AppCompatActivity {

    private SensorManager sensorManager;
    private ShakeDetector shakeDetector;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_shake);

        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
        shakeDetector = new ShakeDetector();

        shakeDetector.setOnShakeListener(new ShakeDetector.OnShakeListener() {
            @Override
            public void onShake() {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        // 在这里处理摇动后的逻辑
                        Toast.makeText(ShakeActivity.this, "摇一摇触发!", Toast.LENGTH_SHORT).show();
                        // 比如播放音效、震动反馈、切换页面等
                    }
                });
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        if (accelerometer != null) {
            sensorManager.registerListener(shakeDetector, accelerometer, SensorManager.SENSOR_DELAY_GAME);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        sensorManager.unregisterListener(shakeDetector);
    }
}

⚠️ 重要提醒:一定要在 onPause() 里注销监听器!否则你的 App 退到后台后,传感器还在工作,会持续耗电。我曾经见过一个案例,开发者忘了注销,用户晚上睡觉前摇了摇手机,结果手机一晚上没休眠,第二天电量掉了 40%。

11.5 摇一摇的视觉与触觉反馈

光有逻辑还不够,用户摇动手机后,需要得到即时的反馈。我个人习惯做三件事:

  1. 震动反馈:调用 Vibrator 服务,让手机震一下。
  2. 音效反馈:播放一个短促的「嗖」或者「叮」的声音。
  3. 动画反馈:界面上的元素做一个缩放或旋转动画。
// 震动反馈
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
if (vibrator != null && vibrator.hasVibrator()) {
    vibrator.vibrate(100);  // 震动 100 毫秒
}

// 音效反馈(需要提前加载 SoundPool)
soundPool.play(shakeSoundId, 1.0f, 1.0f, 0, 0, 1.0f);

// 动画反馈(比如让一个 ImageView 弹跳一下)
ImageView shakeIcon = findViewById(R.id.shake_icon);
shakeIcon.animate()
    .scaleX(1.3f)
    .scaleY(1.3f)
    .setDuration(150)
    .withEndAction(() -> shakeIcon.animate()
        .scaleX(1.0f)
        .scaleY(1.0f)
        .setDuration(150)
        .start())
    .start();

11.6 摇一摇的完整流程

为了让你更直观地理解整个流程,我画了一张图:

摇一摇功能完整流程图 注册传感器监听 获取加速度数据 超过阈值? 防抖检查 触发回调 + 反馈

11.7 常见问题与避坑指南

最后,分享几个我在实际项目中踩过的坑:

  • 阈值调优:不同手机的传感器灵敏度不一样。我建议在开发阶段把阈值设成可配置的,方便测试。
  • 后台耗电:前面说了,一定要在 onPause 里注销。另外,采样频率不要用 SENSOR_DELAY_FASTEST,用 SENSOR_DELAY_GAME 就足够了。
  • 多轴判断:有些开发者只检测 X 轴的变化,这是不对的。用户可能朝任意方向摇晃,必须三个轴都检测。
  • 模拟器测试:Android 模拟器不支持加速度传感器。你必须在真机上测试。我曾经在模拟器上调试了半天,发现传感器根本没数据,白白浪费了时间。

总结一下:摇一摇功能的核心就是「检测变化量 + 防抖 + 反馈」。代码量不大,但细节决定体验。把阈值调好、防抖做好、反馈做足,用户就会觉得「这个 App 很灵敏,很好用」。


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