RN for OpenHarmony 实战 TodoList 项目:浮动添加按钮 FAB

案例开源地址:https://atomgit.com/lqjmac/rn_openharmony_todolist

那个圆圆的加号

打开很多 App,右下角都有一个圆圆的按钮,上面一个加号。点一下,就能添加新内容。这就是 FAB,Floating Action Button,浮动操作按钮。

FAB 是 Material Design 带火的设计。它悬浮在界面上方,不管你滚动到哪里,它都在那里。想添加东西?伸手就能点到。

我们的 TodoList 应用也有一个 FAB,紫色的圆形按钮,白色的加号。点击它打开添加任务的弹窗。简单直接,用户一看就懂。


FAB 的代码

tsx 复制代码
{activeTab === 0 && (
  <TouchableOpacity style={[styles.fab, {backgroundColor: theme.accent}]} onPress={() => setShowAddModal(true)}>
    <Text style={styles.fabIcon}>+</Text>
  </TouchableOpacity>
)}

代码很短,但信息量不小。

条件渲染

activeTab === 0 && 表示只在任务页显示 FAB。统计页和设置页不需要添加任务的按钮,显示 FAB 没有意义。

这是一个常见的模式:FAB 只在需要它的页面显示。如果每个页面都有 FAB,用户会困惑"这个按钮在这个页面是干什么的"。

TouchableOpacity

TouchableOpacity 而不是 Button,因为我们需要完全自定义样式。Button 组件在不同平台上样式不一样,而且自定义能力有限。

TouchableOpacity 按下时会降低透明度,给用户反馈。这个反馈很微妙但很重要,让用户知道"我点到了"。

onPress 回调

点击 FAB 执行 setShowAddModal(true),打开添加任务的弹窗。FAB 本身不处理添加逻辑,只是一个入口。


FAB 的样式

tsx 复制代码
fab: {
  position: 'absolute', 
  right: 20, 
  bottom: 80, 
  width: 56, 
  height: 56, 
  borderRadius: 28, 
  justifyContent: 'center', 
  alignItems: 'center', 
  shadowColor: '#6c5ce7', 
  shadowOffset: {width: 0, height: 4}, 
  shadowOpacity: 0.3, 
  shadowRadius: 8, 
  elevation: 8
},
fabIcon: {fontSize: 32, color: '#fff', fontWeight: '300'},

绝对定位

position: 'absolute' 让 FAB 脱离文档流,可以自由定位。

right: 20, bottom: 80 把 FAB 放在右下角。为什么 bottom 是 80 而不是 20?因为要避开底部的 Tab 栏。Tab 栏大约 60 像素高,加上一些间距,80 像素刚好让 FAB 在 Tab 栏上方。

尺寸和形状

width: 56, height: 56 是 Material Design 推荐的标准 FAB 尺寸。56dp 不大不小,容易点击又不会太占空间。

borderRadius: 28 是宽度的一半,让按钮变成完美的圆形。

内容居中

justifyContent: 'center', alignItems: 'center' 让加号图标在按钮中央。

阴影效果

阴影让 FAB 看起来"浮"在界面上方,有层次感。

iOS 用 shadow* 属性:

  • shadowColor: '#6c5ce7' 阴影颜色,用主题强调色让阴影有一点紫色调
  • shadowOffset: {width: 0, height: 4} 阴影偏移,向下偏移 4 像素,模拟光从上方照射
  • shadowOpacity: 0.3 阴影透明度,30% 不会太重
  • shadowRadius: 8 阴影模糊半径,让阴影边缘柔和

Android 用 elevation: 8,数值越大阴影越明显。8 是一个比较合适的值,FAB 应该有明显的阴影来表示它"浮"在上面。


加号图标

tsx 复制代码
<Text style={styles.fabIcon}>+</Text>

加号用 Text 组件显示,不是图标库。这是最简单的方式,一个加号字符就够了。

图标样式

fontSize: 32 让加号足够大,在 56 像素的按钮里很醒目。

color: '#fff' 白色,和紫色背景形成对比。

fontWeight: '300' 细体。加号默认可能比较粗,用细体看起来更优雅。这是一个小细节,但能让 FAB 看起来更精致。

为什么不用图标库

用图标库(比如 react-native-vector-icons)可以得到更标准的加号图标。但对于一个简单的加号,文字就够了。引入图标库会增加包体积,对于演示项目来说没必要。

如果你的应用需要很多图标,用图标库是更好的选择。图标库的图标更一致,也有更多选择。


FAB 的位置

