开篇:能跑还不够,还要好看

前七课,我们写了不少界面。但说实话,那些界面都只是"能跑"——堆叠的文字、简单的按钮、朴素的边框。真实的应用界面,需要精确的间距、合理的布局、在不同尺寸的屏幕上都能正常显示。

这一课要解决的问题是:

怎么用 React Native 的样式和布局系统,做出好看、好用、适配多种屏幕的界面?

在 React Native 中,样式不是 CSS,而是一个 JavaScript 对象。布局不用 float、不用 grid,而是用 Flexbox。尺寸不是像素,而是逻辑像素。

这一课,我们从 Flexbox 深入讲起,再讲到响应式设计和平台适配。


一、Flexbox 回顾与深入

1.1 主轴与交叉轴

Flexbox 的核心概念只有两个:主轴和交叉轴。

  • 主轴:由 flexDirection 决定的方向;
  • 交叉轴:与主轴垂直的方向。
// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row' }}>                             // 主轴水平
  {/* 主轴:水平方向,交叉轴:垂直方向 */}
</View>                                                             // 结束 View
// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'column' }}>                          // 主轴垂直
  {/* 主轴:垂直方向,交叉轴:水平方向 */}
</View>                                                             // 结束 View

React Native 默认 flexDirection 是 column。这和 Web CSS 的默认值 row 不同,是初学者最容易踩的坑之一。

1.2 justifyContent:主轴对齐

justifyContent 控制子元素在主轴上的排列方式:

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', justifyContent: 'flex-start' }}>...</View>   // 靠主轴起点
<View style={{ flexDirection: 'row', justifyContent: 'center' }}>...</View>       // 居中
<View style={{ flexDirection: 'row', justifyContent: 'flex-end' }}>...</View>     // 靠主轴终点
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>...</View> // 两端对齐,中间均匀
<View style={{ flexDirection: 'row', justifyContent: 'space-around' }}>...</View>  // 每个元素两侧间距相等
<View style={{ flexDirection: 'row', justifyContent: 'space-evenly' }}>...</View>  // 所有间距完全相等

1.3 alignItems:交叉轴对齐

alignItems 控制子元素在交叉轴上的排列方式:

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', alignItems: 'flex-start' }}>...</View>       // 靠交叉轴起点
<View style={{ flexDirection: 'row', alignItems: 'center' }}>...</View>           // 居中
<View style={{ flexDirection: 'row', alignItems: 'flex-end' }}>...</View>         // 靠交叉轴终点
<View style={{ flexDirection: 'row', alignItems: 'stretch' }}>...</View>          // 拉伸填满(默认值)

1.4 alignSelf:单个子元素的对齐

alignItems 作用于所有子元素。如果只想让某一个子元素有不同的交叉轴对齐方式,用 alignSelf:

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', alignItems: 'center', height: 100 }}>  // 父容器
  <View style={{ width: 50, height: 50, backgroundColor: 'red' }} />         // 默认居中
  <View style={{ width: 50, height: 50, backgroundColor: 'green', alignSelf: 'flex-start' }} />  // 单独靠顶部
  <View style={{ width: 50, height: 50, backgroundColor: 'blue' }} />        // 默认居中
</View>                                                             // 结束父容器

第二个方块用 alignSelf: 'flex-start' 单独靠顶部对齐。

1.5 flex 深入

flex 是 Flexbox 中分配空间的核心属性。它其实是三个属性的简写:

  • flexGrow:剩余空间的放大比例;
  • flexShrink:空间不足时的缩小比例;
  • flexBasis:初始大小。
// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', height: 100 }}>                // 父容器
  <View style={{ flex: 1, backgroundColor: 'red' }} />              // 占 1 份
  <View style={{ flex: 2, backgroundColor: 'green' }} />            // 占 2 份
  <View style={{ flex: 1, backgroundColor: 'blue' }} />             // 占 1 份
</View>                                                             // 结束父容器

总份数是 1 + 2 + 1 = 4。红色占 1/4,绿色占 2/4,蓝色占 1/4。

注意:React Native 的 flexShrink 默认是 0,而 Web CSS 默认是 1。这意味着在 React Native 中,空间不足时元素不会自动缩小,可能溢出。需要缩小的话,显式设置 flexShrink: 1。

flex: 1 是最常用的写法,意思是"占满剩余空间"。

1.6 flexWrap:换行

默认情况下,子元素会在一条线上排满。如果放不下,需要换行:

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>          // 允许换行
  {/* 子元素会自动换行 */}
</View>                                                             // 结束 View

flexWrap: 'wrap' 让子元素超出容器宽度时换到下一行。默认值是 nowrap。配合 gap 可以做标签云、卡片网格等布局。

1.7 gap:间距

