You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native 自定义动态Toast组件改造咨询:实现全局调用与多点击依次显示

解决方案:全局可调用+排队显示的Toast组件

嘿,我来帮你搞定这两个Toast的需求!咱们一步步把这个组件改造成既支持全局调用,又能处理多点击排队显示的版本。

核心思路

  1. 全局调用:用React Context实现全局状态管理,把Toast的触发逻辑从组件中抽离,让任意页面都能通过自定义Hook轻松调用。
  2. 排队显示:维护一个消息队列,每次点击把消息加入队列,当前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,
  },
});

使用说明

  1. 全局调用:在任意页面导入useToast Hook,调用showToast(type, message, [header])即可显示Toast,比如:
    const { showToast } = useToast();
    showToast('success', '操作成功!');
    
  2. 排队显示:多次点击按钮时,消息会自动加入队列,当前Toast消失后会依次显示下一个,完全不需要额外处理。

额外优化点

  • 可以扩展更多Toast类型(比如warning、info),只需要在getToastConfig里添加配置即可。
  • 可以自定义Toast的显示时长,把时长作为参数传入showToast。
  • 可以添加Toast的位置选项(比如顶部、底部)。

内容的提问来源于stack exchange,提问作者Jason0011

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 19:19:07