React Native for OpenHarmony Toast 轻提示组件:自动消失的操作反馈

项目开源地址:https://atomgit.com/nutpi/rn_for_openharmony_element

Toast 是移动端最常见的反馈组件之一。

点击收藏按钮,底部冒出一个"收藏成功";复制一段文字,屏幕上闪过"已复制到剪贴板";网络请求失败,顶部滑出一个"网络错误"。这些短暂出现又自动消失的提示,就是 Toast。

和 Alert 不同,Toast 不需要用户主动关闭,它会在几秒后自动消失。这种"轻量级"的特性让它特别适合那些"用户知道就行,不需要做任何响应"的场景。

我在项目里把提示类组件分成三类:

  • [持久提示] 需要用户主动关闭,比如 Alert
  • [临时提示] 自动消失,比如 Toast
  • [阻断提示] 必须响应才能继续,比如 Modal

Toast 属于第二类。它的核心任务是"告诉用户发生了什么",然后悄悄消失,不打扰用户继续操作。

这篇文章会基于项目中真实存在的代码,把 Toast 的实现拆开讲清楚。全文代码片段都来自项目中的真实文件,路径是 src/components/ui/Toast.tsx

Toast 在项目里的位置

这套 UI 组件库的文件组织比较统一。Toast 相关的文件主要在这几处:

  • src/components/ui/Toast.tsx
  • src/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.Value
  • Animated 是 React Native 的动画模块,Toast 的滑入滑出效果就靠它
  • UIThemeColorType 来自项目的主题配置,保证组件风格统一

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 中移除,而不是留一个透明的元素在那里。这样可以:

  1. 避免透明元素意外拦截点击事件
  2. 减少不必要的渲染开销
  3. 让屏幕阅读器不会读到隐藏的内容

主体渲染

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:外部传入的自定义样式

注意 opacitytranslateY 直接传给 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 需要浮在页面内容之上
  • leftright 都设置了 16px 的边距,让 Toast 不会贴边,左右留白
  • flexDirection: 'row':图标和文字横向排列
  • alignItems: 'center':垂直居中
  • padding: UITheme.spacing.md:内边距 12px
  • borderRadius: UITheme.borderRadius.md:圆角 8px
  • ...UITheme.shadow.lg:大号阴影,让 Toast 有"浮起来"的感觉
tsx 复制代码
  top: { top: UITheme.spacing.xl },
  bottom: { bottom: UITheme.spacing.xl },

位置样式:

  • top:距离顶部 24px
  • bottom:距离底部 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 的自动消失特性对无障碍用户可能不太友好。屏幕阅读器用户可能还没听完就消失了。

一些改进建议:

  1. 给 Toast 添加 accessibilityRole="alert",让屏幕阅读器优先朗读
  2. 对于重要信息,考虑增加 duration 或使用 Alert
  3. 提供一个"查看历史提示"的入口

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
Van_captain10 小时前
React Native for OpenHarmony Modal 模态框组件:阻断式交互的设计与实现
javascript·开源·harmonyos
xkxnq10 小时前
第一阶段:Vue 基础入门(第 14天)
前端·javascript·vue.js
前端小臻10 小时前
列举react中类组件和函数组件常用到的方法
前端·javascript·react.js
cn_mengbei10 小时前
鸿蒙原生PC应用开发实战:从零搭建到性能优化,掌握ArkTS与DevEco Studio高效开发技巧
华为·性能优化·harmonyos
研☆香10 小时前
html css js文件开发规范
javascript·css·html
ApachePulsar10 小时前
演讲回顾|中原银行开源消息中间件的落地实践
开源
赵民勇10 小时前
JavaScript中的this详解(ES5/ES6)
前端·javascript·es6
wayne21410 小时前
React Native 状态管理方案全梳理:Redux、Zustand、React Query 如何选
javascript·react native·react.js
我的golang之路果然有问题10 小时前
Mac 上的 Vue 安装和配置记录
前端·javascript·vue.js·笔记·macos