gap 用于设置子元素之间的间距:

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', gap: 12 }}>                    // 子元素间距 12
  <View style={{ width: 50, height: 50, backgroundColor: 'red' }} />   // 方块 1
  <View style={{ width: 50, height: 50, backgroundColor: 'green' }} /> // 方块 2
  <View style={{ width: 50, height: 50, backgroundColor: 'blue' }} />  // 方块 3
</View>                                                             // 结束 View

gap 比手动给每个元素加 margin 更简洁。它还支持 rowGap 和 columnGap。

1.8 百分比尺寸

尺寸可以是百分比,相对于父容器的对应维度:

// JavaScript + React Native (JSX)
<View style={{ width: '100%', height: '50%', backgroundColor: '#eee' }} />  // 宽 100%,高 50%

百分比在响应式布局中非常有用。宽度用百分比,高度用固定值或 flex,是最常见的组合。

1.9 定位

React Native 支持 position: 'relative'(默认)和 position: 'absolute'。

// JavaScript + React Native (JSX)
<View style={{ width: 200, height: 200, backgroundColor: '#eee' }}> // 父容器
  <View                                                             // 绝对定位的子元素
    style={{
      position: 'absolute',                                         // 绝对定位
      top: 10,                                                      // 距顶部 10
      right: 10,                                                    // 距右侧 10
      width: 40,                                                    // 宽 40
      height: 40,                                                   // 高 40
      backgroundColor: 'red',                                       // 红色背景
      borderRadius: 20,                                             // 圆角 20,变成圆形
    }}
  />
</View>                                                             // 结束父容器

绝对定位的子元素相对于最近的定位祖先(默认是父容器)定位,不参与正常布局流。

常见用途:徽标(右上角的红点)、浮动按钮、覆盖层。

1.10 zIndex

zIndex 控制层叠顺序:

// JavaScript + React Native (JSX)
<View style={{ zIndex: 10 }} />                                     // 层叠顺序 10

数值大的在上面。在 React Native 中,同一父容器内的兄弟元素可以通过 zIndex 控制层叠。


二、尺寸与屏幕信息

2.1 逻辑像素

React Native 中的尺寸是无单位的逻辑像素,不是物理像素。

// JavaScript + React Native (JSX)
<View style={{ width: 100, height: 100 }} />                        // 宽高各 100 逻辑像素

在 iPhone 上,1 逻辑像素可能对应 2 或 3 物理像素。在高密度屏幕上,100 逻辑像素的方块看起来和低密度屏幕上一样大。这就是为什么不用写 px。

2.2 Dimensions

Dimensions 提供当前屏幕的宽高:

// JavaScript + React Native
import { Dimensions } from 'react-native';                          // 引入 Dimensions

const { width, height } = Dimensions.get('window');                 // 获取窗口宽高

window 是应用窗口尺寸,screen 是设备屏幕尺寸。大多数情况下用 window。

注意:Dimensions.get 返回的是某一时刻的值。屏幕旋转或分屏时,它不会自动更新。

2.3 useWindowDimensions

更好的方式是使用 useWindowDimensions Hook:

// JavaScript + React Native
import { useWindowDimensions } from 'react-native';                 // 引入 Hook

function App() {                                                    // 定义组件
  const { width, height } = useWindowDimensions();                  // 获取屏幕尺寸

  return (                                                          // 返回界面描述
    <View>                                                          // 外层容器
      <Text>宽度:{width}</Text>                                     // 显示宽度
      <Text>高度:{height}</Text>                                    // 显示高度
    </View>                                                         // 结束容器
  );                                                                // 结束 return
}                                                                   // 结束 App

它会随屏幕尺寸变化自动更新,触发组件重新渲染。这是响应式布局的首选。

2.4 PixelRatio

PixelRatio 提供逻辑像素和物理像素之间的换算:

// JavaScript + React Native
import { PixelRatio } from 'react-native';                          // 引入 PixelRatio

const pixelRatio = PixelRatio.get();                                // 获取像素密度
const physicalWidth = PixelRatio.getPixelSizeForLayoutSize(100);    // 逻辑像素转物理像素

大多数情况下不需要直接用它。React Native 会自动处理像素密度适配。它主要用于获取精确的物理尺寸,比如做 1 物理像素的细线。


三、响应式设计

3.1 为什么需要响应式

手机屏幕尺寸差异巨大:iPhone SE 只有 375 逻辑像素宽,iPad 有 768 甚至 1024。一个在 iPhone 上看起来正常的布局,在 iPad 上可能显得空旷,在小屏手机上可能挤成一团。

响应式设计的目标是:同一份代码,在不同屏幕上都有合理的显示效果。

3.2 百分比 + flex

最基础的响应式手段:

// JavaScript + React Native (JSX)
<View style={{ flex: 1, padding: 16 }}>                             // 占满父容器,内边距 16
  <View style={{ width: '100%', height: 200, backgroundColor: '#eee' }} />  // 宽度占满,高度固定
  <View style={{ flex: 1, flexDirection: 'row', gap: 12, marginTop: 12 }}>  // 横向排列,间距 12
    <View style={{ flex: 1, backgroundColor: 'red' }} />            // 左半部分
    <View style={{ flex: 1, backgroundColor: 'green' }} />          // 右半部分
  </View>                                                           // 结束横向容器
