核心组件
React 你会写了,但 RN 里没有 <div>、<span>、<p>。浏览器 DOM 那套内置元素在这里全部不存在,取而代之的是一小组平台原生组件:View、Text、Image、ScrollView……数量少到可以一口气看完。
这一章按"干什么、关键 props、生产写法、坑"四个角度过一遍这些内置组件。先建立一个预期:内置组件只是地板。RN 官方刻意把核心库做小,视频、地图、图表、富文本、WebView、超长列表这些业务能力全靠社区包。写业务 App 时你的真实组件清单大概率是:内置五件套 + 三五个社区包,而不是把内置组件用出花来。
本手册基线是 RN 0.88 / Expo SDK 58 / TypeScript,架构背景见 01-核心概念与架构.md。
组件全景速览
| 分类 | 组件 | 一句话 |
|---|---|---|
| 布局 | View |
唯一的容器,相当于 div |
| 文本 | Text |
所有文字的载体,可嵌套 |
| 滚动 | ScrollView |
内容全量渲染的滚动容器,适合"页面"不适合"列表" |
| 滚动 | FlatList / SectionList |
虚拟化列表,只渲染可视区附近的内容 |
| 媒体 | Image |
本地图 / 网络图 / data URI 三种来源 |
| 交互 | Pressable |
一切可点元素的统一答案,取代 Touchable* 系列 |
| 交互 | TextInput |
单行/多行输入(完整表单见 06-表单与交互.md) |
| 交互 | Switch |
开关 |
| 浮层 | Modal |
全屏/覆盖层弹窗 |
| 反馈 | ActivityIndicator |
转圈加载 |
| 反馈 | RefreshControl |
下拉刷新,挂在列表组件上 |
| 其他 | SafeAreaView(内置) |
已被官方标注弃用,见下文,用社区包替代 |
| 其他 | KeyboardAvoidingView |
键盘避让,iOS 侧常用 |
注意:这张表里没有 Video、地图、图表、WebView。内置组件到此为止,缺的全是社区包的活。选型顺序永远是:内置够用 → 社区成熟包 → 自己封装原生,别一上来就自绘。
View:一切的容器
View 映射到 iOS 的 UIView、Android 的 ViewGroup。它没有默认外观——不设背景色时完全透明,宽高由内容或 flex 规则决定,且 flexDirection 默认是 column(这是 Web 开发者最大的肌肉记忆杀手,详见 03-样式与布局.md)。
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
export default function App() {
return (
<View style={styles.page}>
<View style={styles.card}>
<Text>View 只能装组件,不能直接装裸字符串</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
page: { flex: 1, backgroundColor: '#F2F3F5' },
card: { margin: 16, padding: 16, borderRadius: 12, backgroundColor: '#fff' },
});pointerEvents:穿透还是拦截
弹层、蒙层、悬浮按钮经常需要"有的区域接手势,有的区域放行"。pointerEvents 有四个值:auto 默认;none 自己和整棵子树都不响应、手势穿透到下层;box-only 只有自己的框响应;box-none 反过来——自己的框不响应、子节点照常响应。
典型场景:一个铺满全屏的悬浮操作层,只有右下角的按钮可点,其余区域要穿透到底下的列表——这时全屏层设 box-none,按钮自己照常接手势。
<View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
<Pressable style={fab} onPress={save}>
<Text style={fabText}>保存</Text>
</Pressable>
</View>StyleSheet.absoluteFill 是内置快捷样式,等价于铺满父级。
collapsable:性能提示,现在基本不用管
collapsable 曾用来提示安卓端"这个 View 是纯布局用的,渲染时可以被合并掉"。老架构时代视图层级越深、原生视图越多,性能越差,手动控制合并是有意义的。0.76 起新架构默认、0.82 起旧架构彻底删除,视图合并(View Flattening)由新架构自动处理,你现在写业务代码基本不需要碰这个属性——读到旧代码里有它,知道是什么即可。
Text:字符串唯一的家
裸字符串会直接把页面炸掉
Web 里 {name} 直接混在 div 里是常态,RN 里任何字符串节点都必须被 Text 包住,否则直接抛红屏错误:
Text strings must be rendered within a <Text> component.// 反例:{name} 是裸字符串,会直接报错
<View>
{name}
</View>
// 推荐写法
<View>
<Text>{name}</Text>
</View>这个坑在从 Web 迁移 JSX 时最容易踩,尤其是 {条件 && '文案'} 这种写法——条件为真时裸字符串就漏出来了。
嵌套 Text 与样式继承
Text 可以嵌在 Text 里(这在 RN 里合法且常用,因为 View 装不了文字):
<Text style={styles.title}>
总计 <Text style={styles.price}>¥128.00</Text>
</Text>嵌套 Text 会继承父级的字体类样式(字号、颜色、字重、字体族、行高这类),布局类属性不跟着传。所以上面 price 只需要写与父级不同的部分。反过来,Text 与 Text 的兄弟之间没有任何继承,每个 Text 的样式都是从 View 布局属性 + 自己的字体属性独立组合。
numberOfLines 与 ellipsizeMode
多行截断靠这两个属性,不需要 -webkit-line-clamp 那套:
<Text numberOfLines={2} ellipsizeMode="tail">
{longArticleSummary}
</Text>ellipsizeMode 取 head / middle / tail / clip(无省略号的硬截断)。标题用 head、正文摘要用 tail、文件名截断用 middle 是常见搭配。
Pressable 包文字的正确姿势
文字永远放在 Pressable 内部的 Text 里,而不是把字符串塞给 Pressable:
<Pressable onPress={submit}>
<Text style={btnText}>提交</Text>
</Pressable>老架构时代安卓上的触摸组件对 children 有苛刻限制(不能 Text/View 混排),新架构(0.82 起唯一架构)已无此包袱,但"Pressable 里放 Text"本就该是肌肉记忆。
Image:三种 source 形态
本地 require
<Image source={require('../assets/logo.png')} style={{ width: 120, height: 40 }} />两个要点。其一,路径必须是字面量相对路径,Metro 在编译期静态分析——require(../assets/${name}.png) 这种动态拼接直接报错。需要动态选图就在模块顶层把图全部 import 好再查表。其二,本地 require 的图必须有显式宽高(或来自 style),不像网络图能拿到 intrinsic size。
多倍图按 iOS 资源命名约定放在同一目录:
assets/
avatar.png // 1x
avatar@2x.png // 2x
avatar@3x.png // 3x代码里只写 require('./avatar.png'),RN 按设备像素密度自动挑。
网络 URI
<Image
source={{ uri: 'https://cdn.example.com/p/1001.jpg' }}
style={styles.thumb}
resizeMode="cover"
backgroundColor="#eee"
onLoad={() => setLoaded(true)}
onError={(e) => console.warn(e.nativeEvent.error)}
/>resizeMode:cover(裁满)/ contain(完整塞进)/ stretch(拉伸变形)/ center(居中原始尺寸)。列表缩略图几乎总是 cover,商品主图详情用 contain。
backgroundColor 占位:网络图下载期间是透明的,给个灰色底让布局"看起来有东西",避免白块跳动。
事件:onLoadStart / onLoad / onLoadEnd / onError,用于埋点和兜底。较新版本的 Image 还支持 errorFallback 之类的加载失败兜底渲染,具体写法以官方文档为准。
注意:RN 的 Image 没有内置磁盘持久缓存。杀进程重启,网络图基本重新下载。生产项目普遍换社区方案(Expo 项目用
expo-image,它能做到内存 + 磁盘双级缓存与淡入过渡),一句话结论:图片加载的缓存与体验问题不在内置 Image 里解决,换包解决。
data URI
Base64 直接当 source:<Image source={{ uri: 'data:image/png;base64,' + b64 }} />。只适合接口返回小头像这类极小内联图,大图解码进内存的开销可观,别走这条路。
ScrollView:页面用它,列表别用它
ScrollView 把全部 children 一次性渲染进内存,只是视口在滚。内容超过几十条、每条又是"图 + 文字 + 嵌套卡片"的列表,用它就是在写一个定时炸弹——首屏渲染卡顿、内存暴涨、低端安卓机直接被系统杀掉。这是每个 RN 新人几乎必交的一次学费:早期用 ScrollView 铺列表,产品用户量上来后闪退报告堆积,一查全是超长 ScrollView。
它的主场是内容有限的页面:设置页、文章详情、表单页。
<ScrollView contentContainerStyle={{ padding: 16 }}>
<ArticleHeader />
<ArticleBody />
<ArticleFooter />
</ScrollView>refreshControl
下拉刷新不是 ScrollView 自带的,而是把一个 RefreshControl 实例挂上去,FlatList/SectionList 同款用法:
const [refreshing, setRefreshing] = useState(false);
<ScrollView
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={async () => {
setRefreshing(true);
await reloadFeed();
setRefreshing(false);
}}
tintColor="#3B82F6"
/>
}
>
<FeedContent />
</ScrollView>horizontal 的陷阱
横向 ScrollView 的 flexDirection 默认变成 row,但内部对齐规则和竖向完全不同;想让内容不满一屏时也均匀分布,要写在 contentContainerStyle 里而不是 style 上:
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16, gap: 12 }}
>
{tags.map((t) => (
<TagChip key={t.id} tag={t} />
))}
</ScrollView>另一个经验坑:横向 ScrollView 嵌在 FlatList 的行里,列表回收复用时滚动位置容易错乱——用 getItemLayout 缓解列表回收问题,或给每行的横向容器固定 key 语义。
FlatList:长列表的标准答案
FlatList 只渲染可视区域附近的内容(虚拟化),配合回收复用,几千条数据也稳。核心 props 逐个给例子,最后合成一个完整可跑的 App。
keyExtractor
<FlatList
data={orders}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <OrderRow order={item} />}
/>默认拿 item.key 或索引。索引当 key 等于没做虚拟化优化——数据插入一条,后面所有 key 全错位。务必给稳定的业务 ID。
extraData:不更新的重灾区
列表行的渲染依赖 data 之外的状态(比如"选中集合")时,必须通过 extraData 告诉列表"这个东西变了请重渲染"。最常见的翻车姿势是传了可变对象:
// 反例:直接改数组内容,引用没变,列表以为世界没变化,UI 不刷新
const selected = useRef<string[]>([]);
selected.current.push(id);
<FlatList data={orders} extraData={selected.current} renderItem={...} />
// 推荐:不可变更新,引用变了,列表才感知到
const [selected, setSelected] = useState<string[]>([]);
setSelected((prev) => [...prev, id]);
<FlatList data={orders} extraData={selected} renderItem={...} />原理:VirtualizedList 对 extraData 做引用比较,push/改字段后引用不变,比较结果相等,行组件不重渲染。这是 RN 面试和线上 bug 的双料常客。
getItemLayout:定高行直接起飞
行高固定时,显式声明让列表跳过测量,滚动定位(scrollToIndex)也不会因估错高度而失灵:
<FlatList
data={messages}
renderItem={renderMessage}
getItemLayout={(_, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
/>ListHeaderComponent:别用行内 JSX
// 反例:每次父组件渲染都创建新的 header 元素,列表视作"换了个组件",整块重建
<FlatList data={list} renderItem={renderItem} ListHeaderComponent={<SearchBar />} />
// 推荐:固定引用
const header = useMemo(() => <SearchBar />, []);
<FlatList data={list} renderItem={renderItem} ListHeaderComponent={header} />同理适用于 ListEmptyComponent、ListFooterComponent。行内 JSX 是列表性能与状态丢失(header 里的输入框失焦、state 清零)的隐形来源。
windowSize 与 removeClippedSubviews 的安卓脾气
windowSize默认 21:以视口为单位,上下各渲染约一半窗口的内容。调小省内存、滚动更快,代价是快速甩动时白屏概率上升。removeClippedSubviews(安卓常用的"视口外卸载视图"开关)历史上在安卓上有翻车记录:绝对定位元素被裁掉看不见、回收后偶现空白格子。默认关着,别因为"性能玄学"随手打开,真有性能问题先走getItemLayout+ 行组件memo这条正路。
完整示例
// App.tsx
import React, { useCallback, useMemo, useState } from 'react';
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
type Product = { id: string; title: string; price: number };
const ROW_HEIGHT = 72;
const data: Product[] = Array.from({ length: 200 }, (_, i) => ({
id: `p-${i}`, title: `商品 ${i + 1}`, price: 9.9 + i,
}));
// 行分隔符也提成顶层组件——行内 () => <JSX/> 同样是"每次渲染换组件"
const Sep = () => <View style={styles.sep} />;
function Row({ item, selected, onToggle }: {
item: Product; selected: boolean; onToggle: (id: string) => void;
}) {
return (
<Pressable
onPress={() => onToggle(item.id)}
style={[styles.row, selected && styles.rowSelected]}
>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.price}>¥{item.price.toFixed(2)}</Text>
</Pressable>
);
}
export default function App() {
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const toggle = useCallback((id: string) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
}, []);
const header = useMemo(
() => <Text style={styles.header}>共 {data.length} 件</Text>, []);
const renderItem = useCallback(
({ item }: { item: Product }) => (
<Row item={item} selected={selectedIds.includes(item.id)} onToggle={toggle} />
),
[selectedIds, toggle],
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id}
extraData={selectedIds}
ItemSeparatorComponent={Sep}
ListHeaderComponent={header}
getItemLayout={(_, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index })}
windowSize={11}
/>
);
}
const styles = StyleSheet.create({
row: {
height: ROW_HEIGHT, flexDirection: 'row', alignItems: 'center',
justifyContent: 'space-between', paddingHorizontal: 16, backgroundColor: '#fff',
},
rowSelected: { backgroundColor: '#EFF6FF' },
title: { fontSize: 16, color: '#111827' },
price: { fontSize: 16, color: '#DC2626', fontWeight: '600' },
header: { padding: 16, fontSize: 14, color: '#6B7280' },
sep: { height: StyleSheet.hairlineWidth, backgroundColor: '#E5E7EB' },
});SectionList 与超长列表进阶
带分组标题(联系人、时间轴)用 SectionList,sections={[{ title, data }]},其余属性与 FlatList 同族。
数据到几千上万行、每行高度不一、还带图片,FlatList 的极限会暴露出来。社区的事实标准是 Shopify 的 FlashList——回收策略比 VirtualizedList 激进得多,一句话:FlatList 先顶着,顶不住了就它。
Pressable:触摸交互的统一答案
RN 早年的触摸组件家族是 TouchableHighlight / TouchableOpacity / TouchableWithoutFeedback / TouchableNativeFeedback,选哪个全靠经验,行为还平台不一致;后来官方用 Pressable 一个组件统一收编——新代码里只用 Pressable,看到老项目里的 Touchable* 按 TouchableOpacity 等价迁移即可。
style 回调:按按压态出样式
style 可以是函数,接一个含 pressed(以及 hovered、focused)的状态对象,返回样式或样式数组——按钮按下变暗不需要 JS 状态:
<Pressable
onPress={save}
style={({ pressed }) => [
styles.btn,
{ opacity: pressed ? 0.7 : 1 },
]}
>
<Text style={styles.btnText}>保存</Text>
</Pressable>android_ripple / hitSlop / disabled
<Pressable
onPress={remove}
disabled={busy}
hitSlop={12}
android_ripple={{ color: 'rgba(0,0,0,0.12)', borderless: true }}
>
<Icon name="trash" />
</Pressable>android_ripple:安卓水波纹反馈(API 21+),borderless适合圆形图标按钮;iOS 没有对应物,用 opacity/scale 回调补。hitSlop:在组件四边外扩可点区域。图标视觉上 16pt,可点区必须够到 44pt 的可按规范——用 hitSlop 扩,别真的把图标画大。disabled:禁用时回调 style 里可读到disabled,置灰提示。
onPress 与 onPressIn 的区别,长按直接 onLongPress
onPressIn:手指按下的瞬间触发;onPressOut:手指抬起/滑出时触发;onPress:按下且未大幅移动后抬起时触发——语义等同"完成一次点击"。
做按压反馈(缩放、变色)才需要 onPressIn/onPressOut;执行业务动作一律挂在 onPress,挂在 onPressIn 会导致"按下即触发、想取消只能快速滑出"的糟糕体验。
长按不需要你手写 onPressIn + setTimeout——Pressable 原生提供 onLongPress,按压超过阈值自动触发,抬起滑出自动取消,平台差异都处理好了:
<Pressable
onPress={() => open(item)}
onLongPress={() => setContextMenu(item)}
pressRetentionOffset={{ top: 20, bottom: 20, left: 20, right: 20 }}
>
<Text>{item.title}</Text>
</Pressable>自己用 setTimeout 模拟长按的版本,在"手指移动算不算取消、iOS/Android 阈值不同"这些细节上几乎一定会做错。
TextInput:先给最小可用版
表单完整世界(校验、键盘类型、焦点链、受控 vs 非受控)留给 06-表单与交互.md,这里只立一个能跑的起点:
import React, { useState } from 'react';
import { Text, TextInput, View } from 'react-native';
export default function App() {
const [email, setEmail] = useState('');
return (
<View style={{ padding: 24 }}>
<Text style={{ marginBottom: 8, color: '#6B7280' }}>邮箱</Text>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="you@example.com"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
style={{ borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 8,
paddingHorizontal: 12, paddingVertical: 10, fontSize: 16 }}
/>
</View>
);
}受控组件(value + onChangeText)是默认姿势;placeholder 颜色用 placeholderTextColor。
Modal:注意安卓返回键
Modal 用于覆盖层弹窗、选择器、全屏对话框。核心属性:visible、transparent(背景透明以便自绘遮罩)、animationType(slide / fade / none)、onRequestClose。
Warning:Modal 不写
onRequestClose,安卓用户按返回键时什么都不会发生——系统把返回事件交给 onRequestClose,你给的是 undefined,返回键就被吞了。这是 Modal 最高频的线上事故。iOS 靠自绘关闭按钮没事,安卓用户直接卡死在弹层里。
const [visible, setVisible] = useState(false);
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={() => setVisible(false)} // 安卓返回键接这里
>
<Pressable style={mask} onPress={() => setVisible(false)}>
{/* 空 onPress 吞掉点击,防止面板内的点击冒到 mask 上把弹窗关了 */}
<Pressable style={sheet} onPress={() => {}}>
<Text>选一个</Text>
{options.map((o) => (
<Pressable key={o} onPress={() => pick(o)}>
<Text style={{ fontSize: 16 }}>{o}</Text>
</Pressable>
))}
</Pressable>
</Pressable>
</Modal>选型上:Modal 适合"临时决策"(选图、确认框、分享面板);如果是流程里的一"页"(支付页、地址编辑页),用导航栈里的全屏路由更合适,返回栈、转场动画、状态保持都是导航框架的事,见 04-导航.md。
不在 Modal 里、但需要接管返回键的场景(比如退出应用前确认),用 BackHandler:
useEffect(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
confirmExit();
return true; // true 表示已处理,阻止默认返回
});
return () => sub.remove();
}, []);小组件合集:ActivityIndicator / RefreshControl / Switch
import React, { useState } from 'react';
import { ActivityIndicator, Switch, Text, View } from 'react-native';
export default function App() {
const [push, setPush] = useState(true);
return (
<View style={{ padding: 24, gap: 20 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<ActivityIndicator size="small" color="#3B82F6" />
<Text>加载中…</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text>推送通知</Text>
<Switch
value={push}
onValueChange={setPush}
trackColor={{ false: '#E5E7EB', true: '#BBF7D0' }}
thumbColor={push ? '#16A34A' : '#9CA3AF'}
/>
</View>
</View>
);
}要点:ActivityIndicator 用 size="large" 做全屏加载态、small 做行内;RefreshControl 前面已讲;Switch 样式自定义有限,需要平台一致的品牌开关时社区有现成组件。trackColor/thumbColor 是安卓上改善默认观感的最低成本手段。
SafeAreaView:内置的那个已经弃用
RN 自带的 SafeAreaView 有两个硬伤:只在 iOS 生效(安卓刘海/手势条区域完全不管),且已被官方文档标注弃用。新架构 + edge-to-edge 时代,内容铺到状态栏和系统导航区后面,insets(安全区内边距)在两个平台都需要手动处理。
bare 模板(含 0.88)已预装社区标准方案 react-native-safe-area-context(^5):根节点包 Provider,容器用它的 SafeAreaView,精确计算用 useSafeAreaInsets。
import React from 'react';
import { SafeAreaProvider, useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
import { StatusBar, Text, View } from 'react-native';
function Screen() {
const insets = useSafeAreaInsets();
return (
<View style={{ flex: 1, backgroundColor: '#fff' }}>
{/* 自绘顶栏手动垫 insets.top;列表/输入交给 SafeAreaView 的 edges 兜底 */}
<View style={{ paddingTop: insets.top + 8, paddingHorizontal: 16 }}>
<Text style={{ fontSize: 18, fontWeight: '600' }}>客服对话</Text>
</View>
<MessageList />
<Composer />
</View>
);
}
export default function App() {
return (
<SafeAreaProvider>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<SafeAreaView edges={['bottom']} style={{ flex: 1 }}>
<Screen />
</SafeAreaView>
</SafeAreaProvider>
);
}Expo 项目里同一个包由 Expo 工作流支撑,用法一致。edge-to-edge 范式下这套东西为什么从"iOS 可选"变成两平台必选,见 03-样式与布局.md 的 Edge-to-Edge 一节。
无障碍:不是可选项
移动端审核(App Store 无障碍审查、企业合规)会真的找上门,而且实现成本极低。核心组件普遍支持这组 props:
accessibilityRole:语义角色,'button'/'image'/'header'/'link',读屏软件靠它播报"按钮,……"accessibilityLabel:替非文字内容发声。图标按钮没有文字,VoiceOver 只会念"按钮",必须给 labelaccessibilityState:{ selected, disabled, checked, busy },状态给读屏
<Pressable
onPress={deleteItem}
accessibilityRole="button"
accessibilityLabel={`删除商品 ${item.title}`}
accessibilityState={{ disabled: busy }}
>
<Icon name="trash" />
</Pressable>布局顺序尽量与视觉顺序一致(屏幕阅读器按节点树顺序走),纯装饰图设 accessibilityElementsHidden(iOS)/ importantForAccessibility="no"(安卓)。把 accessibility 三件套当成按钮的"必填 props"来做,代码评审里缺了直接提。
Variable Fonts:0.88 的字体新能力
RN 0.88 引入了对可变字体(Variable Fonts)的支持:一个字体文件内连续调节字重轴,450、550 这种中间值不再是断档。较新版本的 RN 在 Text 样式上以 fontVariations 一类属性暴露可变轴控制(具体取值格式以官方文档为准),配合内置的 fontWeight 数字档位,排版质感可以精细很多——前提是字体文件本身是可变字体,随包打进 assets。
实战建议
组件选型顺序:内置 → 社区 → 自绘。 内置组件定生死(View/Text/Image/Pressable/FlatList 这五个会占你 JSX 的九成),业务能力全交给社区:
- 列表:先 FlatList,行数爆炸且行高参差再上 FlashList;内容有限的页面用 ScrollView。
- 图片:内置 Image 起步,需要磁盘缓存/淡入/AVIF 直接
expo-image,别自己封装缓存层。 - 触摸:只用 Pressable;按压反馈优先 style 回调,要弹簧动画去
07-动画与手势.md。 - 安全区:只认
react-native-safe-area-context,内置 SafeAreaView 见到就迁。 - 浮层:临时决策用 Modal(写
onRequestClose),流程页交给导航栈。 - 无障碍:icon 按钮必写
accessibilityLabel+accessibilityRole,状态用accessibilityState,写进 PR 检查习惯。