Toast 是移动端最常见的反馈组件之一。
点击收藏按钮,底部冒出一个"收藏成功";复制一段文字,屏幕上闪过"已复制到剪贴板";网络请求失败,顶部滑出一个"网络错误"。这些短暂出现又自动消失的提示,就是 Toast。
和 Alert 不同,Toast 不需要用户主动关闭,它会在几秒后自动消失。这种"轻量级"的特性让它特别适合那些"用户知道就行,不需要做任何响应"的场景。
我在项目里把提示类组件分成三类:
- [持久提示] 需要用户主动关闭,比如 Alert
- [临时提示] 自动消失,比如 Toast
- [阻断提示] 必须响应才能继续,比如 Modal
Toast 属于第二类。它的核心任务是"告诉用户发生了什么",然后悄悄消失,不打扰用户继续操作。
这篇文章会基于项目中真实存在的代码,把 Toast 的实现拆开讲清楚。全文代码片段都来自项目中的真实文件,路径是 src/components/ui/Toast.tsx。
Toast 在项目里的位置
这套 UI 组件库的文件组织比较统一。Toast 相关的文件主要在这几处:
src/components/ui/Toast.tsxsrc/screens/demos/ToastDemo.tsx
建议先看 Toast.tsx,理解组件的动画逻辑和自动隐藏机制;再看 ToastDemo.tsx,了解组件在仓库里被期望怎么用。
依赖引入
tsx
import React, { useEffect, useRef } from 'react';
import { View, Text, Animated, StyleSheet, ViewStyle } from 'react-native';
import { UITheme, ColorType } from './theme';
这段引入包含几个关键模块:
useEffect用于监听 visible 变化,控制动画的启动和定时器的设置useRef用于存储动画值,避免每次渲染都创建新的 Animated.ValueAnimated是 React Native 的动画模块,Toast 的滑入滑出效果就靠它UITheme和ColorType来自项目的主题配置,保证组件风格统一
Toast 的动画比较简单,只用到了 translateY(垂直位移)和 opacity(透明度),都支持原生驱动,性能很好。
ToastProps:接口设计
tsx
interface ToastProps {
visible: boolean;
message: string;
type?: ColorType;
duration?: number;
position?: 'top' | 'bottom';
onHide?: () => void;
icon?: string;
style?: ViewStyle;
}
接口设计覆盖了 Toast 的所有使用场景,我来逐个解释:
-
visible:控制 Toast 是否显示。这是一个受控属性,由外部状态管理。当 visible 从 false 变成 true 时,Toast 会滑入;duration 时间后会自动滑出并调用 onHide
-
message:必填的提示内容。Toast 的信息要简短,通常一句话就够了
-
type:语义类型,决定 Toast 的背景色。支持 primary、secondary、success、warning、danger、info 六种
-
duration:显示时长,单位毫秒,默认 3000(3秒)。可以根据信息的重要程度调整,重要的信息可以显示久一点
-
position:显示位置,top 或 bottom。默认在底部,某些场景(比如键盘弹出时)可能需要在顶部显示
-
onHide:隐藏时的回调函数。Toast 自动消失后会调用这个函数,外部需要在这里把 visible 设回 false
-
icon:自定义图标。如果不传,会根据 type 自动选择默认图标
-
style:允许外部传入额外样式,用于微调位置或覆盖默认样式
组件函数签名与默认值
tsx
export const Toast: React.FC<ToastProps> = ({
visible,
message,
type = 'info',
duration = 3000,
position = 'bottom',
onHide,
icon,
style,
}) => {
这里用解构赋值的方式设置了默认值:
type = 'info':默认是信息类型,蓝色背景,最中性的选择duration = 3000:默认显示 3 秒。这个时长经过考量------太短用户可能没看清,太长又会觉得烦position = 'bottom':默认在底部显示。底部是移动端 Toast 最常见的位置,不会遮挡页面主要内容
为什么默认在底部而不是顶部?因为用户的视线通常在屏幕中上部,底部的 Toast 不会遮挡正在看的内容,但又足够醒目能被注意到。
颜色值和动画值初始化
tsx
const colorValue = UITheme.colors[type];
const translateY = useRef(new Animated.Value(position === 'top' ? -100 : 100)).current;
const opacity = useRef(new Animated.Value(0)).current;
这三行代码完成了关键的初始化:
颜色值 :UITheme.colors[type] 把语义化的类型名转换成实际的颜色值。比如 type = 'success' 会得到绿色。
translateY 初始值:根据 position 决定初始位置。如果是 top,初始值是 -100(在屏幕上方外面);如果是 bottom,初始值是 100(在屏幕下方外面)。动画时会从这个位置滑到 0(正常位置)。
opacity 初始值:初始透明度是 0(完全透明)。动画时会从 0 变到 1(完全不透明)。
为什么用 useRef 而不是 useState?因为 Animated.Value 的变化不需要触发 React 重渲染,它直接驱动原生动画。用 useRef 可以保证动画值在组件生命周期内保持稳定。
默认图标映射
tsx
const defaultIcons: Record<ColorType, string> = {
primary: 'ℹ️',
secondary: '📌',
success: '✅',
warning: '⚠️',
danger: '❌',
info: 'ℹ️',
};
这个对象定义了每种类型对应的默认图标:
- primary 和 info:用 ℹ️ 信息图标
- secondary:用 📌 图钉图标
- success:用 ✅ 对勾图标,表示操作成功
- warning:用 ⚠️ 警告图标
- danger:用 ❌ 叉号图标,表示错误
图标让 Toast 的语义更直观。用户不用仔细看文字,光看图标就能大概知道是成功还是失败。
动画和定时器逻辑
tsx
useEffect(() => {
if (visible) {
Animated.parallel([
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),
Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }),
]).start();
这是 Toast 显示时的入场动画,我来逐层拆解:
useEffect 监听 visible:当 visible 变成 true 时,启动入场动画和定时器。
Animated.parallel:让多个动画同时执行。这里是位移动画和透明度动画同时进行。
Animated.spring:弹簧动画,让 translateY 从初始值(-100 或 100)变到 0。弹簧动画有一点"弹性",比线性动画更自然。
Animated.timing:时间动画,让 opacity 在 200ms 内从 0 变到 1。透明度变化用线性动画就够了,不需要弹性。
useNativeDriver: true:所有动画都用原生驱动,性能更好。
tsx
const timer = setTimeout(() => {
Animated.parallel([
Animated.timing(translateY, {
toValue: position === 'top' ? -100 : 100,
duration: 200,
useNativeDriver: true
}),
Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true }),
]).start(() => onHide?.());
}, duration);
return () => clearTimeout(timer);
}
}, [visible]);
这是 Toast 的自动隐藏逻辑:
setTimeout:在 duration 毫秒后执行隐藏动画。默认是 3000ms,也就是 3 秒后开始隐藏。
隐藏动画:和入场动画相反,translateY 从 0 变回初始位置,opacity 从 1 变回 0。
动画完成回调 :.start(() => onHide?.()) 在动画完成后调用 onHide。这里用可选链(?.)是因为 onHide 可能没传。
清理定时器 :return () => clearTimeout(timer) 是 useEffect 的清理函数。如果组件在定时器触发前被卸载,或者 visible 变化了,需要清除之前的定时器,避免内存泄漏或重复触发。
依赖数组 [visible]:只有 visible 变化时才重新执行这个 effect。
条件渲染
tsx
if (!visible) return null;
如果 visible 是 false,直接返回 null,不渲染任何内容。
这行代码很重要。虽然 Toast 有透明度动画,但当它完全隐藏后,应该从 DOM 中移除,而不是留一个透明的元素在那里。这样可以:
- 避免透明元素意外拦截点击事件
- 减少不必要的渲染开销
- 让屏幕阅读器不会读到隐藏的内容
主体渲染
tsx
return (
<Animated.View
style={[
styles.container,
position === 'top' ? styles.top : styles.bottom,
{ backgroundColor: colorValue, opacity, transform: [{ translateY }] },
style,
]}
>
Toast 的容器是一个 Animated.View,样式合并了四部分:
styles.container:基础样式,包括定位、布局、内边距、圆角、阴影position === 'top' ? styles.top : styles.bottom:根据 position 选择顶部或底部的定位样式{ backgroundColor: colorValue, opacity, transform: [{ translateY }] }:动态样式,包括背景色和动画属性style:外部传入的自定义样式
注意 opacity 和 translateY 直接传给 style,不需要调用 .getValue()。Animated.View 会自动处理 Animated.Value 类型的值。
tsx
<Text style={styles.icon}>{icon || defaultIcons[type]}</Text>
<Text style={styles.message}>{message}</Text>
</Animated.View>
);
};
Toast 的内容很简单,就是图标和文字:
图标:如果外部传了 icon 就用外部的,否则用 defaultIcons 里的默认图标。
消息:直接显示 message 文字,样式设置了白色字体和 flex: 1(占据剩余空间)。
Toast 的内容结构故意设计得很简单。它不像 Alert 那样可以有标题、操作按钮,因为 Toast 的定位就是"轻量级提示",信息太多反而不合适。
样式定义
tsx
const styles = StyleSheet.create({
container: {
position: 'absolute',
left: UITheme.spacing.lg,
right: UITheme.spacing.lg,
flexDirection: 'row',
alignItems: 'center',
padding: UITheme.spacing.md,
borderRadius: UITheme.borderRadius.md,
...UITheme.shadow.lg,
},
容器的基础样式:
position: 'absolute':绝对定位,脱离文档流。Toast 需要浮在页面内容之上left和right都设置了 16px 的边距,让 Toast 不会贴边,左右留白flexDirection: 'row':图标和文字横向排列alignItems: 'center':垂直居中padding: UITheme.spacing.md:内边距 12pxborderRadius: UITheme.borderRadius.md:圆角 8px...UITheme.shadow.lg:大号阴影,让 Toast 有"浮起来"的感觉
tsx
top: { top: UITheme.spacing.xl },
bottom: { bottom: UITheme.spacing.xl },
位置样式:
top:距离顶部 24pxbottom:距离底部 24px
这个距离是经过考量的。太近会贴边,太远会遮挡内容。24px 是一个比较舒适的距离。
tsx
icon: { fontSize: 16, marginRight: UITheme.spacing.sm },
message: { flex: 1, color: UITheme.colors.white, fontSize: UITheme.fontSize.md },
});
图标和文字样式:
- 图标大小 16px,右边距 8px
- 文字用白色(因为背景是彩色的),字号 14px,flex: 1 让文字占据剩余空间
Demo 页面解析
Demo 页面展示了 Toast 的各种用法,路径是 src/screens/demos/ToastDemo.tsx:
tsx
import React, { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import { ComponentShowcase, ShowcaseSection } from '../ComponentShowcase';
import { Toast } from '../../components/ui/Toast';
import { Button } from '../../components/ui/Button';
import { UITheme } from '../../components/ui/theme';
引入了展示框架组件、Toast 组件和 Button 组件。Toast 的 Demo 需要用按钮来触发显示。
tsx
export const ToastDemo: React.FC<{ onBack: () => void }> = ({ onBack }) => {
const [toast, setToast] = useState<{
visible: boolean;
type: any;
message: string;
position: any
}>({
visible: false,
type: 'info',
message: '',
position: 'bottom',
});
用 useState 管理 Toast 的状态。状态对象包含四个字段:
visible:是否显示type:类型message:消息内容position:位置
初始状态是不显示(visible: false)。
tsx
const showToast = (type: any, message: string, position: any = 'bottom') => {
setToast({ visible: true, type, message, position });
};
这是一个辅助函数,用于显示 Toast。调用时传入 type、message 和 position,函数会把 visible 设为 true 并更新其他字段。
tsx
return (
<ComponentShowcase title="Toast" icon="💬" description="轻提示用于显示简短的操作反馈" onBack={onBack}>
<ShowcaseSection title="类型" description="不同语义的提示类型">
<View style={styles.row}>
<Button title="信息" size="sm" onPress={() => showToast('info', '这是一条信息提示')} />
<Button title="成功" size="sm" color="success" onPress={() => showToast('success', '操作成功')} style={styles.ml} />
<Button title="警告" size="sm" color="warning" onPress={() => showToast('warning', '请注意')} style={styles.ml} />
<Button title="错误" size="sm" color="danger" onPress={() => showToast('danger', '操作失败')} style={styles.ml} />
</View>
</ShowcaseSection>
第一个展示区块展示四种语义类型。每个按钮点击后会显示对应类型的 Toast:
- 信息按钮:显示蓝色的 info Toast
- 成功按钮:显示绿色的 success Toast
- 警告按钮:显示橙色的 warning Toast
- 错误按钮:显示红色的 danger Toast
按钮的颜色和 Toast 的颜色对应,让用户更容易理解不同类型的含义。
tsx
<ShowcaseSection title="位置" description="顶部或底部显示">
<View style={styles.row}>
<Button title="顶部提示" size="sm" variant="outline" onPress={() => showToast('info', '顶部显示的提示', 'top')} />
<Button title="底部提示" size="sm" variant="outline" onPress={() => showToast('info', '底部显示的提示', 'bottom')} style={styles.ml} />
</View>
</ShowcaseSection>
第二个展示区块展示位置选项。用 outline 样式的按钮和上面的实心按钮区分开。
顶部 Toast 适合的场景:
- 键盘弹出时,底部被遮挡
- 页面底部有重要操作按钮,不想被遮挡
- 某些设计规范要求在顶部显示
tsx
<ShowcaseSection title="自定义图标" description="使用自定义图标">
<View style={styles.row}>
<Button title="🎉 庆祝" size="sm" onPress={() => showToast('success', '恭喜你完成任务!')} />
<Button title="📦 发货" size="sm" color="info" onPress={() => showToast('info', '您的包裹已发出')} style={styles.ml} />
</View>
</ShowcaseSection>
第三个展示区块展示自定义图标的可能性。虽然这个例子没有直接传 icon 属性,但按钮上的 emoji 暗示了可以用不同的图标。
实际使用时可以这样传自定义图标:
tsx
<Toast icon="🎉" message="恭喜你完成任务!" ... />
tsx
<Toast
visible={toast.visible}
message={toast.message}
type={toast.type}
position={toast.position}
onHide={() => setToast({ ...toast, visible: false })}
/>
</ComponentShowcase>
);
};
页面底部放置了 Toast 组件本身。注意几个关键点:
visible={toast.visible}:由状态控制显示隐藏onHide={() => setToast({ ...toast, visible: false })}:隐藏时把 visible 设回 false
这种模式是 Toast 的标准用法:外部管理 visible 状态,Toast 自动隐藏后通过 onHide 通知外部更新状态。
tsx
const styles = StyleSheet.create({
row: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap' },
ml: { marginLeft: UITheme.spacing.sm },
});
Demo 页面的样式:row 让按钮横向排列并允许换行,ml 提供按钮之间的间距。
实际应用场景
操作反馈
最常见的场景是操作后的反馈:
tsx
const handleSave = async () => {
try {
await api.save(data);
setToast({ visible: true, type: 'success', message: '保存成功' });
} catch (error) {
setToast({ visible: true, type: 'danger', message: '保存失败,请重试' });
}
};
这个例子展示了 Toast 和异步操作的配合:
- 保存成功时显示绿色的成功提示
- 保存失败时显示红色的错误提示
- 用户不需要做任何操作,看一眼就知道结果了
复制到剪贴板
复制操作的反馈:
tsx
const handleCopy = () => {
Clipboard.setString(text);
setToast({ visible: true, type: 'success', message: '已复制到剪贴板' });
};
复制操作没有明显的视觉变化,用户不知道有没有成功。Toast 提供了必要的反馈。
网络状态变化
网络状态变化时的提示:
tsx
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
if (!state.isConnected) {
setToast({ visible: true, type: 'warning', message: '网络已断开', position: 'top' });
} else if (wasOffline) {
setToast({ visible: true, type: 'success', message: '网络已恢复' });
}
});
return () => unsubscribe();
}, []);
网络断开时在顶部显示警告,恢复时显示成功提示。用顶部位置是因为网络状态比较重要,放在顶部更醒目。
表单验证
表单验证失败时的提示:
tsx
const handleSubmit = () => {
if (!email) {
setToast({ visible: true, type: 'warning', message: '请输入邮箱地址' });
return;
}
if (!isValidEmail(email)) {
setToast({ visible: true, type: 'danger', message: '邮箱格式不正确' });
return;
}
// 提交表单...
};
验证失败时用 Toast 提示用户哪里出了问题。这比 Alert 更轻量,不会打断用户的输入流程。
封装 useToast Hook
在实际项目中,每个页面都写一遍 Toast 状态管理会很繁琐。可以封装一个 Hook 来简化使用:
tsx
const useToast = () => {
const [toast, setToast] = useState({
visible: false,
type: 'info' as ColorType,
message: '',
position: 'bottom' as 'top' | 'bottom',
});
const show = (message: string, options?: { type?: ColorType; position?: 'top' | 'bottom' }) => {
setToast({
visible: true,
message,
type: options?.type || 'info',
position: options?.position || 'bottom',
});
};
const hide = () => {
setToast(prev => ({ ...prev, visible: false }));
};
const ToastComponent = () => (
<Toast
visible={toast.visible}
message={toast.message}
type={toast.type}
position={toast.position}
onHide={hide}
/>
);
return { show, hide, ToastComponent };
};
使用方式:
tsx
const MyPage = () => {
const { show, ToastComponent } = useToast();
const handleSave = async () => {
try {
await api.save();
show('保存成功', { type: 'success' });
} catch {
show('保存失败', { type: 'danger' });
}
};
return (
<View>
<Button title="保存" onPress={handleSave} />
<ToastComponent />
</View>
);
};
这样每个页面只需要调用 show() 方法,不需要手动管理状态。
设计建议
内容简短
Toast 的内容应该简短明了,通常不超过 10 个字:
好的例子:
- "保存成功"
- "已复制"
- "网络错误"
不好的例子:
- "您的数据已经成功保存到服务器,感谢您的使用"
- "由于网络连接问题,您的请求未能成功发送,请检查网络后重试"
如果信息太长,考虑用 Alert 而不是 Toast。
时长选择
duration 的选择要根据信息的重要程度:
- 2000ms:非常简短的信息,如"已复制"
- 3000ms:默认时长,适合大多数场景
- 4000-5000ms:稍长的信息,或者需要用户注意的警告
不建议超过 5 秒,太长会让用户觉得烦。
避免堆叠
不要同时显示多个 Toast。如果有新的 Toast 需要显示,应该先隐藏旧的:
tsx
const show = (message: string, type: ColorType) => {
// 先隐藏旧的
setToast(prev => ({ ...prev, visible: false }));
// 稍微延迟后显示新的,让隐藏动画完成
setTimeout(() => {
setToast({ visible: true, message, type, position: 'bottom' });
}, 200);
};
位置一致性
在同一个应用里,Toast 的位置应该保持一致。不要有的页面在顶部,有的在底部,会让用户困惑。
例外情况:
- 键盘弹出时临时改到顶部
- 底部有重要操作按钮时改到顶部
和 Alert 的区别
Toast 和 Alert 都是提示类组件,但使用场景不同:
- Toast:自动消失,不需要用户响应。适合轻量级的操作反馈
- Alert:持久显示,可能需要用户关闭或响应。适合重要信息
选择依据:
- 用户需要仔细阅读吗?需要就用 Alert
- 用户需要做出响应吗?需要就用 Alert
- 只是告知一下就行?用 Toast
举个例子:
- "保存成功"------用 Toast,用户知道就行
- "您的会员即将到期,请续费"------用 Alert,需要用户注意并可能采取行动
和 Snackbar 的区别
在一些设计系统里(比如 Material Design),有一个叫 Snackbar 的组件,和 Toast 很像但有一些区别:
- Toast:纯展示,没有操作按钮
- Snackbar:可以有一个操作按钮,比如"撤销"
当前这个 Toast 实现是纯展示的。如果需要带操作按钮的提示,可以考虑扩展 Toast 或者使用 Alert。
无障碍考虑
Toast 的自动消失特性对无障碍用户可能不太友好。屏幕阅读器用户可能还没听完就消失了。
一些改进建议:
- 给 Toast 添加
accessibilityRole="alert",让屏幕阅读器优先朗读 - 对于重要信息,考虑增加 duration 或使用 Alert
- 提供一个"查看历史提示"的入口
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
