name: rn-styling description: React Native 样式指南
React Native 样式指南
问题说明
React Native 样式与 Web CSS 有本质的不同。NativeWind 弥补了这一差距,但也有自己的规则。此代码库使用混合方法:BrandColors 用于语义化颜色,NativeWind 用于布局工具类。
模式:BrandColors 与 NativeWind 类
规则: 使用 BrandColors 进行语义化颜色定义,使用 NativeWind 进行布局/间距定义。
// ✅ 正确:混合方法
<View className="flex-1 p-4 rounded-lg" style={{ backgroundColor: BrandColors.background }}>
<Text className="text-lg font-semibold" style={{ color: BrandColors.textPrimary }}>
标题
</Text>
</View>
// ❌ 错误:硬编码十六进制颜色(违规扫描器会阻止此操作)
<View className="flex-1 p-4 bg-[#1a1a2e]">
// ❌ 错误:使用 NativeWind 颜色类进行品牌颜色定义
<View className="flex-1 p-4 bg-blue-500">
// ✅ 可接受:NativeWind 品牌别名(如果已配置)
<View className="flex-1 p-4 bg-brand-blue">
何时使用哪个:
| 使用场景 | 方法 |
|---|---|
| 品牌颜色(主要,次要) | BrandColors.primary |
| 背景颜色 | BrandColors.background |
| 文本颜色 | BrandColors.textPrimary, textSecondary |
| 布局(flex, padding, margin) | NativeWind 类 |
| 边框,圆角 | NativeWind 类 |
| 阴影 | 样式对象(NativeWind 阴影在 iOS 上有限) |
模式:主题感知颜色
问题: 支持使用 BrandColors 的浅色/深色模式。
// BrandColors.ts 导出两种主题
import { BrandColors, BrandColorsDark } from '@/constants/BrandColors';
// 当前主题颜色的钩子
import { useColorScheme } from 'react-native';
function useThemeColors() {
const colorScheme = useColorScheme();
return colorScheme === 'dark' ? BrandColorsDark : BrandColors;
}
// 组件使用
function ThemedCard({ title }: { title: string }) {
const colors = useThemeColors();
return (
<View
className="p-4 rounded-lg"
style={{ backgroundColor: colors.cardBackground }}
>
<Text style={{ color: colors.textPrimary }}>{title}</Text>
</View>
);
}
模式:NativeWind 类排序
问题: 与 Web CSS 不同,React Native 不支持层叠样式。对于冲突属性,最后一个类获胜。
// 类排序很重要!
<View className="p-4 p-2" /> // p-2 获胜(最后)
<View className="p-2 p-4" /> // p-4 获胜(最后)
// 条件类 - 明确
<View className={`p-4 ${isCompact ? 'p-2' : ''}`} />
// 如果 isCompact: "p-4 p-2" → p-2 获胜 ✅
// 合并 className 属性
interface Props {
className?: string;
}
function Card({ className }: Props) {
// 父类覆盖默认值(它们最后出现)
return <View className={`p-4 rounded-lg ${className ?? ''}`} />;
}
// 使用:<Card className="p-8" /> → p-8 覆盖 p-4
模式:平台特定样式
import { Platform, StyleSheet } from 'react-native';
// 选项 1: Platform.select
const styles = StyleSheet.create({
shadow: Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
});
// 选项 2: Platform.OS 检查
<View style={Platform.OS === 'ios' ? styles.iosShadow : styles.androidShadow} />
// 选项 3: NativeWind 平台前缀
<View className="ios:pt-12 android:pt-8" />
模式:安全区域处理
import { SafeAreaView } from 'react-native-safe-area-context';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// 选项 1: SafeAreaView 包装器(最简单)
function Screen() {
return (
<SafeAreaView className="flex-1" edges={['top', 'bottom']}>
<Content />
</SafeAreaView>
);
}
// 选项 2: 手动内边距(更多控制)
function Screen() {
const insets = useSafeAreaInsets();
return (
<View
className="flex-1"
style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
>
<Content />
</View>
);
}
// 选项 3: NativeWind 安全区域工具类(如果已配置)
<View className="flex-1 pt-safe pb-safe">
模式:键盘避免
import { KeyboardAvoidingView, Platform } from 'react-native';
function FormScreen() {
return (
<KeyboardAvoidingView
className="flex-1"
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0} // 调整标题栏
>
<ScrollView className="flex-1">
<TextInput />
<TextInput />
<SubmitButton />
</ScrollView>
</KeyboardAvoidingView>
);
}
模式:响应式断点
注意: NativeWind v2 断点与 Web Tailwind 不同。
// NativeWind v2 断点(基于窗口宽度)
// sm: 640px, md: 768px, lg: 1024px, xl: 1280px
// 响应式内边距
<View className="p-2 sm:p-4 md:p-6" />
// 响应式 flex 方向
<View className="flex-col sm:flex-row" />
// 程序性检查屏幕尺寸
import { useWindowDimensions } from 'react-native';
function ResponsiveLayout() {
const { width } = useWindowDimensions();
const isTablet = width >= 768;
return isTablet ? <TabletLayout /> : <PhoneLayout />;
}
模式:动画样式
问题: 避免使用 Animated 值时的重新渲染。
import { Animated } from 'react-native';
function FadeInCard() {
// useRef 持久化 Animated.Value 跨渲染
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true, // 动画 opacity/transform 时总是使用
}).start();
}, []);
return (
<Animated.View
className="p-4 rounded-lg"
style=[
{ backgroundColor: BrandColors.cardBackground },
{ opacity: fadeAnim }, // 动画样式在数组中
]
>
<Text>内容</Text>
</Animated.View>
);
}
样式数组: 结合静态 + 动画样式。
// ✅ 正确:样式数组
style={[styles.card, { opacity: fadeAnim }]}
// ❌ 错误:展开(每次渲染都会创建新对象)
style={{ ...styles.card, opacity: fadeAnim }}
模式:StyleSheet 与内联
// 使用 StyleSheet 的情况:
// - 跨渲染重复使用的复杂样式
// - 具有多个属性的样式
// - 性能关键组件
const styles = StyleSheet.create({
card: {
padding: 16,
borderRadius: 12,
backgroundColor: BrandColors.cardBackground,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
});
// 使用内联/NativeWind 的情况:
// - 简单的布局工具类
// - 一次性样式
// - 条件样式
<View className="flex-1 p-4" />
<View style={{ marginTop: dynamicValue }} />
品牌颜色模式
创建一个集中的颜色常量文件:
// constants/BrandColors.ts
export const BrandColors = {
primary: '#...',
secondary: '#...',
background: '#...',
cardBackground: '#...',
textPrimary: '#...',
textSecondary: '#...',
// ... 等
};
export const BrandColorsDark = {
// 深色模式变体
};
推荐:违规扫描器
考虑添加违规扫描器以阻止:
- 硬编码十六进制颜色(除了允许的例外)
- 直接颜色字符串
NativeWind 注释
如果使用 NativeWind v2(而不是 v4),请留意这些差异:
- RN 组件上的
className属性 - 有限的 Web Tailwind 兼容性
- 一些工具类不受支持
常见问题
| 问题 | 解决方案 |
|---|---|
| 颜色不适用 | 检查 BrandColors 导入,验证主题上下文 |
| NativeWind 类被忽略 | 不是所有的 Tailwind 工具类都有效 - 检查 v2 文档 |
| 阴影不显示(iOS) | 使用 StyleSheet 与 shadowColor/Offset/Opacity/Radius |
| 阴影不显示(Android) | 使用 elevation 属性 |
| 安全区域不被尊重 | 包装在 SafeAreaView 中或使用内边距 |
| 样式在挂载时闪烁 | 使用 Animated 进行过渡 |
推荐文件结构
constants/
BrandColors.ts # 颜色定义
designSystem.ts # 间距,排版比例
components/
ui/Card.tsx # 示例混合样式
app/
_layout.tsx # 主题提供者设置