
案例项目开源地址:https://atomgit.com/nutpi/wanandroid_rn_openharmony
打开一个 App,第一眼看到的往往是顶部标题栏。它告诉你这是什么 App,你现在在哪个页面。看似简单的一块区域,其实承载了不少功能。
我在做 WanAndroid 这个项目时,花了不少时间打磨顶部标题栏。今天把这个过程分享出来,聊聊怎么设计一个既好看又实用的 Header。
从需求说起
顶部标题栏需要展示什么?
首先是 App 名称。用户打开 App,得知道这是什么。我们的 App 叫 WanAndroid,这个名字要显眼。
其次是当前页面。App 有五个 Tab:首页、体系、导航、项目、我的。用户切换 Tab 后,标题栏要告诉他现在在哪个页面。
最后是一些操作按钮。我们放了一个主题切换按钮,点击可以在深色和浅色模式之间切换。
需求理清了,开始写代码。
标题栏的基本结构
tsx
<View style={styles.header}>
<View>
<Text style={[styles.headerTitle, {color: theme.text}]}>WanAndroid</Text>
<Text style={[styles.headerSubtitle, {color: theme.subText}]}>
技术资讯 · {TAB_TITLES[activeTab]}
</Text>
</View>
<TouchableOpacity onPress={() => setDarkMode(!darkMode)}>
<Text style={{fontSize: 24}}>{darkMode ? '🌙' : '☀️'}</Text>
</TouchableOpacity>
</View>
这段代码看起来不长,但每一行都有讲究。让我一点点拆解。
最外层的 View
<View style={styles.header}> 是标题栏的容器。它把里面的内容包起来,统一设置布局和间距。
为什么用 View 而不是其他组件?View 是 React Native 最基础的容器组件,相当于 Web 里的 div。它没有任何默认样式,完全由我们控制。
左侧的标题区域
左侧又嵌套了一个 View,里面放两个 Text。为什么要嵌套?因为我们想让主标题和副标题垂直排列。
如果不嵌套,直接把两个 Text 放在 header 里,它们会和右侧的按钮一起横向排列,布局就乱了。嵌套一个 View,让两个 Text 在这个 View 里垂直排列,然后这个 View 整体和右侧按钮横向排列。
这是 Flexbox 布局的常见技巧:用嵌套的容器来组织不同方向的布局。
主标题
<Text style={[styles.headerTitle, {color: theme.text}]}>WanAndroid</Text>
主标题显示 App 名称。样式用数组合并了两部分:styles.headerTitle 是静态样式(字号、字重),{color: theme.text} 是动态样式(颜色随主题变化)。
为什么颜色要动态设置?因为深色模式和浅色模式的文字颜色不同。深色模式下背景是深色的,文字要用浅色才能看清;浅色模式反过来。
副标题
<Text style={[styles.headerSubtitle, {color: theme.subText}]}>技术资讯 · {TAB_TITLES[activeTab]}</Text>
副标题有两部分:固定的"技术资讯"和动态的当前页面名称。
TAB_TITLES[activeTab] 根据当前激活的 Tab 索引获取页面名称。TAB_TITLES 是 ['首页', '体系', '导航', '项目', '我的'],activeTab 是 0-4 的数字。
中间用 · 分隔,这是一个中点符号,比普通的点更居中,看起来更协调。
副标题用 theme.subText 颜色,比主标题浅一些,形成层次感。主次分明,用户一眼就能抓住重点。
右侧的主题切换按钮
tsx
<TouchableOpacity onPress={() => setDarkMode(!darkMode)}>
<Text style={{fontSize: 24}}>{darkMode ? '🌙' : '☀️'}</Text>
</TouchableOpacity>
TouchableOpacity 是可点击的容器,点击时会有透明度变化的反馈。
onPress={() => setDarkMode(!darkMode)} 点击时切换主题。!darkMode 把当前值取反:如果是 true 变成 false,如果是 false 变成 true。
图标用 emoji:深色模式显示月亮 🌙,浅色模式显示太阳 ☀️。emoji 的好处是不需要引入图标库,跨平台都能显示。
fontSize: 24 让图标大一点,更容易点击。移动端的点击区域建议至少 44x44 像素,太小了手指不好点。
标题栏的样式
tsx
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 16,
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold'
},
headerSubtitle: {
fontSize: 14,
marginTop: 2
},
flexDirection: 'row'
让子元素横向排列。React Native 默认是纵向排列(column),这和 Web CSS 不同。
横向排列后,左侧的标题区域和右侧的按钮会并排显示。
justifyContent: 'space-between'
主轴方向(横向)的对齐方式。space-between 让子元素分散到两端,中间的空间平均分配。
效果是:标题区域靠左,按钮靠右,中间留白。这是标题栏最常见的布局方式。
如果用 flex-start,所有元素都靠左;用 flex-end,都靠右;用 center,都居中。space-between 正好满足我们"左右分布"的需求。
alignItems: 'center'
交叉轴方向(纵向)的对齐方式。center 让子元素垂直居中。
标题区域有两行文字,高度比按钮高。如果不设置 alignItems,默认是 stretch,按钮会被拉伸到和标题区域一样高。设置 center 后,按钮保持原有高度,垂直居中对齐。
paddingVertical: 16
上下各留 16 像素的内边距。这是 paddingTop: 16 和 paddingBottom: 16 的简写。
内边距让标题栏有呼吸感,不会显得拥挤。16 像素是一个常用的间距值,不大不小,刚刚好。
fontSize: 28 和 fontWeight: 'bold'
主标题用 28 像素的粗体字。这个字号在移动端算比较大的,能让用户一眼看到 App 名称。
fontWeight: 'bold' 让字体加粗。React Native 支持的字重值有 normal、bold,以及数字 100 到 900。bold 等于 700。
fontSize: 14 和 marginTop: 2
副标题用 14 像素的字,比主标题小一半。大小对比让层次更分明。
marginTop: 2 让副标题和主标题之间有一点间距,不会紧贴在一起。2 像素很小,但足够让两行文字在视觉上分开。
主题切换的实现
tsx
const {theme, darkMode, setDarkMode} = useTheme();
useTheme 是一个自定义 Hook,从 ThemeContext 获取主题相关的数据和方法。
theme 是当前主题的颜色配置,包含 text、subText、bg、card 等颜色值。
darkMode 是布尔值,表示当前是否是深色模式。
setDarkMode 是函数,用于切换主题。
这三个值配合使用:darkMode 决定显示哪个图标,setDarkMode 处理点击事件,theme 提供颜色值。
为什么用 Context
主题是全局状态,很多组件都需要用到。如果用 props 一层层传递,代码会很繁琐。Context 可以跨层级传递数据,任何组件都能直接获取。
ThemeContext 的实现大概是这样:
tsx
const ThemeContext = createContext();
export const ThemeProvider = ({children}) => {
const [darkMode, setDarkMode] = useState(false);
const theme = darkMode ? darkTheme : lightTheme;
return (
<ThemeContext.Provider value={{theme, darkMode, setDarkMode}}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
ThemeProvider 包裹整个 App,所有子组件都能通过 useTheme 获取主题数据。
切换主题的逻辑
tsx
onPress={() => setDarkMode(!darkMode)}
点击按钮时,把 darkMode 取反。如果当前是 false(浅色模式),变成 true(深色模式);反之亦然。
setDarkMode 更新状态后,React 会重新渲染使用了 theme 的组件。所有颜色都会自动更新,不需要手动处理。
这就是 React 的响应式:状态变化 → 组件重新渲染 → UI 更新。开发者只需要关心状态,UI 会自动同步。
动态副标题的实现
tsx
const TAB_TITLES = ['首页', '体系', '导航', '项目', '我的'];
const [activeTab, setActiveTab] = useState(0);
// 在标题栏中
<Text>技术资讯 · {TAB_TITLES[activeTab]}</Text>
TAB_TITLES 是一个常量数组,存储五个 Tab 的名称。
activeTab 是状态,存储当前激活的 Tab 索引(0-4)。
TAB_TITLES[activeTab] 根据索引获取对应的名称。比如 activeTab 是 2,TAB_TITLES[2] 就是"导航"。
为什么用数组而不是对象
也可以用对象:
tsx
const TAB_TITLES = {
home: '首页',
tree: '体系',
nav: '导航',
project: '项目',
mine: '我的'
};
但这样 activeTab 就要存字符串('home'、'tree' 等),比较时也要用字符串。数组用索引更简洁,而且索引可以直接用于其他数组(比如图标数组)。
状态更新时的联动
当用户点击底部 Tab 时,setActiveTab(index) 更新状态。状态更新后,标题栏会重新渲染,副标题自动显示新的页面名称。
这种联动是自动的,不需要手动同步。只要保证标题栏和 Tab 栏用的是同一个 activeTab 状态,它们就会保持一致。
状态栏的适配
tsx
<StatusBar
barStyle={darkMode ? 'light-content' : 'dark-content'}
backgroundColor={theme.bg}
/>
StatusBar 是 React Native 提供的组件,用于控制系统状态栏(显示时间、电量、信号的那一栏)。
barStyle
状态栏文字的颜色。light-content 是白色文字,适合深色背景;dark-content 是黑色文字,适合浅色背景。
我们根据 darkMode 动态设置:深色模式用白色文字,浅色模式用黑色文字。这样状态栏文字始终清晰可见。
backgroundColor
状态栏的背景色。设置成 theme.bg,和 App 背景色一致,看起来浑然一体。
注意:backgroundColor 只在 Android 上生效。iOS 的状态栏是透明的,会显示下面的内容。
为什么要适配状态栏
如果不设置,状态栏可能和 App 内容冲突。比如深色背景配黑色状态栏文字,就看不清时间和电量了。
状态栏虽然不是 App 的一部分,但它和 App 一起显示在屏幕上,视觉上要协调。
SafeAreaView 的作用
tsx
<SafeAreaView style={[styles.container, {backgroundColor: theme.bg}]}>
{/* 标题栏和其他内容 */}
</SafeAreaView>
SafeAreaView 确保内容不会被刘海、圆角、底部手势条等遮挡。
现在的手机屏幕形状各异:有刘海屏、水滴屏、挖孔屏,底部可能有虚拟按键或手势条。这些区域不能放重要内容,否则会被遮挡或误触。
SafeAreaView 会自动计算安全区域,在不安全的区域留出空白。我们的标题栏放在 SafeAreaView 里,就不用担心被刘海遮挡了。
和普通 View 的区别
普通 View 不会考虑安全区域,内容可能延伸到刘海下面。SafeAreaView 会自动加上 padding,把内容限制在安全区域内。
在没有刘海的设备上,SafeAreaView 和普通 View 表现一样,不会有额外的空白。
完整的标题栏代码
tsx
import React, {useState} from 'react';
import {
SafeAreaView,
StatusBar,
StyleSheet,
Text,
View,
TouchableOpacity,
} from 'react-native';
import {useTheme} from './src/context/ThemeContext';
const TAB_TITLES = ['首页', '体系', '导航', '项目', '我的'];
const MainApp = () => {
const {theme, darkMode, setDarkMode} = useTheme();
const [activeTab, setActiveTab] = useState(0);
return (
<SafeAreaView style={[styles.container, {backgroundColor: theme.bg}]}>
<StatusBar
barStyle={darkMode ? 'light-content' : 'dark-content'}
backgroundColor={theme.bg}
/>
{/* 顶部导航栏 */}
<View style={styles.header}>
<View>
<Text style={[styles.headerTitle, {color: theme.text}]}>
WanAndroid
</Text>
<Text style={[styles.headerSubtitle, {color: theme.subText}]}>
技术资讯 · {TAB_TITLES[activeTab]}
</Text>
</View>
<TouchableOpacity onPress={() => setDarkMode(!darkMode)}>
<Text style={{fontSize: 24}}>{darkMode ? '🌙' : '☀️'}</Text>
</TouchableOpacity>
</View>
{/* 页面内容 */}
{/* ... */}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 16,
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold'
},
headerSubtitle: {
fontSize: 14,
marginTop: 2
},
});
顶部标题栏虽然简单,但细节很多。字号、颜色、间距、对齐,每一个都影响用户体验。
好的标题栏让用户一眼就知道自己在哪,想去哪里也能快速找到入口。这就是设计的价值:让复杂的事情变得简单。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net