
应用的门面
打开一个 App,第一眼看到的往往是顶部导航栏。它告诉你这是什么应用,你在哪个页面,有哪些操作可以做。
导航栏虽然占的空间不大,但承载的信息量不小。我们的 TodoList 应用的导航栏包含三个元素:应用名称、副标题、主题切换开关。简单,但该有的都有。
说实话,很多开发者不太重视导航栏的设计。觉得不就是放个标题嘛,有什么好设计的。但细节决定品质,一个精心设计的导航栏能让整个应用的档次提升一个层级。
导航栏的结构
先看代码:
tsx
<View style={styles.header}>
<View>
<Text style={[styles.headerTitle, {color: theme.text}]}>Todo App</Text>
<Text style={[styles.headerSubtitle, {color: theme.subText}]}>RN for OpenHarmony</Text>
</View>
<View style={styles.themeSwitch}>
<Text style={{color: theme.subText}}>🌙</Text>
<Switch value={darkMode} onValueChange={setDarkMode} trackColor={{false: '#767577', true: theme.accent}} thumbColor={darkMode ? '#fff' : '#f4f3f4'} />
</View>
</View>
结构很清晰:一个大容器 header,里面左边是标题区域,右边是主题切换区域。
左侧标题区域
标题区域有两行文字。第一行是应用名称"Todo App",大字号加粗,是视觉焦点。第二行是副标题"RN for OpenHarmony",小字号浅色,说明这是什么技术栈。
为什么要有副标题?因为这是一个演示项目,副标题告诉用户这个 App 是用 React Native for OpenHarmony 开发的。如果是正式产品,副标题可以换成 slogan,比如"让每一天都有条理"之类的。
右侧主题切换
右侧是一个月亮 emoji 加一个开关。月亮暗示这是深色模式的切换,开关控制深色/浅色主题。这个功能我们后面会专门讲,这里先知道它在导航栏里就行。
导航栏的样式
tsx
header: {flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 16},
headerTitle: {fontSize: 28, fontWeight: 'bold'},
headerSubtitle: {fontSize: 14, marginTop: 2},
themeSwitch: {flexDirection: 'row', alignItems: 'center', gap: 8},
header 容器样式
flexDirection: 'row' 让子元素水平排列。默认是 column,垂直排列。导航栏是横向布局,所以要改成 row。
justifyContent: 'space-between' 让子元素两端对齐。标题在左边,主题切换在右边,中间的空间自动分配。这是导航栏最常见的布局方式。
alignItems: 'center' 让子元素垂直居中。标题区域有两行文字,高度比主题切换区域高。如果不设置垂直居中,它们会顶部对齐,看起来不协调。
paddingVertical: 16 是上下内边距。让导航栏有一定的高度,不会显得太挤。
headerTitle 标题样式
fontSize: 28 是比较大的字号。作为应用名称,要足够醒目。
fontWeight: 'bold' 加粗。和副标题形成对比,强调主次关系。
headerSubtitle 副标题样式
fontSize: 14 比标题小很多。副标题是辅助信息,不应该抢标题的风头。
marginTop: 2 和标题之间有一点间距。太近会显得拥挤,太远会显得分离。2 像素刚刚好。
themeSwitch 主题切换样式
flexDirection: 'row' 让月亮图标和开关水平排列。
alignItems: 'center' 垂直居中对齐。
gap: 8 是图标和开关之间的间距。gap 是比较新的属性,比用 marginRight 更方便。
标题的颜色
tsx
<Text style={[styles.headerTitle, {color: theme.text}]}>Todo App</Text>
<Text style={[styles.headerSubtitle, {color: theme.subText}]}>RN for OpenHarmony</Text>
标题用 theme.text,副标题用 theme.subText。
theme 是我们定义的主题对象,根据深色/浅色模式返回不同的颜色:
tsx
const theme = {
bg: darkMode ? '#0f0f23' : '#f5f5f5',
card: darkMode ? '#1a1a2e' : '#ffffff',
text: darkMode ? '#ffffff' : '#333333',
subText: darkMode ? '#888888' : '#666666',
border: darkMode ? '#2a2a4a' : '#e0e0e0',
accent: '#6c5ce7',
};
深色模式下,text 是白色,subText 是灰色。浅色模式下,text 是深灰色,subText 是浅一点的灰色。
这样切换主题时,导航栏的颜色会自动变化,不用单独处理。
Switch 组件
主题切换用的是 React Native 内置的 Switch 组件:
tsx
<Switch
value={darkMode}
onValueChange={setDarkMode}
trackColor={{false: '#767577', true: theme.accent}}
thumbColor={darkMode ? '#fff' : '#f4f3f4'}
/>
value 属性
value={darkMode} 绑定开关的状态。darkMode 是 true 时开关打开,false 时关闭。
onValueChange 属性
onValueChange={setDarkMode} 是状态变化时的回调。用户拨动开关,setDarkMode 会被调用,参数是新的布尔值。
这里直接把 setDarkMode 传给 onValueChange,因为它们的签名一致。onValueChange 会传一个布尔值,setDarkMode 接收一个布尔值。
trackColor 属性
trackColor={``{false: '#767577', true: theme.accent}} 设置轨道颜色。
false 是关闭状态的轨道颜色,灰色。true 是打开状态的轨道颜色,主题强调色紫色。
thumbColor 属性
thumbColor={darkMode ? '#fff' : '#f4f3f4'} 设置滑块颜色。
深色模式下滑块是白色,浅色模式下是浅灰色。这个颜色差异很微妙,但能让开关在不同背景下都清晰可见。
状态栏的配置
导航栏上面还有系统状态栏,显示时间、电量、信号等。我们需要配置状态栏的样式,让它和应用风格一致:
tsx
<StatusBar barStyle={darkMode ? 'light-content' : 'dark-content'} backgroundColor={theme.bg} />
barStyle 属性
barStyle 控制状态栏文字和图标的颜色。
'light-content' 是浅色内容,适合深色背景。深色模式下,背景是深色的,状态栏文字要用浅色才能看清。
'dark-content' 是深色内容,适合浅色背景。浅色模式下,背景是浅色的,状态栏文字要用深色。
backgroundColor 属性
backgroundColor={theme.bg} 设置状态栏背景色,和应用背景色一致。
这个属性主要在 Android 上有效。iOS 的状态栏是透明的,会显示下面的内容。
SafeAreaView 的作用
我们的应用最外层用了 SafeAreaView:
tsx
<SafeAreaView style={[styles.container, {backgroundColor: theme.bg}]}>
<StatusBar ... />
...
</SafeAreaView>
SafeAreaView 会自动避开系统的安全区域,比如 iPhone 的刘海和底部横条。这样导航栏不会被刘海遮挡。
如果用普通的 View,在有刘海的设备上,导航栏可能会和状态栏重叠,看起来很乱。SafeAreaView 帮我们处理了这个问题。
导航栏的位置
导航栏放在 content 容器的最上面:
tsx
<Animated.View style={[styles.content, {opacity: fadeAnim, transform: [{translateY: slideAnim}]}]}>
{/* 顶部导航栏 */}
<View style={styles.header}>
...
</View>
{/* 当前页面内容 */}
{renderCurrentPage()}
{/* 浮动添加按钮 */}
...
{/* 底部 Tab 栏 */}
...
</Animated.View>
导航栏是固定在顶部的,不会随着页面内容滚动。这是因为它在 FlatList 外面。如果把导航栏放在 FlatList 里面,它会随着列表滚动,这不是我们想要的效果。
导航栏的动画
注意到导航栏是在 Animated.View 里面的:
tsx
<Animated.View style={[styles.content, {opacity: fadeAnim, transform: [{translateY: slideAnim}]}]}>
应用启动时,整个内容区域(包括导航栏)会有一个淡入和上滑的动画:
tsx
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(50)).current;
useEffect(() => {
Animated.parallel([
Animated.timing(fadeAnim, {toValue: 1, duration: 500, useNativeDriver: true}),
Animated.spring(slideAnim, {toValue: 0, tension: 50, friction: 8, useNativeDriver: true}),
]).start();
}, []);
fadeAnim 从 0 变到 1,实现淡入效果。slideAnim 从 50 变到 0,实现从下往上滑入的效果。
这个动画让应用启动时不会突然出现,而是优雅地展现出来。虽然只有半秒钟,但能让用户感觉应用更精致。
不同页面的导航栏
我们的应用有三个页面:任务列表、统计、设置。它们共用同一个导航栏。
tsx
const renderCurrentPage = () => {
switch (activeTab) {
case 0: return <TasksPage />;
case 1: return <StatsPage />;
case 2: return <SettingsPage />;
default: return <TasksPage />;
}
};
切换页面时,导航栏不变,只有下面的内容变化。这种设计让用户知道自己还在同一个应用里,只是看不同的内容。
如果每个页面都有不同的导航栏,用户可能会困惑"我是不是进了另一个应用"。统一的导航栏提供了一致的体验。
当然,有些应用会根据页面显示不同的导航栏标题。比如在任务列表页显示"任务",在统计页显示"统计"。这也是可以的,取决于产品设计。
导航栏的高度
我们没有显式设置导航栏的高度,而是用 paddingVertical: 16 让内容撑开高度。
这样做的好处是导航栏的高度会根据内容自适应。如果标题文字变大,导航栏会自动变高。如果以后要加更多元素,也不用手动调整高度。
有些设计会固定导航栏高度,比如 44 像素或 56 像素。这样做的好处是高度一致,不会因为内容变化而跳动。缺点是灵活性差,内容多了可能放不下。
我们选择自适应高度,因为导航栏的内容是固定的,不会有太大变化。
导航栏的边框
我们的导航栏没有底部边框,和下面的内容是融为一体的。
有些应用会给导航栏加一条底部边框,把导航栏和内容区域分开。这样做的好处是层次分明,缺点是多了一条线,视觉上稍显复杂。
tsx
header: {
...
borderBottomWidth: 1,
borderBottomColor: theme.border,
},
如果想加边框,可以这样写。我们选择不加,让界面更简洁。
导航栏的阴影
另一种区分导航栏和内容的方式是加阴影:
tsx
header: {
...
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
阴影让导航栏看起来"浮"在内容上面,有层次感。elevation 是 Android 的阴影属性,shadow* 是 iOS 的。
我们的设计选择不加阴影,保持扁平化的风格。阴影和边框都是可选的,取决于整体设计风格。
响应式设计
在不同屏幕尺寸上,导航栏应该怎么表现?
我们的导航栏用了 flexDirection: 'row' 和 justifyContent: 'space-between',这是响应式的。不管屏幕多宽,标题在左边,主题切换在右边,中间的空间自动分配。
在很窄的屏幕上(比如小屏手机),标题和主题切换可能会靠得很近。如果内容太多放不下,可以考虑:
- 缩小字号
- 隐藏副标题
- 把主题切换移到设置页
我们的设计在大多数设备上都能正常显示,不需要特殊处理。
导航栏的可访问性
为了让导航栏对所有用户友好:
tsx
<View style={styles.header} accessibilityRole="header">
<View accessibilityLabel="Todo App,RN for OpenHarmony">
<Text style={[styles.headerTitle, {color: theme.text}]}>Todo App</Text>
<Text style={[styles.headerSubtitle, {color: theme.subText}]}>RN for OpenHarmony</Text>
</View>
<View style={styles.themeSwitch}>
<Switch
...
accessibilityLabel={darkMode ? '关闭深色模式' : '开启深色模式'}
/>
</View>
</View>
accessibilityRole="header" 告诉屏幕阅读器这是一个标题区域。
accessibilityLabel 提供屏幕阅读器朗读的文字。开关的标签会根据当前状态变化,告诉用户点击后会发生什么。
导航栏的扩展
如果以后要在导航栏加更多功能,比如搜索按钮、通知图标、用户头像,怎么办?
可以把右侧区域改成一个按钮组:
tsx
<View style={styles.headerRight}>
<TouchableOpacity style={styles.headerButton}>
<Text>🔍</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.headerButton}>
<Text>🔔</Text>
</TouchableOpacity>
<Switch ... />
</View>
用 flexDirection: 'row' 和 gap 让多个按钮水平排列。
不过要注意,导航栏的空间有限。放太多东西会显得拥挤,也会让用户不知道该点哪个。保持简洁是更好的选择。
小结
导航栏是应用的门面,虽然简单但很重要。
我们的导航栏包含应用标题、副标题和主题切换开关。用 flexDirection: 'row' 和 justifyContent: 'space-between' 实现左右布局。标题用大字号加粗,副标题用小字号浅色。主题切换用 Switch 组件,配合 StatusBar 让状态栏颜色也跟着变化。
导航栏固定在顶部,不随内容滚动。三个页面共用同一个导航栏,提供一致的体验。启动时有淡入和上滑的动画,让应用显得更精致。
好的导航栏应该是"看一眼就知道这是什么应用,找一下就能找到想要的功能"。简洁、清晰、一致,这是导航栏设计的三个关键词。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net