</View>                                                             // 结束外层容器

宽度用百分比或 flex,布局自动适应屏幕宽度。间距用固定值,保证视觉节奏一致。

3.3 useWindowDimensions 做断点

根据屏幕宽度调整布局:

// JavaScript + React Native
import { useWindowDimensions } from 'react-native';                 // 引入 Hook

function App() {                                                    // 定义组件
  const { width } = useWindowDimensions();                          // 获取屏幕宽度
  const isTablet = width >= 768;                                    // 是否平板

  return (                                                          // 返回界面描述
    <View style={{ flexDirection: isTablet ? 'row' : 'column' }}>   // 平板横排,手机纵排
      {/* 子元素 */}
    </View>                                                         // 结束容器
  );                                                                // 结束 return
}                                                                   // 结束 App

这是最常用的断点策略:手机竖屏、手机横屏、平板。

3.4 条件样式

根据屏幕尺寸计算样式:

// JavaScript + React Native
const { width } = useWindowDimensions();                            // 获取屏幕宽度
const cardWidth = width > 600 ? (width - 48) / 3 : (width - 32) / 2; // 平板三列,手机两列

<View style={{ width: cardWidth, height: cardWidth, backgroundColor: '#eee' }} />  // 卡片

3.5 字体响应式

字体大小也可以根据屏幕调整:

// JavaScript + React Native
const { width } = useWindowDimensions();                            // 获取屏幕宽度
const baseFontSize = width > 600 ? 20 : 16;                         // 平板字号大,手机字号小

<Text style={{ fontSize: baseFontSize }}>标题</Text>                 // 应用字号

但字体不要缩得太小或太大。通常只在平板和手机之间做一个粗略的区分。

3.6 一个响应式卡片网格

// JavaScript + React Native
import { useWindowDimensions, View, Text, FlatList, StyleSheet } from 'react-native';  // 引入组件

const data = [                                                      // 数据
  { id: '1', title: '卡片一' },                                     // 第一张
  { id: '2', title: '卡片二' },                                     // 第二张
  { id: '3', title: '卡片三' },                                     // 第三张
  { id: '4', title: '卡片四' },                                     // 第四张
];                                                                  // 结束数据

export default function App() {                                     // 定义并默认导出 App
  const { width } = useWindowDimensions();                          // 获取屏幕宽度
  const numColumns = width > 600 ? 3 : 2;                           // 平板三列,手机两列
  const gap = 12;                                                   // 卡片间距
  const padding = 16;                                               // 容器内边距
  const itemWidth = (width - padding * 2 - gap * (numColumns - 1)) / numColumns;  // 卡片宽度

  return (                                                          // 返回界面描述
    <FlatList                                                       // 高效列表
      data={data}                                                   // 数据源
      key={numColumns}                                              // 列数变化时重新渲染
      numColumns={numColumns}                                       // 列数
      keyExtractor={(item) => item.id}                              // 从每项提取唯一 key
      contentContainerStyle={{ padding }}                           // 内容内边距
      columnWrapperStyle={{ gap }}                                  // 列间距
      renderItem={({ item }) => (                                   // 如何渲染每一项
        <View style={[styles.card, { width: itemWidth, height: itemWidth }]}>  // 卡片
          <Text>{item.title}</Text>                                 // 显示标题
        </View>                                                     // 结束卡片
      )}                                                            // 结束 renderItem
      ItemSeparatorComponent={() => <View style={{ height: gap }} />}  // 行间距
    />                                                              // 结束 FlatList
  );                                                                // 结束 return
}                                                                   // 结束 App

const styles = StyleSheet.create({                                  // 创建样式表
  card: {                                                           // 卡片样式
    backgroundColor: '#eee',                                        // 背景色
    borderRadius: 8,                                                // 圆角
    justifyContent: 'center',                                       // 垂直居中
    alignItems: 'center',                                           // 水平居中
  },                                                                // 结束卡片样式
});                                                                 // 结束样式表

numColumns 根据屏幕宽度变化。切换列数时,需要给 FlatList 加 key={numColumns},强制重新渲染。


四、平台适配

4.1 Platform.OS

iOS 和 Android 在很多地方有差异:状态栏高度、阴影效果、触摸反馈、默认字体。React Native 提供了 Platform 模块来区分平台:

// JavaScript + React Native
import { Platform } from 'react-native';                            // 引入 Platform

if (Platform.OS === 'ios') {                                        // iOS 特有逻辑
  // ...
} else if (Platform.OS === 'android') {                             // Android 特有逻辑
  // ...
}                                                                   // 结束判断

Platform.OS 的值是 'ios'、'android',在 Web 上是 'web'。

4.2 Platform.select

根据平台选择不同值:

