前言
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的调用非常简单,怎么依据传感器提供的数值得到我们想要的效果,需要数学功底,感兴趣的小伙伴可以去试一试