FAB 放在右下角是最常见的位置。为什么?

拇指可达

大多数人用右手握手机,右下角是拇指最容易够到的地方。把最常用的操作放在这里,用户单手就能完成。

不遮挡内容

右下角通常是内容较少的区域。列表项从左边开始,右下角往往是空白。FAB 放在这里不会遮挡重要内容。

视觉平衡

界面左上角通常有标题,右下角放一个 FAB,形成对角线的平衡。

其他位置

FAB 也可以放在其他位置:

  • 右上角:不太常见,因为那里通常有其他按钮
  • 左下角:左撇子友好,但不是主流
  • 底部居中:有些应用这样做,但会和 Tab 栏冲突
  • 跟随内容:FAB 在列表底部,随内容滚动。这种设计比较少见

我们选择右下角,是最安全的选择。


FAB 和 Tab 栏的关系

FAB 在 Tab 栏上方,两者不重叠。

tsx 复制代码
fab: {
  ...
  bottom: 80,
},
tabBar: {
  position: 'absolute',
  bottom: 0,
  ...
  paddingBottom: 20,
},

Tab 栏高度大约 60 像素(paddingVertical: 8 + paddingBottom: 20 + 内容高度)。FAB 的 bottom: 80 让它在 Tab 栏上方,中间有一些间距。

如果 FAB 和 Tab 栏重叠,用户可能误触。保持间距是必要的。


FAB 的点击区域

FAB 是 56x56 像素,这个尺寸够大吗?

Apple 的人机界面指南建议触摸目标至少 44x44 点。Material Design 建议至少 48x48 dp。我们的 56x56 超过了这两个标准,点击区域足够大。

如果 FAB 太小,用户可能点不准,需要多次尝试。这会让人沮丧。56 像素是一个舒适的尺寸。


FAB 的动画

当前的实现没有特别的动画,只有 TouchableOpacity 默认的透明度变化。可以加一些动画让 FAB 更生动。

按下缩放

按下时 FAB 稍微缩小,松开时弹回:

tsx 复制代码
const scaleAnim = useRef(new Animated.Value(1)).current;

const onPressIn = () => {
  Animated.spring(scaleAnim, {toValue: 0.9, useNativeDriver: true}).start();
};

const onPressOut = () => {
  Animated.spring(scaleAnim, {toValue: 1, useNativeDriver: true}).start();
};

