rn_for_openharmony常用组件_Tooltip提示

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

做移动端开发的时候,经常遇到一个问题:界面上有个图标或者按钮,用户不知道是干嘛的。

PC 端好办,鼠标悬停显示个提示就行。移动端没有鼠标,怎么办?

答案是长按。用户长按某个元素,弹出一个小气泡告诉他这是什么。松手气泡消失,不影响正常操作。

今天我们来实现这个 Tooltip 组件。


完整源码一览

先把代码完整贴出来,文件位置 src/components/ui/Tooltip.tsx

tsx 复制代码
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native';
import { UITheme } from './theme';

interface TooltipProps {
  content: string;
  children: React.ReactNode;
  position?: 'top' | 'bottom' | 'left' | 'right';
  variant?: 'dark' | 'light';
  style?: ViewStyle;
}

export const Tooltip: React.FC<TooltipProps> = ({
  content,
  children,
  position = 'top',
  variant = 'dark',
  style,
}) => {
  const [visible, setVisible] = useState(false);

  const getPositionStyle = (): ViewStyle => {
    switch (position) {
      case 'top':
        return { bottom: '100%', left: '50%', marginBottom: 8 };
      case 'bottom':
        return { top: '100%', left: '50%', marginTop: 8 };
      case 'left':
        return { right: '100%', top: '50%', marginRight: 8 };
      case 'right':
        return { left: '100%', top: '50%', marginLeft: 8 };
      default:
        return {};
    }
  };

  const bgColor = variant === 'dark' ? UITheme.colors.gray[800] : UITheme.colors.white;
  const textColor = variant === 'dark' ? UITheme.colors.white : UITheme.colors.gray[800];

  return (
    <View style={[styles.container, style]}>
      <TouchableOpacity
        onPressIn={() => setVisible(true)}
        onPressOut={() => setVisible(false)}
        activeOpacity={1}
      >
        {children}
      </TouchableOpacity>
      {visible && (
        <View
          style={[
            styles.tooltip,
            getPositionStyle(),
            { backgroundColor: bgColor },
            variant === 'light' && styles.lightShadow,
          ]}
        >
          <Text style={[styles.text, { color: textColor }]}>{content}</Text>
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: { position: 'relative' },
  tooltip: {
    position: 'absolute',
    paddingHorizontal: UITheme.spacing.md,
    paddingVertical: UITheme.spacing.sm,
    borderRadius: UITheme.borderRadius.sm,
    zIndex: 1000,
  },
  lightShadow: { ...UITheme.shadow.md, borderWidth: 1, borderColor: UITheme.colors.gray[200] },
  text: { fontSize: UITheme.fontSize.sm, whiteSpace: 'nowrap' },
});

60 行代码,一个完整的 Tooltip。下面我们一步步拆解。


Step 1:引入依赖

tsx 复制代码
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native';
import { UITheme } from './theme';

这里有个关键点:useState

Tooltip 需要记住"当前是否显示"这个状态。用户按下去,状态变成 true,气泡出现;用户松手,状态变成 false,气泡消失。

TouchableOpacity 是 React Native 提供的可触摸组件,它能监听按下和松开事件,正好满足我们的需求。


Step 2:定义属性接口

tsx 复制代码
interface TooltipProps {
  content: string;
  children: React.ReactNode;
  position?: 'top' | 'bottom' | 'left' | 'right';
  variant?: 'dark' | 'light';
  style?: ViewStyle;
}

五个属性,各有用途:

content 是提示文字,必填。没有提示内容,Tooltip 就没有存在的意义。

children 是被包裹的元素,也是必填。Tooltip 是个"包装器",它需要知道包装谁。

position 控制气泡出现的位置。上下左右四个方向,默认在上方。为什么默认上方?因为用户的手指按住元素时,通常会遮住下方,气泡显示在上方不容易被挡住。

variant 是视觉风格。dark 是深色背景浅色文字,light 是浅色背景深色文字。深色更醒目,浅色更柔和。

style 用于外部样式覆盖,标准操作。


Step 3:组件函数签名和状态

tsx 复制代码
export const Tooltip: React.FC<TooltipProps> = ({
  content,
  children,
  position = 'top',
  variant = 'dark',
  style,
}) => {
  const [visible, setVisible] = useState(false);

解构赋值的同时设置默认值,这是 React 组件的常见写法。

visible 状态控制气泡的显示隐藏。初始值 false,气泡默认不显示。

为什么用 useState 而不是 useRef?因为 visible 的变化需要触发重新渲染。useRef 的变化不会触发渲染,气泡就不会出现。


Step 4:位置计算函数

tsx 复制代码
const getPositionStyle = (): ViewStyle => {
  switch (position) {
    case 'top':
      return { bottom: '100%', left: '50%', marginBottom: 8 };
    case 'bottom':
      return { top: '100%', left: '50%', marginTop: 8 };
    case 'left':
      return { right: '100%', top: '50%', marginRight: 8 };
    case 'right':
      return { left: '100%', top: '50%', marginLeft: 8 };
    default:
      return {};
  }
};

这个函数根据 position 返回不同的定位样式。

position: 'top' 时,气泡在触发元素上方:

  • bottom: '100%' 让气泡的底边对齐触发元素的顶边
  • left: '50%' 让气泡水平居中(大致居中,精确居中需要额外处理)
  • marginBottom: 8 让气泡和触发元素之间有 8px 间距

position: 'bottom' 时,逻辑相反:

  • top: '100%' 让气泡的顶边对齐触发元素的底边
  • marginTop: 8 提供间距

position: 'left'position: 'right' 是水平方向的定位:

  • left 用 right: '100%',气泡在左边
  • right 用 left: '100%',气泡在右边
  • top: '50%' 让气泡垂直居中

这种百分比定位是相对于父元素的,所以父元素需要设置 position: 'relative'


Step 5:颜色计算

tsx 复制代码
const bgColor = variant === 'dark' ? UITheme.colors.gray[800] : UITheme.colors.white;
const textColor = variant === 'dark' ? UITheme.colors.white : UITheme.colors.gray[800];

两行代码,两个三元表达式。

dark 模式:深灰背景 + 白色文字

light 模式:白色背景 + 深灰文字

为什么不直接写在 style 里?因为这两个值在 JSX 里要用两次(背景色和文字色),提取成变量更清晰。


Step 6:渲染结构

tsx 复制代码
return (
  <View style={[styles.container, style]}>
    <TouchableOpacity
      onPressIn={() => setVisible(true)}
      onPressOut={() => setVisible(false)}
      activeOpacity={1}
    >
      {children}
    </TouchableOpacity>
    {visible && (
      <View
        style={[
          styles.tooltip,
          getPositionStyle(),
          { backgroundColor: bgColor },
          variant === 'light' && styles.lightShadow,
        ]}
      >
        <Text style={[styles.text, { color: textColor }]}>{content}</Text>
      </View>
    )}
  </View>
);

结构分三层来看:

最外层 View

styles.container 设置了 position: 'relative',这是绝对定位的基础。气泡用绝对定位,它需要一个相对定位的父元素作为参照。

TouchableOpacity 触摸层

包裹 children,监听触摸事件。

onPressIn 在手指按下时触发,设置 visible 为 true。
onPressOut 在手指抬起时触发,设置 visible 为 false。

activeOpacity={1} 禁用了按下时的透明度变化。默认情况下 TouchableOpacity 按下会变半透明,但 Tooltip 的触发元素不需要这个反馈,所以设为 1(完全不透明)。

气泡层

{visible && ...} 是条件渲染,只有 visible 为 true 时才渲染气泡。

样式数组里有四项:

  1. styles.tooltip 基础样式
  2. getPositionStyle() 位置样式
  3. { backgroundColor: bgColor } 背景色
  4. variant === 'light' && styles.lightShadow 浅色模式的阴影

第四项用了短路求值,只有 light 模式才会加上阴影样式。


Step 7:样式定义

tsx 复制代码
const styles = StyleSheet.create({
  container: { position: 'relative' },
  tooltip: {
    position: 'absolute',
    paddingHorizontal: UITheme.spacing.md,
    paddingVertical: UITheme.spacing.sm,
    borderRadius: UITheme.borderRadius.sm,
    zIndex: 1000,
  },
  lightShadow: { ...UITheme.shadow.md, borderWidth: 1, borderColor: UITheme.colors.gray[200] },
  text: { fontSize: UITheme.fontSize.sm, whiteSpace: 'nowrap' },
});

container

只有一个属性 position: 'relative',为气泡的绝对定位提供参照。

tooltip

position: 'absolute' 让气泡脱离文档流,可以定位到任意位置。

paddingHorizontalpaddingVertical 给文字留出呼吸空间。

borderRadius 让气泡有圆角,看起来更柔和。

zIndex: 1000 确保气泡显示在其他元素上方。这个值要足够大,不然可能被其他元素遮住。

lightShadow

浅色模式需要阴影来和背景区分开。...UITheme.shadow.md 展开主题里的阴影配置,再加上细边框增强边界感。

text

whiteSpace: 'nowrap' 防止文字换行。Tooltip 的内容通常很短,换行会让气泡变得很奇怪。


Demo 实战

看完实现,来看看怎么用。文件位置 src/screens/demos/TooltipDemo.tsx

基础用法

tsx 复制代码
<Tooltip content="这是一条提示信息">
  <Button title="长按我" size="sm" />
</Tooltip>

最简单的用法:用 Tooltip 包裹一个按钮,设置 content。用户长按按钮,提示信息就会出现。

Tooltip 是个"高阶组件"的思路------它不改变被包裹元素的外观和行为,只是给它增加了"长按显示提示"的能力。

四个方向

tsx 复制代码
<Tooltip content="顶部提示" position="top">
  <Button title="上" size="sm" variant="outline" />
</Tooltip>
<Tooltip content="底部提示" position="bottom">
  <Button title="下" size="sm" variant="outline" />
</Tooltip>
<Tooltip content="左侧提示" position="left">
  <Button title="左" size="sm" variant="outline" />
</Tooltip>
<Tooltip content="右侧提示" position="right">
  <Button title="右" size="sm" variant="outline" />
</Tooltip>

四个按钮,四个方向。实际使用时要根据元素在页面中的位置选择合适的方向:

  • 元素靠近顶部,用 bottom
  • 元素靠近底部,用 top
  • 元素靠近左边,用 right
  • 元素靠近右边,用 left

原则是让气泡有足够的空间显示,不要超出屏幕。

深色和浅色

tsx 复制代码
<Tooltip content="深色提示" variant="dark">
  <Button title="深色" size="sm" />
</Tooltip>
<Tooltip content="浅色提示" variant="light" style={styles.ml}>
  <Button title="浅色" size="sm" variant="outline" />
</Tooltip>

深色气泡在浅色背景上更醒目,浅色气泡在深色背景上更合适。

大多数情况下用深色就行,它的对比度更高,更容易被注意到。

包裹文字

tsx 复制代码
<Text style={styles.text}>
  这是一段文字,其中
  <Tooltip content="这是一个专业术语的解释">
    <Text style={styles.highlight}>专业术语</Text>
  </Tooltip>
  需要额外说明。
</Text>

Tooltip 不只能包裹按钮,也能包裹文字。

这个例子里,"专业术语"四个字被高亮显示(蓝色+下划线),用户长按就能看到解释。这种用法在文档类应用里很常见。

样式定义:

tsx 复制代码
highlight: { color: UITheme.colors.primary, textDecorationLine: 'underline' },

蓝色文字加下划线,暗示"这里有额外信息"。

包裹图标

tsx 复制代码
<Tooltip content="设置">
  <Text style={styles.icon}>⚙️</Text>
</Tooltip>
<Tooltip content="帮助" style={styles.ml}>
  <Text style={styles.icon}>❓</Text>
</Tooltip>
<Tooltip content="通知" style={styles.ml}>
  <Text style={styles.icon}>🔔</Text>
</Tooltip>

图标按钮是 Tooltip 最典型的使用场景。

图标的含义不是所有人都能一眼看懂,加上 Tooltip 可以降低用户的认知负担。特别是一些不太常见的图标,Tooltip 几乎是必须的。


几个可以改进的地方

这个 Tooltip 实现了核心功能,但还有优化空间:

气泡没有箭头

很多 Tooltip 设计会在气泡上加一个小三角形,指向触发元素。这需要额外的 View 和一些三角形的 CSS 技巧。

居中不够精确

left: '50%' 只是让气泡的左边缘在中点,气泡本身并没有居中。精确居中需要知道气泡的宽度,然后用 transform: translateX(-50%),但 React Native 对 transform 的百分比支持有限。

没有边界检测

如果触发元素在屏幕边缘,气泡可能会超出屏幕。完善的实现需要检测边界,自动调整位置。

动画缺失

气泡是瞬间出现和消失的,加上淡入淡出动画会更优雅。

这些都是可以后续迭代的方向。


使用建议

Tooltip 虽然好用,但不要滥用:

  1. 内容要简短。Tooltip 不是用来放大段文字的,一句话说清楚最好。

  2. 不要放关键信息。用户可能不会长按,所以 Tooltip 里的内容应该是"锦上添花"而不是"必须知道"。

  3. 触发元素要有暗示。用户怎么知道这个元素可以长按?通过视觉暗示,比如下划线、问号图标、或者和其他元素不同的样式。

  4. 位置要合理。别让气泡被手指挡住,也别让它超出屏幕。


实际项目中的应用

来看几个真实场景的代码:

表单字段说明

tsx 复制代码
const FormField = () => (
  <View style={styles.field}>
    <View style={styles.labelRow}>
      <Text style={styles.label}>手机号</Text>
      <Tooltip content="用于接收验证码和找回密码">
        <Text style={styles.helpIcon}>ⓘ</Text>
      </Tooltip>
    </View>
    <Input placeholder="请输入手机号" />
  </View>
);

表单标签旁边放一个小图标,用户不确定为什么要填这个字段时,长按图标就能看到说明。

功能按钮提示

tsx 复制代码
const Toolbar = () => (
  <View style={styles.toolbar}>
    <Tooltip content="撤销上一步操作" position="bottom">
      <TouchableOpacity style={styles.toolBtn}>
        <Text>↩️</Text>
      </TouchableOpacity>
    </Tooltip>
    <Tooltip content="重做已撤销的操作" position="bottom">
      <TouchableOpacity style={styles.toolBtn}>
        <Text>↪️</Text>
      </TouchableOpacity>
    </Tooltip>
    <Tooltip content="保存当前内容" position="bottom">
      <TouchableOpacity style={styles.toolBtn}>
        <Text>💾</Text>
      </TouchableOpacity>
    </Tooltip>
  </View>
);

工具栏的图标按钮,每个都加上 Tooltip。因为工具栏通常在顶部,所以 position 设为 bottom。

数据指标解释

tsx 复制代码
const StatsCard = ({ value, label, explanation }) => (
  <View style={styles.statsCard}>
    <Text style={styles.statsValue}>{value}</Text>
    <View style={styles.statsLabelRow}>
      <Text style={styles.statsLabel}>{label}</Text>
      {explanation && (
        <Tooltip content={explanation}>
          <Text style={styles.questionMark}>?</Text>
        </Tooltip>
      )}
    </View>
  </View>
);

// 使用
<StatsCard 
  value="85%" 
  label="转化率" 
  explanation="访问用户中完成购买的比例"
/>

数据看板里的指标,有些用户可能不理解是什么意思。加个问号图标,长按显示解释。

禁用按钮说明

tsx 复制代码
const SubmitButton = ({ disabled, disabledReason }) => {
  if (disabled) {
    return (
      <Tooltip content={disabledReason}>
        <View style={styles.disabledButton}>
          <Text style={styles.disabledText}>提交</Text>
        </View>
      </Tooltip>
    );
  }
  
  return (
    <Button title="提交" onPress={handleSubmit} />
  );
};

// 使用
<SubmitButton 
  disabled={!isFormValid} 
  disabledReason="请先填写所有必填项"
/>

按钮被禁用时,用户可能不知道为什么不能点。用 Tooltip 告诉他原因,比单纯的灰色按钮友好得多。


回顾

Tooltip 组件的核心是三件事:

  1. useState 管理显示状态
  2. TouchableOpacityonPressInonPressOut 触发状态变化
  3. 用绝对定位把气泡放到正确的位置

代码不多,但把移动端"长按提示"这个交互模式封装得很好。下次遇到"用户可能不知道这是什么"的场景,试试 Tooltip。


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

相关推荐
HyperAI超神经18 小时前
完整回放|上海创智/TileAI/华为/先进编译实验室/AI9Stars深度拆解 AI 编译器技术实践
人工智能·深度学习·机器学习·开源
清风徐来QCQ18 小时前
SpringMvC
前端·javascript·vue.js
Swift社区18 小时前
ArkTS Web 组件里,如何通过 javaScriptProxy 让 JS 同步调用原生方法
开发语言·前端·javascript
兆龙电子单片机设计18 小时前
【STM32项目开源】STM32单片机智能语音家居控制系统
stm32·单片机·嵌入式硬件·物联网·开源·自动化
Hi_kenyon18 小时前
快速入门VUE与JS(二)--getter函数(取值器)与setter(存值器)
前端·javascript·vue.js
奔跑的露西ly18 小时前
【HarmonyOS NEXT】多线程并发-Worker
华为·harmonyos
全栈前端老曹18 小时前
【前端路由】React Router 权限路由控制 - 登录验证、私有路由封装、高阶组件实现路由守卫
前端·javascript·react.js·前端框架·react-router·前端路由·权限路由
行者9619 小时前
Flutter与OpenHarmony深度整合:打造高性能自定义图表组件
flutter·harmonyos·鸿蒙
zhuà!19 小时前
uv-picker在页面初始化时,设置初始值无效
前端·javascript·uv
摸鱼的春哥19 小时前
实战:在 Docker (Windows) 中构建集成 yt-dlp 的“满血版” n8n 自动化工作流
前端·javascript·后端