移动UI设计技能(ReactNative) ui-mobile

本技能涉及移动用户界面设计的最佳实践,包括React Native框架的使用,以及iOS和Android平台的特定设计模式。它涵盖了从触摸目标尺寸、颜色对比度、可见性规则到辅助功能标签等一系列关键的UI设计原则,确保移动应用的可访问性和用户友好性。

移动开发 0 次安装 0 次浏览 更新于 3/5/2026

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)
□ 适当的加载状态
□ 深色模式支持
□ 辅助功能标签
□ 动作上的触觉反馈
□ 适当的地方拉刷新