<Animated.View style={[styles.fab, {backgroundColor: theme.accent, transform: [{scale: scaleAnim}]}]}>
  <TouchableOpacity 
    style={{width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center'}}
    onPressIn={onPressIn}
    onPressOut={onPressOut}
    onPress={() => setShowAddModal(true)}
  >
    <Text style={styles.fabIcon}>+</Text>
  </TouchableOpacity>
</Animated.View>

按下时缩小到 90%,松开时弹回 100%。spring 动画有弹性效果,比 timing 更自然。

出现动画

页面加载时,FAB 可以有一个出现动画:

tsx 复制代码
const fabAnim = useRef(new Animated.Value(0)).current;

useEffect(() => {
  Animated.spring(fabAnim, {toValue: 1, tension: 50, friction: 8, useNativeDriver: true}).start();
}, []);

<Animated.View style={[styles.fab, {
  backgroundColor: theme.accent,
  transform: [{scale: fabAnim}],
  opacity: fabAnim,
}]}>
  ...
</Animated.View>

FAB 从 0 放大到 1,同时从透明变成不透明。这个动画让 FAB 的出现更柔和,不会突然蹦出来。

旋转动画

点击 FAB 时,加号可以旋转变成叉号,表示"关闭":

tsx 复制代码
const rotateAnim = useRef(new Animated.Value(0)).current;

const toggleModal = () => {
  if (showAddModal) {
    Animated.timing(rotateAnim, {toValue: 0, duration: 200, useNativeDriver: true}).start();
  } else {
    Animated.timing(rotateAnim, {toValue: 1, duration: 200, useNativeDriver: true}).start();
  }
  setShowAddModal(!showAddModal);
};

const rotation = rotateAnim.interpolate({
  inputRange: [0, 1],
  outputRange: ['0deg', '45deg'],
});

<Animated.Text style={[styles.fabIcon, {transform: [{rotate: rotation}]}]}>+</Animated.Text>

加号旋转 45 度就变成了叉号。这种动画在展开式 FAB 中很常见。

我们的应用没有用这些动画,保持简单。但如果你想让 FAB 更有趣,可以试试。


展开式 FAB

有些应用的 FAB 点击后会展开,显示多个选项:

tsx 复制代码
const [expanded, setExpanded] = useState(false);

{expanded && (
  <>
    <TouchableOpacity style={[styles.miniFab, {bottom: 150}]} onPress={addTask}>
      <Text>📝</Text>
    </TouchableOpacity>
    <TouchableOpacity style={[styles.miniFab, {bottom: 210}]} onPress={addNote}>
      <Text>📌</Text>
    </TouchableOpacity>
  </>
)}

<TouchableOpacity style={styles.fab} onPress={() => setExpanded(!expanded)}>
  <Text style={styles.fabIcon}>{expanded ? '×' : '+'}</Text>
</TouchableOpacity>

点击主 FAB,上方弹出几个小 FAB,每个对应不同的操作。再点击主 FAB 收起。

这种设计适合有多个主要操作的场景。我们的应用只有"添加任务"一个操作,不需要展开式 FAB。


FAB 的可访问性

tsx 复制代码
<TouchableOpacity 
  style={[styles.fab, {backgroundColor: theme.accent}]} 
  onPress={() => setShowAddModal(true)}
  accessibilityRole="button"
  accessibilityLabel="添加新任务"
>
  <Text style={styles.fabIcon}>+</Text>
</TouchableOpacity>

accessibilityRole="button" 告诉屏幕阅读器这是一个按钮。

accessibilityLabel="添加新任务" 是屏幕阅读器朗读的文字。视障用户听到"添加新任务"就知道这个按钮是干什么的,比听到"加号"更清楚。


FAB 和键盘

当键盘弹出时,FAB 可能会被遮挡。如果 FAB 在输入框下方,用户可能看不到它。

解决方案:

隐藏 FAB

键盘弹出时隐藏 FAB:

tsx 复制代码
const [keyboardVisible, setKeyboardVisible] = useState(false);

useEffect(() => {
  const showListener = Keyboard.addListener('keyboardDidShow', () => setKeyboardVisible(true));
  const hideListener = Keyboard.addListener('keyboardDidHide', () => setKeyboardVisible(false));
  return () => {
    showListener.remove();
    hideListener.remove();
  };
}, []);

{!keyboardVisible && activeTab === 0 && (
  <TouchableOpacity style={styles.fab} ...>
    ...
  </TouchableOpacity>
)}

上移 FAB

键盘弹出时把 FAB 往上移,保持在键盘上方:

tsx 复制代码
<TouchableOpacity style={[styles.fab, {bottom: keyboardVisible ? keyboardHeight + 20 : 80}]} ...>

我们的应用在任务页没有输入框(输入框在弹窗里),所以不需要处理这个问题。


FAB 的颜色

tsx 复制代码
{backgroundColor: theme.accent}

FAB 用主题强调色,紫色。这是整个应用最醒目的颜色,用在最重要的操作按钮上。

FAB 的颜色应该和应用的主色调一致。如果应用是蓝色调,FAB 就用蓝色。如果是绿色调,FAB 就用绿色。

有些应用的 FAB 用红色或橙色,表示"这是最重要的操作"。这也可以,但要注意和整体设计协调。

小结

FAB 是一个简单但有效的设计。圆形按钮,加号图标,悬浮在右下角,点击打开添加弹窗。用绝对定位固定位置,用阴影表示悬浮效果,只在需要的页面显示。


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

相关推荐
frontend_frank18 小时前
脱离 Electron autoUpdater:uni-app跨端更新:Windows+Android统一实现方案
android·前端·javascript·electron·uni-app
hqzing18 小时前
低成本玩转鸿蒙容器的丐版方案
docker·harmonyos
wulijuan88866618 小时前
BroadcastChannel API 同源的多个标签页可以使用 BroadcastChannel 进行通讯
前端·javascript·vue.js
kilito_0118 小时前
数字时钟翻页效果
javascript·css·css3
Van_Moonlight18 小时前
RN for OpenHarmony 实战 TodoList 项目:今日任务数量统计
javascript·开源·harmonyos
xkxnq19 小时前
第一阶段:Vue 基础入门(第 13天)
前端·javascript·vue.js
赵民勇19 小时前
ES5中prototype和prototype.constructor详解
javascript
Van_captain19 小时前
rn_for_openharmony常用组件_Tabs选项卡
javascript·开源·harmonyos
特立独行的猫a19 小时前
低成本搭建鸿蒙PC运行环境:基于 Docker 的 x86_64 服务器
docker·容器·harmonyos·鸿蒙pc