【React Native娓娓道来】第8课 样式与布局进阶:Flexbox 深入、响应式设计与平台适配
开篇:能跑还不够,还要好看
前七课,我们写了不少界面。但说实话,那些界面都只是"能跑"——堆叠的文字、简单的按钮、朴素的边框。真实的应用界面,需要精确的间距、合理的布局、在不同尺寸的屏幕上都能正常显示。
这一课要解决的问题是:
怎么用 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:综合应用题——响应式商品详情页
题目:创建一个响应式商品详情页:
- 手机上是上下布局:图片在上,信息在下;
- 平板上是左右布局:图片在左,信息在右;
- 使用安全区域;
- 图片宽度占容器的 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 样式与布局的进阶知识:
- Flexbox 的核心是主轴和交叉轴,
flexDirection决定主轴方向; justifyContent控制主轴对齐,alignItems控制交叉轴对齐,alignSelf控制单个子元素;flex是flexGrow、flexShrink、flexBasis的简写,React Native 的flexShrink默认是 0;flexWrap控制换行,gap设置子元素间距;- 尺寸是无单位的逻辑像素,宽度常用百分比,高度常用固定值或 flex;
position: 'absolute'用于浮动元素,zIndex控制层叠;useWindowDimensions是响应式布局的首选,它随屏幕变化自动更新;- 响应式的核心手段:百分比、flex、断点判断、条件样式;
Platform.OS和Platform.select处理平台差异;- iOS 和 Android 的阴影、触摸反馈、状态栏处理方式不同;
SafeAreaView避开刘海和底部指示条,推荐使用react-native-safe-area-context。
记住一句话:
样式 = 函数(屏幕尺寸, 平台),状态变,样式变,视图更新。
第9课预告
下一课,我们进入 【React Native娓娓道来】第9课 动画与手势:Animated、Reanimated 与 Pressable 进阶。
更多推荐

所有评论(0)