
那个圆圆的加号
打开很多 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