name: ui-mobile description: 移动用户界面模式 - React Native, iOS/Android, 触摸目标
移动UI设计技能(React Native)
加载方式:base.md + react-native.md
强制性:移动可访问性标准
这些规则是不可协商的。每个UI元素都必须通过这些检查。
1. 触摸目标(关键)
// 所有交互元素的最小尺寸为44x44点
const MINIMUM_TOUCH_SIZE = 44;
// 每个按钮、链接、图标按钮都必须满足此要求
const styles = StyleSheet.create({
button: {
minHeight: MINIMUM_TOUCH_SIZE,
minWidth: MINIMUM_TOUCH_SIZE,
paddingVertical: 12,
paddingHorizontal: 16,
},
iconButton: {
width: MINIMUM_TOUCH_SIZE,
height: MINIMUM_TOUCH_SIZE,
justifyContent: 'center',
alignItems: 'center',
},
});
// 永远不要这样做:
style={{ height: 30 }} // ✗ 太小
style={{ padding: 4 }} // ✗ 结果是微小的目标
2. 颜色对比度(关键)
// WCAG 2.1 AA: 文本4.5:1,大文本/界面元素3:1
// 安全组合:
const colors = {
// 浅色模式
textPrimary: '#000000', // 白色背景 = 21:1 ✓
textSecondary: '#374151', // 灰-700在白色背景 = 9.2:1 ✓
// 深色模式
textPrimaryDark: '#FFFFFF', // 灰-900背景 = 16:1 ✓
textSecondaryDark: '#E5E7EB', // 灰-200在灰-900背景 = 11:1 ✓
};
// 禁止 - 对比度失败:
// ✗ '#9CA3AF' (灰-400)在白色背景 = 2.6:1
// ✗ '#6B7280' (灰-500)在'#111827' = 4.0:1
// ✗ 任何低于4.5:1比率的文本
3. 可见性规则
// 所有按钮都必须有可见的边界
// 主要的:带有对比文本的实心背景
<Pressable style={styles.primaryButton}>
<Text style={{ color: '#FFFFFF' }}>提交</Text>
</Pressable>
const styles = StyleSheet.create({
primaryButton: {
backgroundColor: '#1F2937', // 灰-800
paddingVertical: 16,
paddingHorizontal: 24,
borderRadius: 12,
minHeight: 44,
},
});
// 次要的:可见背景
<Pressable style={styles.secondaryButton}>
<Text style={{ color: '#1F2937' }}>取消</Text>
</Pressable>
const styles = StyleSheet.create({
secondaryButton: {
backgroundColor: '#F3F4F6', // 灰-100
minHeight: 44,
},
});
// 幽灵:必须有可见边框
<Pressable style={styles.ghostButton}>
<Text style={{ color: '#374151' }}>跳过</Text>
</Pressable>
const styles = StyleSheet.create({
ghostButton: {
borderWidth: 1,
borderColor: '#D1D5DB', // 灰-300
minHeight: 44,
},
});
// 永远不要创建不可见的按钮:
// ✗ backgroundColor: 'transparent'没有边框
// ✗ 文本颜色与背景匹配
4. 辅助功能标签(必需)
// 每个交互元素都需要辅助功能属性
// 按钮
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel="提交表单"
accessibilityHint="双击提交您的信息"
>
<Text>提交</Text>
</Pressable>
// 图标按钮(没有可见文本 = 必须有标签)
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel="关闭菜单"
>
<CloseIcon />
</Pressable>
// 图像
<Image
accessible={true}
accessibilityRole="image"
accessibilityLabel="用户头像"
source={...}
/>
5. 焦点/选择状态
// 每个Pressable都需要可见的按下状态
<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
>
{children}
</Pressable>
const styles = StyleSheet.create({
button: {
backgroundColor: '#1F2937',
},
buttonPressed: {
opacity: 0.7,
// 或者
backgroundColor: '#374151',
},
});
核心理念
**移动UI是关于触摸、速度和焦点的。**没有悬停状态,更小的屏幕,拇指友好的目标。为单手使用和中断恢复而设计。
平台差异
iOS与Android
import { Platform } from 'react-native';
// 平台特定的值
const styles = StyleSheet.create({
shadow: Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
android: {
elevation: 4,
},
}),
// iOS使用SF Pro,Android使用Roboto
text: {
fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
},
});
设计语言
iOS (人机界面指南)
─────────────────────────────────
- 平面设计,带有微妙的深度
- SF Symbols图标
- 大标题(34pt)
- 圆角(10-14pt)
- 蓝色作为默认色调
Android (Material Design 3)
─────────────────────────────────
- Material You动态颜色
- 轮廓/填充图标
- 中等标题(22pt)
- 圆角(12-28pt)
- 主题中的主色
间距系统
4px基础网格
// React Native间距 - 一致的比例
const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
'2xl': 48,
} as const;
// 使用
const styles = StyleSheet.create({
container: {
padding: spacing.md,
gap: spacing.sm,
},
});
安全区域
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const Screen = ({ children }) => {
const insets = useSafeAreaInsets();
return (
<View style={{
flex: 1,
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: Math.max(insets.left, 16),
paddingRight: Math.max(insets.right, 16),
}}>
{children}
</View>
);
};
排版
字型规模
const typography = {
// 大标题(iOS风格)
largeTitle: {
fontSize: 34,
fontWeight: '700' as const,
letterSpacing: 0.37,
},
// 章节标题
title: {
fontSize: 22,
fontWeight: '700' as const,
letterSpacing: 0.35,
},
// 卡片标题
headline: {
fontSize: 17,
fontWeight: '600' as const,
letterSpacing: -0.41,
},
// 正文
body: {
fontSize: 17,
fontWeight: '400' as const,
letterSpacing: -0.41,
lineHeight: 22,
},
// 二级文本
callout: {
fontSize: 16,
fontWeight: '400' as const,
letterSpacing: -0.32,
},
// 小标签
caption: {
fontSize: 12,
fontWeight: '400' as const,
letterSpacing: 0,
},
};
颜色系统
语义颜色
// 使用语义名称,而不是字面颜色
const colors = {
// 背景
background: '#FFFFFF',
backgroundSecondary: '#F2F2F7',
backgroundTertiary: '#FFFFFF',
// 表面
surface: '#FFFFFF',
surfaceElevated: '#FFFFFF',
// 文本
label: '#000000',
labelSecondary: '#3C3C43', // 60%透明度
labelTertiary: '#3C3C43', // 30%透明度
// 动作
primary: '#007AFF',
destructive: '#FF3B30',
success: '#34C759',
warning: '#FF9500',
// 分隔符
separator: '#3C3C43', // 29%透明度
opaqueSeparator: '#C6C6C8',
};
// 深色模式变体
const darkColors = {
background: '#000000',
backgroundSecondary: '#1C1C1E',
label: '#FFFFFF',
labelSecondary: '#EBEBF5', // 60%透明度
separator: '#545458',
};
动态颜色(React Native)
import { useColorScheme } from 'react-native';
const useColors = () => {
const scheme = useColorScheme();
return scheme === 'dark' ? darkColors : colors;
};
// 使用
const MyComponent = () => {
const colors = useColors();
return (
<View style={{ backgroundColor: colors.background }}>
<Text style={{ color: colors.label }}>Hello</Text>
</View>
);
};
触摸目标
最小尺寸
// 关键:最小44pt触摸目标
const touchable = {
minHeight: 44,
minWidth: 44,
};
// 按钮与适当尺寸
const styles = StyleSheet.create({
button: {
minHeight: 44,
paddingHorizontal: 16,
paddingVertical: 12,
justifyContent: 'center',
alignItems: 'center',
},
// 图标按钮(正方形)
iconButton: {
width: 44,
height: 44,
justifyContent: 'center',
alignItems: 'center',
},
// 列表行
listRow: {
minHeight: 44,
paddingVertical: 12,
paddingHorizontal: 16,
},
});
触摸反馈
import { Pressable } from 'react-native';
// iOS风格的不透明度反馈
const Button = ({ children, onPress }) => (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.button,
pressed && { opacity: 0.7 },
]}
>
{children}
</Pressable>
);
// Android风格的波纹
const AndroidButton = ({ children, onPress }) => (
<Pressable
onPress={onPress}
android_ripple={{
color: 'rgba(0, 0, 0, 0.1)',
borderless: false,
}}
style={styles.button}
>
{children}
</Pressable>
);
组件模式
卡片
const Card = ({ children, style }) => (
<View style={[styles.card, style]}>
{children}
</View>
);
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
},
android: {
elevation: 2,
},
}),
},
});
按钮
// 主按钮
const PrimaryButton = ({ title, onPress, disabled }) => (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => [
styles.primaryButton,
pressed && styles.primaryButtonPressed,
disabled && styles.buttonDisabled,
]}
>
<Text style={styles.primaryButtonText}>{title}</Text>
</Pressable>
);
const styles = StyleSheet.create({
primaryButton: {
backgroundColor: '#007AFF',
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
alignItems: 'center',
},
primaryButtonPressed: {
backgroundColor: '#0056B3',
},
primaryButtonText: {
color: '#FFFFFF',
fontSize: 17,
fontWeight: '600',
},
buttonDisabled: {
opacity: 0.5,
},
});
// 次按钮
const SecondaryButton = ({ title, onPress }) => (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.secondaryButton,
pressed && { opacity: 0.7 },
]}
>
<Text style={styles.secondaryButtonText}>{title}</Text>
</Pressable>
);
输入字段
const TextField = ({ label, value, onChangeText, error }) => {
const [focused, setFocused] = useState(false);
return (
<View style={styles.textFieldContainer}>
{label && (
<Text style={styles.textFieldLabel}>{label}</Text>
)}
<TextInput
value={value}
onChangeText={onChangeText}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
style={[
styles.textField,
focused && styles.textFieldFocused,
error && styles.textFieldError,
]}
placeholderTextColor="#8E8E93"
/>
{error && (
<Text style={styles.errorText}>{error}</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
textFieldContainer: {
gap: 8,
},
textFieldLabel: {
fontSize: 15,
fontWeight: '500',
color: '#3C3C43',
},
textField: {
backgroundColor: '#F2F2F7',
borderRadius: 10,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 17,
color: '#000000',
borderWidth: 2,
borderColor: 'transparent',
},
textFieldFocused: {
borderColor: '#007AFF',
backgroundColor: '#FFFFFF',
},
textFieldError: {
borderColor: '#FF3B30',
},
errorText: {
fontSize: 13,
color: '#FF3B30',
},
});
列表
// 分组列表(iOS设置风格)
const GroupedList = ({ sections }) => (
<ScrollView style={styles.groupedList}>
{sections.map((section, i) => (
<View key={i} style={styles.section}>
{section.title && (
<Text style={styles.sectionHeader}>{section.title}</Text>
)}
<View style={styles.sectionContent}>
{section.items.map((item, j) => (
<React.Fragment key={j}>
{j > 0 && <View style={styles.separator} />}
<Pressable
style={({ pressed }) => [
styles.listRow,
pressed && { backgroundColor: '#E5E5EA' },
]}
onPress={item.onPress}
>
<Text style={styles.listRowText}>{item.title}</Text>
<ChevronRight color="#C7C7CC" />
</Pressable>
</React.Fragment>
))}
</View>
</View>
))}
</ScrollView>
);
const styles = StyleSheet.create({
groupedList: {
flex: 1,
backgroundColor: '#F2F2F7',
},
section: {
marginTop: 35,
},
sectionHeader: {
fontSize: 13,
fontWeight: '400',
color: '#6D6D72',
textTransform: 'uppercase',
marginLeft: 16,
marginBottom: 8,
},
sectionContent: {
backgroundColor: '#FFFFFF',
borderRadius: 10,
marginHorizontal: 16,
overflow: 'hidden',
},
listRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 16,
minHeight: 44,
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#C6C6C8',
marginLeft: 16,
},
});
导航模式
底部标签栏
// 适当的底部标签尺寸
const tabBarStyle = {
height: Platform.OS === 'ios' ? 83 : 65, // 考虑主页指示器
paddingBottom: Platform.OS === 'ios' ? 34 : 10,
paddingTop: 10,
backgroundColor: '#F8F8F8',
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#C6C6C8',
};
// 标签项
const TabItem = ({ icon, label, active }) => (
<View style={styles.tabItem}>
<Icon name={icon} color={active ? '#007AFF' : '#8E8E93'} size={24} />
<Text style=[
styles.tabLabel,
{ color: active ? '#007AFF' : '#8E8E93' }
]}>
{label}
</Text>
</View>
);
头部
// 大标题头(iOS)
const LargeTitleHeader = ({ title, rightAction }) => {
const insets = useSafeAreaInsets();
return (
<View style={[styles.header, { paddingTop: insets.top }]}>
<View style={styles.headerContent}>
<Text style={styles.largeTitle}>{title}</Text>
{rightAction}
</View>
</View>
);
};
const styles = StyleSheet.create({
header: {
backgroundColor: '#F8F8F8',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#C6C6C8',
},
headerContent: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingBottom: 8,
},
largeTitle: {
fontSize: 34,
fontWeight: '700',
letterSpacing: 0.37,
},
});
动画
本地驱动动画
import { Animated } from 'react-native';
// 尽可能使用本地驱动
const fadeIn = (value: Animated.Value) => {
Animated.timing(value, {
toValue: 1,
duration: 200,
useNativeDriver: true, // 对性能至关重要
}).start();
};
// 弹簧带来自然感觉
const bounce = (value: Animated.Value) => {
Animated.spring(value, {
toValue: 1,
damping: 15,
stiffness: 150,
useNativeDriver: true,
}).start();
};
用于复杂动画的Reanimated
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
const AnimatedCard = ({ children }) => {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const onPressIn = () => {
scale.value = withSpring(0.95);
};
const onPressOut = () => {
scale.value = withSpring(1);
};
return (
<Pressable onPressIn={onPressIn} onPressOut={onPressOut}>
<Animated.View style={[styles.card, animatedStyle]}>
{children}
</Animated.View>
</Pressable>
);
};
加载状态
骨架加载器
const SkeletonLoader = ({ width, height, borderRadius = 4 }) => {
const opacity = useSharedValue(0.3);
useEffect(() => {
opacity.value = withRepeat(
withSequence(
withTiming(1, { duration: 500 }),
withTiming(0.3, { duration: 500 })
),
-1,
false
);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
return (
<Animated.View
style=[
{ width, height, borderRadius, backgroundColor: '#E5E5EA' },
animatedStyle,
]}
/>
);
};
活动指示器
import { ActivityIndicator } from 'react-native';
// 使用平台原生指示器
<ActivityIndicator size="large" color="#007AFF" />
// 带加载状态的按钮
const LoadingButton = ({ loading, title, onPress }) => (
<Pressable
onPress={onPress}
disabled={loading}
style={styles.button}
>
{loading ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.buttonText}>{title}</Text>
)}
</Pressable>
);
辅助功能
VoiceOver / TalkBack
// 可访问按钮
<Pressable
onPress={onPress}
accessible={true}
accessibilityRole="button"
accessibilityLabel="提交表单"
accessibilityHint="双击提交您的信息"
>
<Text>提交</Text>
</Pressable>
// 可访问图像
<Image
source={icon}
accessible={true}
accessibilityRole="image"
accessibilityLabel="用户头像"
/>
// 将相关元素分组
<View
accessible={true}
accessibilityRole="summary"
accessibilityLabel={`${name}, ${role}, ${status}`}
>
<Text>{name}</Text>
<Text>{role}</Text>
<Text>{status}</Text>
</View>
动态字体(iOS)
import { PixelRatio } from 'react-native';
// 根据系统设置缩放字体
const fontScale = PixelRatio.getFontScale();
const scaledFontSize = (size: number) => size * fontScale;
// 或者使用allowFontScaling
<Text allowFontScaling={true} style={{ fontSize: 17 }}>
此文本随着系统设置缩放
</Text>
反模式
永不要做
✗ 小于44pt的触摸目标
✗ 小于12pt的文本
✗ 悬停状态(移动设备上没有悬停)
✗ 固定高度破坏大文本
✗ 忽略安全区域
✗ 在Android上使用重阴影(使用elevation)
✗ 白色文本在浅色背景上不检查对比度
✗ 非原生动画(JS驱动的变换)
✗ 忽略平台约定(iOS与Android)
✗ 到处使用内联样式(使用StyleSheet.create)
常见错误
// ✗ 破坏辅助功能的硬编码尺寸
style={{ height: 40 }} // 文本可能更大
// ✓ 最小高度与填充
style={{ minHeight: 44, paddingVertical: 12 }}
// ✗ 在Android上使用阴影
shadowColor: '#000' // 不起作用
// ✓ 平台特定
...Platform.select({
ios: { shadowColor: '#000', ... },
android: { elevation: 4 },
})
// ✗ 固定状态栏高度
paddingTop: 44
// ✓ 使用安全区域
paddingTop: insets.top
快速参考
移动默认值
触摸目标:最小44pt
字体大小:最小12pt,17pt正文,34pt大标题
圆角:10-14pt(iOS),12-28pt(Android)
间距:4/8/16/24/32网格
动画:200-300ms,本地驱动
阴影:iOS shadowOpacity 0.08-0.15,Android elevation 2-8
高端感觉清单
□ 所有触摸目标44pt+
□ 一致的间距(4pt网格)
□ 平台适当的样式
□ 安全区域处理
□ 原生动画(60fps)
□ 适当的加载状态
□ 深色模式支持
□ 辅助功能标签
□ 动作上的触觉反馈
□ 适当的地方拉刷新