react native应用中实现微信摇一摇和倾斜跳转功能

34 阅读5分钟

前言

App(android端iOS端)相较web端应用突出的优势之一就是有更多的调用硬件的能力,微信有过经典的"摇一摇"功能,这个功能基本都用过,还有现在非常流行的进入应用稍微动一下设备就疯狂跳转,这些功能都是依赖设备硬件传感器实现的,今天通过简单的代码揭秘

依赖

还是要强调一下环境依赖,这在RN应用中尤为重要,以下是核心依赖

  • "expo": "~54.0.31",
  • "expo-sensors": "~15.0.8",
  • "react": "19.1.0"
  • "react-native": "0.81.5",

功能简述

  • 摇一摇 通常是指我们快速晃动设备,设备传感器监测到某些值进而判定摇一摇成功,执行后续操作,这个比较好想,应该是加速度传感器,因为我们快速晃动设备意味着速度的变化,必然产生加速度(这个不了解的可以翻一翻高中的物理)
  • 设备倾斜跳转 通常是指我们打开某些垃圾应用时但凡对设备稍有动作,屏幕就显示一个盘状UI表示我们的设备发生倾斜,进而执行应用跳转,而应用跳转使用的是深度链接,每个现代app都配置了slug,应用厂商间相互协作互相知道彼此应用的slug并利用深度链接跨应用打开应用。既然是倾斜设备,大家应该首先想到陀螺仪,但是事实真是如此吗?其实还是加速度传感器,我们重点要知道设备的俯仰角

摇一摇

我们在现实世界中是三维空间,加速度是有方向和大小值(向量/矢量)这属于高中知识;设备传感器一旦开启会有检测频率,这个不用多解释,频率高意味着检测间隙时间短,响应更加灵敏;时间间隔,我们可以限定一定时间区间辅助我们判断是否是有效的摇一摇:从触发加速到加速度停止的时间;expo-sensors中的传感器是Accelerometer 基于以上三个条件我们做如下封装

import { Accelerometer, DeviceMotion, type DeviceMotionMeasurement, type DeviceMotionOrientation } from 'expo-sensors';

/**
 * 手机摇一摇
 * 获取设备在三维空间中的线性加速度
 * @param interval 设备运动检测频率
 * @param threshold 加速度阈值 晃动速度大小
 * @param callback 回调函数
 * @param duration 时间间隔 从设备开始运动到结束运动的时间间隔
 * @returns 可手动终止监听回调函数 或组件卸载时自动终止监听回调函数
 */
export const useShakeDeviceMotion = (
    interval?: number,
    threshold?: number,
    duration?: number,
    callback?: (info: DeviceMotionMeasurement['acceleration']) => void
) => {
    const subscription = useRef<EventSubscription | null>(null);
    interval ??= 1000;
    threshold ??= 1.5;
    duration ??= 1000;
    callback ??= acceleration => {
        const { x, y, z, timestamp } = acceleration ?? { x: 0, y: 0, z: 0, timestamp: 0 };
        /**
         * 加速度计算
         * 1.计算加速度的平方和
         * 2.开根号
         * 3.判断是否超过阈值
         * 4.判断时间间隔是否超过1秒
         * 5.满足以上条件,表示快速晃动手机
         */
        const accelerationMagnitude = Math.sqrt(x * x + y * y + z * z);
        if (accelerationMagnitude > threshold && Date.now() - timestamp > duration) {
            console.log('加速度超过阈值', accelerationMagnitude);
        }
    };
    // 订阅开启传感器数据
    const subscribe = () => {
        Accelerometer.setUpdateInterval(interval);
        subscription.current = Accelerometer.addListener(acceleration => callback(acceleration));
    };

    const remove = () => {
        subscription.current?.remove();
        subscription.current = null;
    };
    useEffect(() => {
        subscribe();
        return () => {
            remove();
        };
    }, []);
    return { remove, subscribe };
};

核心是计算方式和条件判断,当符合条件,可以通过 expo-audio等依赖库播放音效并在UI界面添加动画,表示触发摇一摇

倾斜跳转

移动设备的倾斜通常是是指沿着长边变化和短边翻转,通过加速度传感器计算得到俯仰角,旋转角,当值在我们期望的区间表示触发了倾斜,然后利用深度链接进行邪恶的操作:


/**
 * 通过加速度传感器获取当前设备倾斜角度
 * @param interval 设备运动检测频率,默认50
 * @param callback 回调函数,函数接收两个参数,分别是pitch(前后倾斜角度)和roll(左右倾斜角度)
 * @returns 返回一个对象,包含两个属性:isAvailable(设备是否支持加速度计)
 *  和angles(当前倾斜角度)和手动终止监听回调函数
 *  如果只需要某一种传感器,可以按需引入,比如Accelerometer加速度传感器,
 * 如果同时需要多种传感器,可以使用DeviceMotion.setUpdateInterval()方法设置传感器更新频率。
 * DeviceMotion.addListener()方法设置传感器更新频率。这将同时获得多种传感器数据
 * DeviceMotion是处理设备各个平台上的设备的运动
 */
export const useTiltSensor = (
    interval?: number,
    callback?: (isAvailable: boolean, pitch: number, roll: number) => void
) => {
    interval ??= 50;
    const subscription = useRef<EventSubscription | null>(null);
    const [angles, setAngles] = useState({
        pitch: 0, //平放手机,沿着长边左右翘起手机
        roll: 0, //平放手机,沿着短边上下翘起手机
    });
    const [isAvailable, setIsAvailable] = useState(false);
    /**取消监听 */
    const unsubscribe = () => {
        subscription.current?.remove();
        subscription.current = null;
    };
    useEffect(() => {
        /**
         * 检查设备是否支持加速度计
         */
        Accelerometer.isAvailableAsync().then(available => {
            setIsAvailable(available);
            if (available) {
                Accelerometer.setUpdateInterval(interval);
                if (subscription.current) {
                    unsubscribe();
                }
                // 订阅加速度计数据
                subscription.current = Accelerometer.addListener(({ x, y, z }) => {
                    // 计算倾斜角度(单位:度)
                    const pitch = Math.atan2(x, z) * (180 / Math.PI);
                    const roll = Math.atan2(-y, Math.sqrt(x * x + z * z)) * (180 / Math.PI);
                    if (callback) {
                        callback(true, pitch, roll);
                    }
                    setAngles({ pitch, roll });
                });
            } else {
                if (callback) {
                    callback(false, 0, 0);
                }
            }
        });
        // 清理订阅
        return () => unsubscribe();
    }, []);

    return { isAvailable, angles, unsubscribe };
};

核心是计算

const pitch = Math.atan2(x, z) * (180 / Math.PI);

const roll = Math.atan2(-y, Math.sqrt(x * x + z * z)) * (180 / Math.PI);

对应的ui我们可以使用动画 控制元素的rotateX和rotateY:

    const rollValue = useSharedValue(0);
    const pitchValue = useSharedValue(0);
    const { isAvailable, unsubscribe } = useTiltSensor(50, (available, pitch, roll) => {
        if (available) {
            const roundedPitch = Math.round(pitch * 10) / 10;
            const roundedRoll = Math.round(roll * 10) / 10;
            // 应用反向旋转和角度限制,具体限制多少看需求
            const maxRotation = 30;
            const constrainedPitch = Math.max(-maxRotation, Math.min(maxRotation, -roundedPitch));
            const constrainedRoll = Math.max(-maxRotation, Math.min(maxRotation, -roundedRoll));
            rollValue.value = withTiming(constrainedRoll, { duration: 50 });
            pitchValue.value = withTiming(constrainedPitch, { duration: 50 });

            // 可选:使用弹簧动画获得更自然的物理效果
            // rollValue.value = withSpring(constrainedRoll, { stiffness: 100, damping: 10 });
            // pitchValue.value = withSpring(constrainedPitch, { stiffness: 100, damping: 10 });
        }
    });
    const animatedStyle = useAnimatedStyle(() => {
        return {
            transform: [
                { rotateX: `${rollValue.value}deg` },
                { rotateY: `${pitchValue.value}deg` },
                { perspective: 1000 },
            ],
        };
    });

    return <Animated.View style={[style, animatedStyle]}>{children}</Animated.View>;

如上功能的核心是算法,api的调用非常简单,怎么依据传感器提供的数值得到我们想要的效果,需要数学功底,感兴趣的小伙伴可以去试一试