基于Expo与React Native开发闹钟应用的技术求助
解决方案:基于Expo实现持续响铃的闹钟应用
问题本质
你当前用Expo Calendar创建事件的方式,只是借助系统日历的提醒功能,仅能触发单次通知,无法实现持续响铃至用户处理的核心需求,也不支持通过手机移动停止响铃。要达成目标,需要结合Expo的通知、音频和传感器能力重构方案。
核心依赖
expo-notifications:处理定时触发,支持后台唤醒expo-audio:播放循环铃声,支持后台运行expo-sensors:监听设备加速度变化,实现移动停止功能expo-task-manager:注册后台任务,确保闹钟在应用后台/关闭时仍能触发
实现步骤及代码
1. 安装依赖
在项目根目录执行:
expo install expo-notifications expo-audio expo-sensors expo-task-manager
2. 权限配置
在app.json中添加必要权限:
{ "expo": { "ios": { "infoPlist": { "NSNotificationsUsageDescription": "用于发送闹钟通知", "NSMotionUsageDescription": "用于检测手机移动停止闹钟", "NSAudioUsageDescription": "用于播放闹钟铃声" }, "backgroundModes": ["audio"] }, "android": { "permissions": [ "POST_NOTIFICATIONS", "VIBRATE", "ACTIVITY_RECOGNITION" ] } } }
3. 完整组件代码
import React, { useEffect, useState, useRef } from "react"; import { TextInput, Text, TouchableOpacity, View, Button, Alert } from "react-native"; import DateTimePicker from "@react-native-community/datetimepicker"; import Icon from "react-native-vector-icons/Ionicons"; import * as Notifications from "expo-notifications"; import * as Audio from "expo-audio"; import * as Accelerometer from "expo-sensors"; import * as TaskManager from "expo-task-manager"; // 后台任务标识 const ALARM_TASK = "ALARM_TRIGGER_TASK"; // 注册后台任务:触发通知+播放铃声 TaskManager.defineTask(ALARM_TASK, async ({ data }) => { if (!data) return; // 触发即时通知 await Notifications.scheduleNotificationAsync({ content: { title: data.title, body: "点击停止闹钟", sound: "default", vibrate: [0, 250, 250, 250], }, trigger: null, }); // 启动循环铃声 await playAlarmSound(); }); // 播放循环铃声 const playAlarmSound = async () => { await Audio.setAudioModeAsync({ allowsRecordingIOS: false, playsInSilentModeIOS: true, staysActiveInBackground: true, shouldDuckAndroid: false, }); const sound = new Audio.Sound(); try { // 替换为自定义铃声路径,或使用系统默认 await sound.loadAsync(require("./assets/alarm.mp3")); await sound.setIsLoopingAsync(true); await sound.playAsync(); global.alarmSound = sound; } catch (err) { console.error("铃声播放失败:", err); } }; // 停止铃声 const stopAlarmSound = async () => { if (global.alarmSound) { await global.alarmSound.stopAsync(); await global.alarmSound.unloadAsync(); global.alarmSound = null; } }; const AddAlarm = () => { const [date, setDate] = useState(new Date()); const [mode, setMode] = useState("date"); const [showPicker, setShowPicker] = useState(false); const [alarmTitle, setAlarmTitle] = useState(""); const accelerometerRef = useRef(null); // 初始化权限+监听通知点击 useEffect(() => { const requestPermissions = async () => { const [notifPerm, audioPerm, sensorPerm] = await Promise.all([ Notifications.requestPermissionsAsync(), Audio.requestPermissionsAsync(), Accelerometer.requestPermissionsAsync(), ]); if (notifPerm.status !== "granted" || audioPerm.status !== "granted" || sensorPerm.status !== "granted") { Alert.alert("权限不足", "需要通知、音频和传感器权限才能使用闹钟"); } }; // 通知点击停止铃声 const notificationListener = Notifications.addNotificationResponseReceivedListener(() => { stopAlarmSound(); accelerometerRef.current?.remove(); }); requestPermissions(); return () => notificationListener.remove(); }, []); // 监听设备移动停止闹钟 useEffect(() => { const startMovementDetection = () => { accelerometerRef.current = Accelerometer.addListener(({ x, y, z }) => { // 检测明显移动(阈值可调整) const movementIntensity = Math.abs(x) + Math.abs(y) + Math.abs(z); if (movementIntensity > 15) { stopAlarmSound(); accelerometerRef.current.remove(); Alert.alert("闹钟已停止", "检测到手机移动"); } }); Accelerometer.setUpdateInterval(1000); }; // 铃声触发后启动移动检测 const alarmTriggerListener = Notifications.addNotificationReceivedListener(startMovementDetection); return () => alarmTriggerListener.remove(); }, []); const handleDateChange = (_, selectedDate) => { const currentDate = selectedDate || date; setShowPicker(false); setDate(currentDate); }; const showDatePicker = () => setShowPicker(true) || setMode("date"); const showTimePicker = () => setShowPicker(true) || setMode("time"); const setAlarm = async () => { if (!alarmTitle.trim()) { Alert.alert("提示", "请输入闹钟标题"); return; } const alarmTimestamp = date.getTime(); if (alarmTimestamp <= Date.now()) { Alert.alert("提示", "请选择未来的时间"); return; } // 注册后台任务 await Notifications.registerTaskAsync(ALARM_TASK); // 调度闹钟 await Notifications.scheduleNotificationAsync({ content: { title: alarmTitle, data: { title: alarmTitle }, }, trigger: { date: new Date(alarmTimestamp), repeats: false, }, }); Alert.alert("成功", `闹钟已设置为: ${date.toLocaleString()}`); setAlarmTitle(""); setDate(new Date()); }; return ( <View style={{ flex: 1, padding: 10 }}> <TouchableOpacity style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", height: 40, marginTop: 10 }} onPress={showDatePicker} > <Text>{date.toLocaleDateString()}</Text> <Icon name="calendar" size={30} color="gray" /> </TouchableOpacity> <View style={{ height: 1, backgroundColor: "#ccc", marginVertical: 10 }} /> <TouchableOpacity style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", height: 40 }} onPress={showTimePicker} > <Text>{date.toLocaleTimeString()}</Text> <Icon name="time-outline" size={30} color="gray" /> </TouchableOpacity> <View style={{ height: 1, backgroundColor: "#ccc", marginVertical: 10 }} /> <TextInput placeholder="闹钟标题" onChangeText={setAlarmTitle} value={alarmTitle} style={{ padding: 10, borderWidth: 1, borderColor: "#ccc", borderRadius: 5, marginBottom: 20 }} /> {showPicker && ( <DateTimePicker value={date} mode={mode} is24Hour={true} onChange={handleDateChange} /> )} <View style={{ position: "absolute", bottom: 50, left: 10, right: 10 }}> <Button onPress={setAlarm} title="设置闹钟" /> </View> </View> ); }; export default AddAlarm;
功能说明
- 后台持续响铃:通过
expo-task-manager注册后台任务,确保闹钟在应用后台/关闭时仍能触发;expo-audio配置后台音频模式,保证铃声持续播放。 - 停止方式:
- 点击通知直接停止铃声
- 检测设备加速度变化(移动手机),超过阈值自动停止铃声
- 自定义铃声:将
require("./assets/alarm.mp3")替换为你的自定义音频文件即可。
注意事项
- iOS需确保开启后台音频能力(已在
app.json配置) - 安卓需避免应用被系统强制杀死,否则后台任务可能失效
- 加速度阈值可根据需求调整,当前15为明显移动的判定标准
内容的提问来源于stack exchange,提问作者Beyza Odabaşı
相关产品推荐
相关产品推荐

