顶部标题栏的设计与实现:让用户知道自己在哪

案例项目开源地址: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: 16paddingBottom: 16 的简写。

内边距让标题栏有呼吸感,不会显得拥挤。16 像素是一个常用的间距值,不大不小,刚刚好。

fontSize: 28 和 fontWeight: 'bold'

主标题用 28 像素的粗体字。这个字号在移动端算比较大的,能让用户一眼看到 App 名称。

fontWeight: 'bold' 让字体加粗。React Native 支持的字重值有 normalbold,以及数字 100900bold 等于 700

fontSize: 14 和 marginTop: 2

副标题用 14 像素的字,比主标题小一半。大小对比让层次更分明。

marginTop: 2 让副标题和主标题之间有一点间距,不会紧贴在一起。2 像素很小,但足够让两行文字在视觉上分开。

主题切换的实现

tsx 复制代码
const {theme, darkMode, setDarkMode} = useTheme();

useTheme 是一个自定义 Hook,从 ThemeContext 获取主题相关的数据和方法。

theme 是当前主题的颜色配置,包含 textsubTextbgcard 等颜色值。

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

相关推荐
Van_Moonlight10 小时前
RN for OpenHarmony 实战 TodoList 项目:空状态占位图
javascript·开源·harmonyos
kaizq10 小时前
AI-MCP-SQLite-SSE本地服务及CherryStudio便捷应用
python·sqlite·llm·sse·mcp·cherry studio·fastmcp
程序员小假10 小时前
我们来说一下无锁队列 Disruptor 的原理
java·后端
资生算法程序员_畅想家_剑魔11 小时前
Kotlin常见技术分享-02-相对于Java 的核心优势-协程
java·开发语言·kotlin
ProgramHan11 小时前
Spring Boot 3.2 新特性:虚拟线程的落地实践
java·jvm·spring boot
nbsaas-boot11 小时前
Go vs Java 的三阶段切换路线图
java·开发语言·golang
anyup11 小时前
2026第一站:分享我在高德大赛现场学到的技术、产品与心得
前端·架构·harmonyos
!chen12 小时前
Error: error:0308010C:digital envelope routines::unsupporte
python
毕设源码-钟学长12 小时前
【开题答辩全过程】以 基于Java的慕课点评网站为例,包含答辩的问题和答案
java·开发语言