// JavaScript + React Native
import { Platform, StyleSheet } from 'react-native';                // 引入组件

const styles = StyleSheet.create({                                  // 创建样式表
  container: {                                                      // 容器样式
    paddingTop: Platform.select({                                   // 根据平台选择值
      ios: 44,                                                      // iOS 用 44
      android: 24,                                                  // Android 用 24
      default: 0,                                                   // 其他用 0
    }),                                                             // 结束 select
  },                                                                // 结束容器样式
});                                                                 // 结束样式表

Platform.select 接收一个对象,返回当前平台对应的值。没有匹配时返回 default 的值。

4.3 平台特定样式

常见的平台差异:

阴影:

// JavaScript + React Native
const shadow = Platform.select({                                    // 根据平台选择阴影样式
  ios: {                                                            // iOS 用 shadow 属性
    shadowColor: '#000',                                            // 阴影颜色
    shadowOffset: { width: 0, height: 2 },                          // 阴影偏移
    shadowOpacity: 0.1,                                             // 阴影透明度
    shadowRadius: 4,                                                // 阴影模糊半径
  },                                                                // 结束 iOS
  android: {                                                        // Android 用 elevation
    elevation: 4,                                                   // 海拔高度
  },                                                                // 结束 Android
});                                                                 // 结束 select

iOS 用 shadow* 属性,Android 用 elevation。

触摸反馈:

// JavaScript + React Native (JSX)
<Pressable                                                          // 可点击容器
  android_ripple={{ color: '#ccc' }}                                // Android 水波纹效果
  style={({ pressed }) => [                                         // 根据按下状态选择样式
    { opacity: pressed && Platform.OS === 'ios' ? 0.6 : 1 },        // iOS 用透明度变化
  ]}                                                                // 结束样式
>                                                                   // 结束开始标签
  <Text>点击我</Text>                                                // 文本
</Pressable>                                                        // 结束 Pressable

Android 用 android_ripple 实现水波纹效果,iOS 用透明度变化模拟按下效果。

状态栏高度:

// JavaScript + React Native
import { StatusBar, Platform } from 'react-native';                 // 引入组件

const statusBarHeight = Platform.OS === 'android' ? StatusBar.currentHeight : 0;  // Android 有状态栏高度,iOS 为 0

4.4 平台特定文件

如果一个组件在 iOS 和 Android 上差异很大,可以用平台特定文件后缀:

// 文件结构
MyComponent.ios.js                                                  // iOS 版本
MyComponent.android.js                                              // Android 版本

导入时只需要:

// JavaScript + React Native
import MyComponent from './MyComponent';                            // 自动选择对应平台的文件

React Native 会根据平台自动选择对应文件。这种方式适合平台差异非常大的场景。

4.5 哪些情况需要平台适配

  • 阴影:iOS 用 shadow*,Android 用 elevation;
  • 状态栏:Android 有 StatusBar.currentHeight,iOS 没有;
  • 字体:不同平台默认字体不同;
  • 触摸反馈:iOS 和 Android 的视觉反馈习惯不同;
  • 键盘:Android 键盘行为与 iOS 不同;
  • 权限:权限请求方式不同。

大多数情况下,Platform.select 就够了。只有差异特别大时,才用平台特定文件。


五、安全区域

5.1 什么是安全区域

现代手机有刘海、圆角、底部指示条。这些区域不应该放重要内容,否则会被遮挡。这些"不被遮挡"的区域叫安全区域。

5.2 react-native-safe-area-context

这是社区推荐的安全区域库,跨平台支持:

# Bash 终端命令
npx expo install react-native-safe-area-context                    # 安装安全区域库

用法:

// JavaScript + React Native
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';  // 引入组件

export default function App() {                                     // 定义并默认导出 App
  return (                                                          // 返回界面描述
    <SafeAreaProvider>                                              // 安全区域提供者
      <SafeAreaView style={{ flex: 1 }}>                            // 安全区域容器
        {/* 内容 */}
      </SafeAreaView>                                               // 结束安全区域容器
    </SafeAreaProvider>                                             // 结束提供者
  );                                                                // 结束 return
}                                                                   // 结束 App

SafeAreaProvider 放在最外层,SafeAreaView 包裹需要避开安全区域的内容。

也可以使用 useSafeAreaInsets 获取具体的安全区域内边距:

// JavaScript + React Native
import { useSafeAreaInsets } from 'react-native-safe-area-context'; // 引入 Hook

function App() {                                                    // 定义组件
  const insets = useSafeAreaInsets();                               // 获取安全区域内边距

  return (                                                          // 返回界面描述
    <View style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}>  // 根据安全区域设置内边距
      {/* 内容 */}
    </View>                                                         // 结束容器
  );                                                                // 结束 return
}                                                                   // 结束 App

这在自定义头部或底部栏时特别有用。

5.3 与导航器配合

