React Native 自定义动态Toast组件改造咨询:实现全局调用与多点击依次显示
解决方案:全局可调用+排队显示的Toast组件
嘿,我来帮你搞定这两个Toast的需求!咱们一步步把这个组件改造成既支持全局调用,又能处理多点击排队显示的版本。
核心思路
- 全局调用:用React Context实现全局状态管理,把Toast的触发逻辑从组件中抽离,让任意页面都能通过自定义Hook轻松调用。
- 排队显示:维护一个消息队列,每次点击把消息加入队列,当前Toast显示完成后自动取出下一个消息展示。
步骤1:创建全局Toast上下文
新建ToastContext.js,提供全局的Toast调用方法:
import React, { createContext, useContext, useState, useCallback } from 'react'; const ToastContext = createContext(); export const ToastProvider = ({ children }) => { const [toastQueue, setToastQueue] = useState([]); // 全局调用方法:接收类型、消息、可选自定义标题 const showToast = useCallback((type, message, header) => { setToastQueue(prev => [...prev, { type, message, header }]); }, []); return ( <ToastContext.Provider value={{ showToast }}> {children} </ToastContext.Provider> ); }; // 自定义Hook,简化页面调用 export const useToast = () => { const context = useContext(ToastContext); if (!context) { throw new Error('useToast must be used within a ToastProvider'); } return context; };
步骤2:改造Toast组件为队列驱动型
修改Toast.js,让它只负责根据队列显示Toast,并处理关闭后的队列更新:
import React, { useRef, useState, useEffect } from "react"; import { StyleSheet, Text, View, Animated, TouchableOpacity, Dimensions } from "react-native"; import { AntDesign, Entypo } from "@expo/vector-icons"; const Toast = ({ toastQueue, onToastComplete }) => { const windowHeight = Dimensions.get("window").height; const [currentToast, setCurrentToast] = useState(null); const popAnim = useRef(new Animated.Value(windowHeight * -1)).current; const timerRef = useRef(null); // 根据Toast类型获取对应配置 const getToastConfig = (type) => { switch(type) { case 'success': return { color: "#6dcf81", header: "Success!", icon: "checkcircleo" }; case 'fail': return { color: "#bf6060", header: "Failed!", icon: "closecircleo" }; default: return { color: "#333", header: "Info!", icon: "infocirlceo" }; } }; // Toast弹出动画 const popIn = () => { Animated.timing(popAnim, { toValue: windowHeight * 0.35 * -1, duration: 300, useNativeDriver: true, }).start(() => { // 设置10秒后自动消失 timerRef.current = setTimeout(popOut, 10000); }); }; // Toast收起动画 const popOut = () => { Animated.timing(popAnim, { toValue: windowHeight * -1, duration: 300, useNativeDriver: true, }).start(() => { onToastComplete(); clearTimeout(timerRef.current); }); }; // 手动关闭Toast const instantPopOut = () => { Animated.timing(popAnim, { toValue: windowHeight * -1, duration: 150, useNativeDriver: true, }).start(() => { onToastComplete(); clearTimeout(timerRef.current); }); }; // 监听队列变化,自动显示下一个Toast useEffect(() => { if (toastQueue.length > 0 && !currentToast) { setCurrentToast(toastQueue[0]); popIn(); } }, [toastQueue, currentToast]); // 无消息时不渲染 if (!currentToast) return null; const config = getToastConfig(currentToast.type); return ( <View style={styles.toastWrapper}> <Animated.View style={[styles.toastContainer, { transform: [{ translateY: popAnim }] }]} > <View style={styles.toastRow}> <AntDesign name={config.icon} size={24} color={config.color} /> <View style={styles.toastText}> <Text style={{ fontWeight: "bold", fontSize: 15 }}> {currentToast.header || config.header} </Text> <Text style={{ fontSize: 12 }}>{currentToast.message}</Text> </View> <TouchableOpacity onPress={instantPopOut}> <Entypo name="cross" size={24} color="black" /> </TouchableOpacity> </View> </Animated.View> </View> ); }; export default Toast; const styles = StyleSheet.create({ toastWrapper: { position: 'absolute', top: 0, left: 0, right: 0, alignItems: 'center', zIndex: 9999, // 确保Toast在所有组件上层 }, toastContainer: { height: 60, width: 350, backgroundColor: "#f5f5f5", justifyContent: "center", alignItems: "center", borderRadius: 10, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 5, marginTop: 40, // 避开状态栏 }, toastRow: { width: "90%", flexDirection: "row", alignItems: "center", justifyContent: "space-evenly", }, toastText: { width: "70%", padding: 2, }, });
步骤3:改造根组件(原Home.js)
在根组件中包裹ToastProvider,并维护Toast队列:
import React, { useState } from 'react'; import { StyleSheet, SafeAreaView, Button, View } from 'react-native'; import Toast from './Toast'; import { ToastProvider, useToast } from './ToastContext'; // 示例:任意页面都可以用useToast调用Toast const AnyPage = () => { const { showToast } = useToast(); return ( <View style={styles.buttonContainer}> <Button title="Show Success Toast" onPress={() => showToast('success', 'You pressed the success button')} /> <Button title="Show Fail Toast" onPress={() => showToast('fail', 'You pressed the fail button')} style={{ marginTop: 20 }} /> <Button title="Show Custom Toast" onPress={() => showToast('info', 'This is a custom info message', 'Custom Info')} style={{ marginTop: 20 }} /> </View> ); }; export default function App() { const [toastQueue, setToastQueue] = useState([]); // Toast关闭后,移除队列第一个元素 const handleToastComplete = () => { setToastQueue(prev => prev.slice(1)); }; return ( <ToastProvider> <SafeAreaView style={styles.container}> <AnyPage /> {/* Toast只需要在根组件渲染一次 */} <Toast toastQueue={toastQueue} onToastComplete={handleToastComplete} /> </SafeAreaView> </ToastProvider> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, buttonContainer: { gap: 16, }, });
使用说明
- 全局调用:在任意页面导入
useToastHook,调用showToast(type, message, [header])即可显示Toast,比如:const { showToast } = useToast(); showToast('success', '操作成功!'); - 排队显示:多次点击按钮时,消息会自动加入队列,当前Toast消失后会依次显示下一个,完全不需要额外处理。
额外优化点
- 可以扩展更多Toast类型(比如warning、info),只需要在
getToastConfig里添加配置即可。 - 可以自定义Toast的显示时长,把时长作为参数传入
showToast。 - 可以添加Toast的位置选项(比如顶部、底部)。
内容的提问来源于stack exchange,提问作者Jason0011
相关产品推荐
相关产品推荐

