12、抛硬币应用:结合随机数与加速度阈值

抛硬币这个功能,听起来挺简单的对吧?

手机模拟抛硬币,无非就是「摇一摇,出结果」。但真要做起来,你会发现一个核心问题:怎么判断用户是真的在「抛」?

你想想看,用户可能只是拿着手机走路,或者从口袋里掏出来。这些动作都会产生加速度数据。如果随便一晃就触发抛硬币,那这应用就没法用了。

所以,这一章我们要解决两个关键点:

  • 如何用加速度传感器检测「抛」这个动作
  • 如何结合随机数生成「正面」或「反面」

12.1 抛硬币的动作特征分析

我刚开始做这个功能时,也踩过坑。我以为只要检测到加速度超过某个值,就算一次抛掷。结果呢?用户轻轻一碰手机,硬币就翻了。用户体验极差。

后来我仔细分析了一下真实的抛硬币动作:

  1. 有一个明显的「甩起」过程——加速度会突然增大
  2. 然后有一个「静止」过程——手机回到手中,加速度归零
  3. 整个过程持续时间很短——通常在0.3秒到0.8秒之间

说白了,我们要检测的是一个「脉冲信号」,而不是一个持续的高加速度。

核心思路:

检测加速度的「峰值」是否超过阈值,并且在峰值之后加速度迅速回落。这才是真正的「抛」。

12.2 阈值怎么定?

阈值设得太低,随便一晃就触发。设得太高,用户使劲甩都不动。我个人的经验是:

  • 走路时的加速度变化:大约在 5-10 m/s² 之间
  • 跑步时的加速度变化:大约在 10-20 m/s² 之间
  • 刻意抛掷时的加速度:通常在 25 m/s² 以上

所以,我建议把阈值设在 25 m/s² 左右。当然,这个值你可以做成可配置的,让用户自己调整灵敏度。

小技巧:

我在项目中遇到过一个问题:不同手机的传感器灵敏度不一样。有的手机轻轻一晃就25了,有的手机使劲甩才到20。所以,我后来加了一个「校准模式」——让用户先抛几次,系统自动学习阈值。

12.3 代码实现:检测抛掷动作

嗯,这里要注意。我们不能在每次传感器事件触发时都去判断,那样太频繁了。我们需要一个「状态机」来控制流程。

public class CoinFlipDetector {
    private static final float THRESHOLD = 25.0f; // 加速度阈值 m/s²
    private static final long COOLDOWN_MS = 500;  // 冷却时间 500ms
    
    private boolean isReady = true;
    private long lastFlipTime = 0;
    private OnCoinFlipListener listener;
    
    public interface OnCoinFlipListener {
        void onCoinFlip(float peakAcceleration);
    }
    
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) return;
        
        long now = System.currentTimeMillis();
        if (!isReady) return;
        if (now - lastFlipTime < COOLDOWN_MS) return;
        
        float x = event.values[0];
        float y = event.values[1];
        float z = event.values[2];
        
        // 计算合加速度
        float magnitude = (float) Math.sqrt(x*x + y*y + z*z);
        // 减去重力加速度
        float netAcceleration = magnitude - SensorManager.GRAVITY_EARTH;
        
        if (netAcceleration > THRESHOLD) {
            isReady = false;
            lastFlipTime = now;
            
            if (listener != null) {
                listener.onCoinFlip(netAcceleration);
            }
            
            // 延迟重置状态
            new Handler(Looper.getMainLooper()).postDelayed(() -> {
                isReady = true;
            }, COOLDOWN_MS);
        }
    }
}

这段代码里,我做了几件事:

  • 计算合加速度,减去重力,得到「净加速度」
  • 净加速度超过阈值,才认为是抛掷动作
  • 加了一个冷却时间,防止连续触发

注意:

千万不要在 onSensorChanged 里做耗时操作!这个回调频率很高,可能每秒几十次。我曾经见过有人在这里写数据库操作,结果手机直接卡死。

12.4 结合随机数:生成抛掷结果

检测到抛掷动作之后,就该生成结果了。这里有个哲学问题:抛硬币真的是随机的吗?

从物理角度讲,抛硬币的结果取决于初始速度、旋转角度、空气阻力等等。但从应用角度讲,我们直接用随机数模拟就行了。用户要的是「不可预测」的感觉,而不是真正的物理模拟。

public class CoinFlipResult {
    private static final Random random = new Random();
    
    public enum Side {
        HEADS,  // 正面
        TAILS   // 反面
    }
    
    public static Side flip() {
        // 50% 概率
        return random.nextBoolean() ? Side.HEADS : Side.TAILS;
    }
    
    // 带权重的抛掷(比如用户想要「正面」概率高一点)
    public static Side flipWithWeight(float headsProbability) {
        // headsProbability 范围 0.0 ~ 1.0
        return random.nextFloat() < headsProbability ? Side.HEADS : Side.TAILS;
    }
}

你看,核心代码就这么几行。但有个细节我想说一下:Random 实例要复用。每次 new Random() 的话,如果两次调用间隔太短,可能会生成相同的随机序列。我习惯把它声明为静态变量。

12.5 完整的抛硬币流程

把上面两部分结合起来,完整的流程是这样的:

  1. 用户摇晃手机
  2. 传感器检测到加速度超过阈值
  3. 触发抛掷事件
  4. 生成随机结果(正面/反面)
  5. 播放动画(硬币翻转效果)
  6. 显示结果

这里我画了一张流程图,帮你理清整个逻辑:

抛硬币应用核心流程图 等待用户摇晃 加速度传感器持续监听 净加速度 > 25 m/s²? 随机生成正面/反面 否(继续监听)

12.6 用户体验优化

功能做完了,但要让用户觉得「爽」,还得加点料。

1. 震动反馈

检测到抛掷动作时,让手机震动一下。用户会感觉「嗯,手机响应了」。

// 在检测到抛掷时触发
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null && vibrator.hasVibrator()) {
    vibrator.vibrate(50); // 震动50ms
}

2. 音效

抛硬币怎么能没有「叮」的一声?我习惯用 SoundPool 来播放短音效,延迟低,适合这种即时反馈的场景。

3. 动画

硬币翻转的动画,可以用属性动画来做。一个简单的实现:用两个 ImageView 交替显示正面和反面,然后做旋转动画。嗯,这个实现细节比较多,我们后面专门讲动画的时候再展开。

我的经验:

动画时长控制在 600ms-800ms 之间最舒服。太短了看不清,太长了用户着急。我试过 1.5 秒的动画,用户反馈「等得花儿都谢了」。

12.7 避坑指南

最后,分享几个我踩过的坑:

  • 传感器延迟问题:有些低端手机的传感器采样率很低,可能 50ms 才采样一次。这种情况下,峰值可能被漏掉。我建议用 SensorManager.SENSOR_DELAY_GAME 来注册监听器,采样率会高一些。
  • 横竖屏切换:抛硬币过程中如果用户旋转了手机,加速度数据会突变。我一般会在检测到抛掷后锁定屏幕方向,等动画结束再解锁。
  • 后台限制:Android 8.0 以后,后台应用获取传感器数据的频率会被限制。如果你的应用退到后台,传感器可能不工作了。这个要注意。

好了,抛硬币应用的核心逻辑就这些。说白了就是「阈值检测 + 随机数」,但细节决定了用户体验的好坏。下一章我们聊聊怎么让硬币翻转动画更流畅——嗯,不过那是后话了。


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