使用 React Navigation 时,导航器已经处理了安全区域。大多数情况下不需要再手动包 SafeAreaView。只有自定义头部或底部栏时,才需要用到 useSafeAreaInsets。


六、完整示例:响应式个人主页

把这一课的知识点组合起来,做一个响应式个人主页。

// JavaScript + React Native
import { View, Text, ScrollView, useWindowDimensions, StyleSheet } from 'react-native';  // 引入组件
import { SafeAreaView } from 'react-native-safe-area-context';      // 引入安全区域

const skills = ['React Native', 'JavaScript', 'TypeScript', 'Node.js', 'GraphQL'];  // 技能列表

export default function App() {                                     // 定义并默认导出 App
  const { width } = useWindowDimensions();                          // 获取屏幕宽度
  const isTablet = width >= 768;                                    // 是否平板

  return (                                                          // 返回界面描述
    <SafeAreaView style={styles.safe}>                              // 安全区域容器
      <ScrollView contentContainerStyle={styles.container}>         // 可滚动容器
        <View style={[styles.header, isTablet && styles.headerTablet]}>  // 头部,平板上横向
          <View style={[styles.avatar, isTablet && styles.avatarTablet]} />  // 头像
          <View style={styles.info}>                                // 信息区
            <Text style={[styles.name, isTablet && styles.nameTablet]}>小明</Text>  // 姓名
            <Text style={styles.bio}>热爱编程,正在学习 React Native</Text>  // 简介
          </View>                                                   // 结束信息区
        </View>                                                     // 结束头部

        <View style={styles.section}>                               // 技能区块
          <Text style={styles.sectionTitle}>技能</Text>             // 区块标题
          <View style={styles.skills}>                              // 技能容器
            {skills.map((skill) => (                                // 遍历技能
              <View key={skill} style={styles.tag}>                 // 每个技能标签
                <Text style={styles.tagText}>{skill}</Text>         // 标签文字
              </View>                                               // 结束标签
            ))}                                                     // 结束 map
          </View>                                                   // 结束技能容器
        </View>                                                     // 结束技能区块

        <View style={styles.section}>                               // 简介区块
          <Text style={styles.sectionTitle}>简介</Text>             // 区块标题
          <Text style={styles.paragraph}>                           // 简介正文
            这是一段用于演示响应式布局的文字。在手机上,布局是上下排列;
            在平板上,头部会变成横向布局,头像更大,字体也更大。
          </Text>                                                   // 结束正文
        </View>                                                     // 结束简介区块
      </ScrollView>                                                 // 结束滚动容器
    </SafeAreaView>                                                 // 结束安全区域容器
  );                                                                // 结束 return
}                                                                   // 结束 App

const styles = StyleSheet.create({                                  // 创建样式表
  safe: { flex: 1, backgroundColor: '#fff' },                       // 安全区域容器样式
  container: { padding: 16, paddingBottom: 40 },                    // 内容容器样式
  header: { flexDirection: 'column', alignItems: 'center', marginBottom: 24 },  // 头部(手机)
  headerTablet: { flexDirection: 'row', alignItems: 'center' },     // 头部(平板)
  avatar: {                                                         // 头像(手机)
    width: 80,                                                      // 宽 80
    height: 80,                                                     // 高 80
    borderRadius: 40,                                               // 圆角 40,变成圆形
    backgroundColor: '#4A90D9',                                     // 蓝色背景
    marginBottom: 12,                                               // 底部间距 12
  },                                                                // 结束头像
  avatarTablet: {                                                   // 头像(平板)
    width: 120,                                                     // 宽 120
    height: 120,                                                    // 高 120
    borderRadius: 60,                                               // 圆角 60
    marginBottom: 0,                                                // 无底部间距
    marginRight: 24,                                                // 右侧间距 24
  },                                                                // 结束头像(平板)
  info: { alignItems: 'center' },                                   // 信息区居中
  name: { fontSize: 22, fontWeight: 'bold', marginBottom: 4 },      // 姓名(手机)
  nameTablet: { fontSize: 28 },                                     // 姓名(平板)
  bio: { fontSize: 14, color: '#666' },                             // 简介
  section: { marginBottom: 24 },                                    // 区块间距
  sectionTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 12 },  // 区块标题
  skills: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },       // 技能容器
  tag: {                                                            // 标签
    paddingHorizontal: 12,                                          // 水平内边距 12
    paddingVertical: 6,                                             // 垂直内边距 6
    backgroundColor: '#eef3fb',                                     // 浅蓝背景
    borderRadius: 16,                                               // 圆角 16
  },                                                                // 结束标签
  tagText: { color: '#4A90D9', fontSize: 14 },                      // 标签文字
  paragraph: { fontSize: 15, lineHeight: 24, color: '#444' },       // 段落文字
});                                                                 // 结束样式表

这个例子综合运用了:SafeAreaView 避开安全区域;useWindowDimensions 获取屏幕宽度;断点判断 isTablet;条件样式切换布局方向;flexWrap + gap 做标签云;flexDirection: 'column' 到 'row' 的响应式切换。


