RN for OpenHarmony 实战 TodoList 项目:顶部导航栏

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

应用的门面

打开一个 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} 绑定开关的状态。darkModetrue 时开关打开,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',这是响应式的。不管屏幕多宽,标题在左边,主题切换在右边,中间的空间自动分配。

在很窄的屏幕上(比如小屏手机),标题和主题切换可能会靠得很近。如果内容太多放不下,可以考虑:

  1. 缩小字号
  2. 隐藏副标题
  3. 把主题切换移到设置页

我们的设计在大多数设备上都能正常显示,不需要特殊处理。


导航栏的可访问性

为了让导航栏对所有用户友好:

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

相关推荐
技术狂小子7 小时前
前端开发中那些看似微不足道却影响体验的细节
javascript
用户12039112947267 小时前
使用 Tailwind CSS 构建现代登录页面:从 Vite 配置到 React 交互细节
前端·javascript·react.js
花归去8 小时前
echarts 柱状图曲线图
开发语言·前端·javascript
老前端的功夫8 小时前
TypeScript 类型魔术:模板字面量类型的深层解密与工程实践
前端·javascript·ubuntu·架构·typescript·前端框架
Nan_Shu_6148 小时前
学习: Threejs (2)
前端·javascript·学习
G_G#8 小时前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
程序猿追9 小时前
【鸿蒙PC桌面端实战】从零构建 ArkTS 高性能图像展示器:DevEco Studio 调试与 HDC 命令行验证全流程
华为·harmonyos
@大迁世界9 小时前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
Amumu121389 小时前
React面向组件编程
开发语言·前端·javascript