七、样式与"视图 = 函数(状态)"

样式也是"视图 = 函数(状态)"的一部分。屏幕尺寸是状态,样式是输出:

// 概念表达
样式 = 函数(屏幕尺寸, 平台)                                          // 样式由屏幕尺寸和平台决定
  • 屏幕宽度变化,isTablet 变化,布局样式重新计算;
  • 平台不同,Platform.select 返回不同值;
  • 状态变,样式变,视图更新。

React Native 没有 CSS 的媒体查询,但 useWindowDimensions + 条件样式就是 React Native 的响应式方案。它依然是声明式的:你描述"在什么尺寸下用什么样式",框架负责在尺寸变化时更新。


八、练习题

练习题1:选择题——React Native 默认的 flexDirection

题目:React Native 中 View 的 flexDirection 默认值是什么?

A. row
B. column
C. row-reverse
D. column-reverse

参考答案:B

解读:React Native 默认 flexDirection 是 column,与 Web CSS 默认的 row 不同。这是初学者容易踩的坑。


练习题2:填空题——主轴对齐

题目:补全代码,让子元素在主轴两端对齐,中间均匀分布。

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', justifyContent: '____' }}>    // 补全属性值

参考答案:space-between

解读:space-between 让第一个元素靠起点,最后一个靠终点,中间均匀分布。其他选项还有 space-around 和 space-evenly。


练习题3:改错题——flexShrink 默认值

题目:下面代码想让三个方块平分宽度,但红色方块溢出了。请指出问题。

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row' }}>                             // 横向排列
  <View style={{ width: 200, height: 50, backgroundColor: 'red' }} />  // 固定宽度 200
  <View style={{ flex: 1, height: 50, backgroundColor: 'green' }} />   // 占 1 份
  <View style={{ flex: 1, height: 50, backgroundColor: 'blue' }} />    // 占 1 份
</View>                                                             // 结束容器

参考答案:React Native 中 flexShrink 默认是 0,宽度超出时不会自动缩小。可以给红色方块加 flexShrink: 1,或者把宽度改成 flex: 1。

解读:React Native 的 flexShrink 默认值与 Web CSS 不同。空间不足时,需要显式设置 flexShrink 才会缩小。


练习题4:代码阅读题——flex 分配

题目:阅读代码,说出三个方块的宽度比例。

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', width: 400 }}>                 // 宽 400 的容器
  <View style={{ flex: 1, backgroundColor: 'red' }} />              // 占 1 份
  <View style={{ flex: 3, backgroundColor: 'green' }} />            // 占 3 份
</View>                                                             // 结束容器

参考答案:红色占 1/4(100),绿色占 3/4(300)。

解读:flex 值决定剩余空间的分配比例。总份数是 1 + 3 = 4。


练习题5:动手题——居中布局

题目:写一个 View 的样式,使其子元素在水平和垂直方向都居中。

参考答案:

// JavaScript + React Native
{
  flex: 1,                                                          // 占满父容器
  justifyContent: 'center',                                         // 主轴居中
  alignItems: 'center',                                             // 交叉轴居中
}

解读:默认 flexDirection 是 column,主轴是垂直方向,交叉轴是水平方向。justifyContent: 'center' 让子元素在主轴(垂直)居中,alignItems: 'center' 让子元素在交叉轴(水平)居中。


练习题6:简答题——gap 与 margin 的区别

题目:用 gap 设置子元素间距,和给每个子元素加 margin 相比,有什么好处?

参考答案:gap 只作用于子元素之间,不会在第一个元素前面或最后一个元素后面添加多余间距。margin 需要给每个元素设置,容易出现首尾多余间距,还要考虑相邻元素 margin 叠加的问题。

解读:gap 更简洁、更可预测,是现代 Flexbox 布局推荐的方式。


练习题7:百分比尺寸题

题目:写一个 View,宽度占父容器的 80%,高度为 200。

参考答案:

// JavaScript + React Native (JSX)
<View style={{ width: '80%', height: 200, backgroundColor: '#eee' }} />  // 宽 80%,高 200

解读:百分比相对于父容器的对应维度。宽度用百分比,高度用固定值,是响应式布局的常见组合。


练习题8:绝对定位题

题目:在父容器右上角放一个 20x20 的红色圆点。

参考答案:

// JavaScript + React Native (JSX)
<View style={{ width: 100, height: 100, backgroundColor: '#eee' }}>  // 父容器
  <View                                                              // 红色圆点
    style={{
      position: 'absolute',                                          // 绝对定位
      top: 4,                                                        // 距顶部 4
      right: 4,                                                      // 距右侧 4
      width: 20,                                                     // 宽 20
      height: 20,                                                    // 高 20
      borderRadius: 10,                                              // 圆角 10,变成圆形
      backgroundColor: 'red',                                        // 红色背景
    }}
  />
</View>                                                              // 结束父容器

解读:position: 'absolute' 让子元素脱离正常布局流,相对于父容器定位。


练习题9:useWindowDimensions 题

题目:用 useWindowDimensions 获取屏幕宽度,并根据宽度判断是否是平板(宽度大于等于 768)。

参考答案:

// JavaScript + React Native
import { useWindowDimensions } from 'react-native';                 // 引入 Hook

const { width } = useWindowDimensions();                            // 获取屏幕宽度
const isTablet = width >= 768;                                      // 是否平板

解读:useWindowDimensions 会随屏幕尺寸变化自动更新,是响应式布局的首选。


练习题10:条件样式题

题目:根据 isTablet 切换 flexDirection,平板上是 row,手机上是 column。

参考答案:

// JavaScript + React Native (JSX)
<View style={{ flexDirection: isTablet ? 'row' : 'column' }}>      // 根据是否平板切换方向

解读:条件样式是 React Native 响应式布局的核心手段。根据屏幕尺寸计算样式值,状态变,样式变。


练习题11:Platform.select 题

题目:用 Platform.select 给容器设置不同的 paddingTop:iOS 是 44,Android 是 24,其他是 0。

参考答案:

// JavaScript + React Native
import { Platform } from 'react-native';                            // 引入 Platform

const styles = {                                                    // 样式对象
  container: {                                                      // 容器样式
    paddingTop: Platform.select({                                   // 根据平台选择值
      ios: 44,                                                      // iOS 用 44
      android: 24,                                                  // Android 用 24
      default: 0,                                                   // 其他用 0
    }),                                                             // 结束 select
  },                                                                // 结束容器样式
};                                                                  // 结束样式

解读:Platform.select 接收一个对象,返回当前平台对应的值。适合处理平台差异较小的样式。


练习题12:平台阴影题

题目:写一个跨平台的阴影样式,iOS 用 shadow* 属性,Android 用 elevation。

参考答案:

// JavaScript + React Native
import { Platform } from 'react-native';                            // 引入 Platform

const shadow = Platform.select({                                    // 根据平台选择阴影样式
  ios: {                                                            // iOS 用 shadow 属性
    shadowColor: '#000',                                            // 阴影颜色
    shadowOffset: { width: 0, height: 2 },                          // 阴影偏移
    shadowOpacity: 0.1,                                             // 阴影透明度
    shadowRadius: 4,                                                // 阴影模糊半径
  },                                                                // 结束 iOS
  android: {                                                        // Android 用 elevation
    elevation: 4,                                                   // 海拔高度
  },                                                                // 结束 Android
});                                                                 // 结束 select

解读:iOS 和 Android 的阴影实现机制不同。Platform.select 让同一个样式对象在不同平台使用不同的属性。


练习题13:flexWrap 题

题目:写一个标签云布局,标签横向排列,放不下时自动换行,标签之间间距 8。

参考答案:

// JavaScript + React Native (JSX)
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>  // 换行 + 间距 8
  {tags.map((tag) => (                                              // 遍历标签
    <View key={tag} style={{ paddingHorizontal: 12, paddingVertical: 6, backgroundColor: '#eef3fb', borderRadius: 16 }}>  // 标签样式
      <Text>{tag}</Text>                                            // 标签文字
    </View>                                                         // 结束标签
  ))}                                                               // 结束 map
</View>                                                             // 结束容器

解读:flexWrap: 'wrap' 让子元素超出容器宽度时换行,gap: 8 设置间距。这是标签云、技能列表的常见写法。


练习题14:综合响应式题

题目:创建一个卡片网格,手机上两列,平板上三列,卡片之间有间距。

参考答案:

// JavaScript + React Native
import { View, Text, FlatList, useWindowDimensions } from 'react-native';  // 引入组件

export default function App() {                                     // 定义并默认导出 App
  const { width } = useWindowDimensions();                          // 获取屏幕宽度
  const numColumns = width > 600 ? 3 : 2;                           // 平板三列,手机两列
  const gap = 12;                                                   // 卡片间距
  const padding = 16;                                               // 容器内边距
  const itemWidth = (width - padding * 2 - gap * (numColumns - 1)) / numColumns;  // 卡片宽度

  return (                                                          // 返回界面描述
    <FlatList                                                       // 高效列表
      data={data}                                                   // 数据源
      key={numColumns}                                              // 列数变化时重新渲染
      numColumns={numColumns}                                       // 列数
      keyExtractor={(item) => item.id}                              // 从每项提取唯一 key
      contentContainerStyle={{ padding }}                           // 内容内边距
      columnWrapperStyle={{ gap }}                                  // 列间距
      renderItem={({ item }) => (                                   // 如何渲染每一项
        <View style={{ width: itemWidth, height: itemWidth, backgroundColor: '#eee' }}>  // 卡片
          <Text>{item.title}</Text>                                 // 显示标题
        </View>                                                     // 结束卡片
      )}                                                            // 结束 renderItem
      ItemSeparatorComponent={() => <View style={{ height: gap }} />}  // 行间距
    />                                                              // 结束 FlatList
  );                                                                // 结束 return
}                                                                   // 结束 App

解读:numColumns 根据屏幕宽度变化。key={numColumns} 强制列表在列数变化时重新渲染。itemWidth 根据列数、间距、内边距计算。


练习题15:综合应用题——响应式商品详情页

题目:创建一个响应式商品详情页:

  1. 手机上是上下布局:图片在上,信息在下;
  2. 平板上是左右布局:图片在左,信息在右;
  3. 使用安全区域;
  4. 图片宽度占容器的 100%(手机)或 50%(平板)。

参考答案:

// JavaScript + React Native
import { View, Text, ScrollView, useWindowDimensions, StyleSheet } from 'react-native';  // 引入组件
import { SafeAreaView } from 'react-native-safe-area-context';      // 引入安全区域

export default function App() {                                     // 定义并默认导出 App
  const { width } = useWindowDimensions();                          // 获取屏幕宽度
  const isTablet = width >= 768;                                    // 是否平板

  return (                                                          // 返回界面描述
    <SafeAreaView style={styles.safe}>                              // 安全区域容器
      <ScrollView contentContainerStyle={styles.container}>         // 可滚动容器
        <View style={[styles.content, { flexDirection: isTablet ? 'row' : 'column' }]}>  // 响应式布局方向
          <View style={[styles.image, { width: isTablet ? '50%' : '100%' }]} />  // 图片区域
          <View style={[styles.info, { width: isTablet ? '50%' : '100%' }]}>     // 信息区域
            <Text style={styles.title}>商品名称</Text>              // 商品名称
            <Text style={styles.price}>¥ 199</Text>                 // 商品价格
            <Text style={styles.desc}>                              // 商品描述
              这是一段商品描述。在手机上,图片和信息是上下排列;
              在平板上,它们变成左右排列,图片占左侧一半。
            </Text>                                                 // 结束描述
          </View>                                                   // 结束信息区
        </View>                                                     // 结束内容区
      </ScrollView>                                                 // 结束滚动容器
    </SafeAreaView>                                                 // 结束安全区域容器
  );                                                                // 结束 return
}                                                                   // 结束 App

const styles = StyleSheet.create({                                  // 创建样式表
  safe: { flex: 1, backgroundColor: '#fff' },                       // 安全区域样式
  container: { padding: 16 },                                       // 内容容器样式
  content: { gap: 16 },                                             // 内容区,间距 16
  image: {                                                          // 图片区域
    aspectRatio: 1,                                                 // 宽高比 1:1
    backgroundColor: '#eee',                                        // 灰色背景
    borderRadius: 8,                                                // 圆角 8
  },                                                                // 结束图片区域
  info: { justifyContent: 'center' },                               // 信息区垂直居中
  title: { fontSize: 22, fontWeight: 'bold', marginBottom: 8 },     // 标题样式
  price: { fontSize: 20, color: '#d00', marginBottom: 12 },         // 价格样式
  desc: { fontSize: 15, lineHeight: 24, color: '#444' },            // 描述样式
});                                                                 // 结束样式表

解读:这道题综合运用了响应式断点、条件样式、flexDirection 切换、百分比宽度、SafeAreaView 安全区域、aspectRatio 宽高比等知识点。手机和平板使用同一份代码,通过 isTablet 状态切换布局方向。这正是"样式 = 函数(屏幕尺寸)"的体现。


九、总结

这一课我们学习了 React Native 样式与布局的进阶知识:

  1. Flexbox 的核心是主轴和交叉轴,flexDirection 决定主轴方向;
  2. justifyContent 控制主轴对齐,alignItems 控制交叉轴对齐,alignSelf 控制单个子元素;
  3. flex 是 flexGrow、flexShrink、flexBasis 的简写,React Native 的 flexShrink 默认是 0;
  4. flexWrap 控制换行,gap 设置子元素间距;
  5. 尺寸是无单位的逻辑像素,宽度常用百分比,高度常用固定值或 flex;
  6. position: 'absolute' 用于浮动元素,zIndex 控制层叠;
  7. useWindowDimensions 是响应式布局的首选,它随屏幕变化自动更新;
  8. 响应式的核心手段:百分比、flex、断点判断、条件样式;
  9. Platform.OS 和 Platform.select 处理平台差异;
  10. iOS 和 Android 的阴影、触摸反馈、状态栏处理方式不同;
  11. SafeAreaView 避开刘海和底部指示条,推荐使用 react-native-safe-area-context。

记住一句话:

样式 = 函数(屏幕尺寸, 平台),状态变,样式变,视图更新。


第9课预告

下一课,我们进入 【React Native娓娓道来】第9课 动画与手势:Animated、Reanimated 与 Pressable 进阶。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