Skyroc Native UI

Avatar

展示用户或实体头像的圆形图片组件

头像(Avatar)用圆形图片表示用户或实体,加载失败或没有图片时降级为首字母、图标等占位内容。组件直接渲染成一个 radius="full"Image,不自建加载状态:空 src 与加载失败等价、src 变化时重置状态这两件事都由 Image 内部收敛,AvatarGroup 则负责把多个头像横向叠压并折叠成 +N

import { Avatar, AvatarGroup } from '@skyroc/native-ui';

基础用法

src 传图片地址,fallback 传没有图片时的降级内容。fallbackstring / number 时自动包一层 Text 并套用当前尺寸的字号,传自定义节点则原样渲染。

AvatarBasic.tsx
Loading…
import { Avatar, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 固定 seed,避免每次刷新拿到不同的示例图片 */
const FACE = 'https://picsum.photos/seed/av1/100';

const AvatarBasic = () => {
  return (
    <View className="flex-row flex-wrap items-center gap-4 bg-background p-4">
      <Avatar
        alt="示例用户头像"
        src={FACE}
      />
      <Avatar fallback="张" />
      <Avatar fallback={7} />
      <Avatar fallback={<Text className="text-xs font-semibold text-primary">VIP</Text>} />
    </View>
  );
};

export { AvatarBasic };

何时使用

  • 列表、评论、会话等需要标识"这条内容属于谁"的位置。
  • 用户没有上传头像时,用姓氏首字母 + 主题色底做占位,比一张灰色破图更可读。
  • 需要表达"多人参与"(协作者、群成员、点赞者)时用 AvatarGroup,配合 max / total 折叠出 +N
  • 只是展示一张普通图片、不需要圆形与降级语义时,直接用 Image

尺寸

size 提供 6 档,同时决定头像直径与 fallback 的文字字号:

尺寸直径fallback 字号
xs2410
sm3212
md4014(默认)
lg4816
xl5618
2xl6420
AvatarSize.tsx
Loading…
import { Avatar, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const SIZES = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const;

const AvatarSize = () => {
  return (
    <View className="flex-row flex-wrap items-end gap-4 bg-background p-4">
      {SIZES.map(size => (
        <View
          className="items-center gap-1.5"
          key={size}
        >
          <Avatar
            fallback="AB"
            size={size}
          />
          <Text className="text-xs text-muted-foreground">{size}</Text>
        </View>
      ))}
    </View>
  );
};

export { AvatarSize };

尺寸只落在根节点上,图片与 fallback 都相对根节点铺满,所以不需要再给内部节点单独设宽高。

降级内容

以下三种情况都会进入 fallbacksrc 为空、src 加载失败、src 指向的资源不可达。不传 fallback 时回落到 Image 内置的破损图片图标。

AvatarFallback.tsx
Loading…
import { Avatar } from '@skyroc/native-ui';
import { View } from 'react-native';

/** DNS 解析失败的地址,用于稳定触发 fallback */
const BROKEN = 'https://invalid-url.test/broken.jpg';

const AvatarFallback = () => {
  return (
    <View className="flex-row flex-wrap items-center gap-4 bg-background p-4">
      <Avatar
        fallback="坏"
        src={BROKEN}
      />
      <Avatar
        fallback="空"
        src={undefined}
      />
      <Avatar
        alt="王小明的头像"
        fallback="王"
        src={BROKEN}
      />
      <Avatar />
    </View>
  );
};

export { AvatarFallback };

fallback 底部的圆盘来自 Image 的 error slot(bg-muted + 居中),想换成品牌色时覆盖 classNames.fallbackclassNames.fallbackText,不必自己包一层 View 摆位置。

底层图片属性

imageProps 透传给内部 Image,用来控制过渡时长、加载占位等图片行为。alt / src / className / classNames / errorSlot 已由 Avatar 自身的 API 接管,因此从 imageProps 的类型里排除。

AvatarImageProps.tsx
Loading…
import { Avatar, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 固定 seed,避免每次刷新拿到不同的示例图片 */
const FACES = ['https://picsum.photos/seed/av2/100', 'https://picsum.photos/seed/av3/100'];

const AvatarImageProps = () => {
  return (
    <View className="flex-row flex-wrap items-end gap-5 bg-background p-4">
      <View className="items-center gap-2">
        <Avatar
          imageProps={{ transition: 300 }}
          size="xl"
          src={FACES[0]}
        />
        <Text className="text-xs text-muted-foreground">transition=300</Text>
      </View>
      <View className="items-center gap-2">
        <Avatar
          imageProps={{ showLoading: true, transition: 500 }}
          size="xl"
          src={`${FACES[1]}?loading`}
        />
        <Text className="text-xs text-muted-foreground">showLoading</Text>
      </View>
    </View>
  );
};

export { AvatarImageProps };

Avatar 默认关掉了加载指示器(showLoading={false})——头像尺寸小,转圈是噪音,根节点的 bg-muted 已经先占住位置。需要时用 imageProps={{ showLoading: true }} 打开。注意 imageProps 在内部属性之后展开,contentFitradiusshowLoading 都可以被它覆盖,传 radius 会让头像不再是圆形。

样式覆盖

className 追加到根节点,classNames 按 slot 细粒度覆盖:

slot作用位置
root根节点,尺寸与底色都在这里
image图片自身,默认额外补一层 rounded-full
fallback降级内容的容器(Image 的 error slot)
fallbackText降级内容为 string / number 时自动包裹的 Text
AvatarStyles.tsx
Loading…
import { Avatar } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 固定 seed,避免每次刷新拿到不同的示例图片 */
const FACE = 'https://picsum.photos/seed/av5/100';

const AvatarStyles = () => {
  return (
    <View className="flex-row flex-wrap items-center gap-4 bg-background p-4">
      <Avatar
        classNames={{ fallback: 'bg-primary', fallbackText: 'text-primary-foreground' }}
        fallback="A"
      />
      <Avatar
        classNames={{ fallback: 'bg-destructive', fallbackText: 'text-destructive-foreground' }}
        fallback="B"
      />
      <Avatar
        classNames={{ fallback: 'bg-success', fallbackText: 'text-success-foreground' }}
        fallback="C"
      />
      <Avatar
        className="rounded-lg"
        classNames={{ image: 'rounded-lg' }}
        src={FACE}
      />
    </View>
  );
};

export { AvatarStyles };

改成方角头像要同时给 classNameclassNames.image:Android 上父级 overflow-hidden 加圆角偶发裁剪失效,image 上那层圆角是兜底,只改根节点会漏出方角。

动态换图

src 变化时内部会重置加载状态,坏图之后切回正常图片可以正常恢复显示,不会残留上一次的失败态。

AvatarDynamicSource.tsx
Loading…
import { Avatar, Button, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

/** DNS 解析失败的地址,用于稳定触发 fallback */
const BROKEN = 'https://invalid-url.test/broken.jpg';

/** 包含正常图片与坏图,用于验证 src 变化后失败状态能够恢复 */
const GALLERY = [
  'https://picsum.photos/seed/av1/100',
  BROKEN,
  'https://picsum.photos/seed/av3/100',
  'https://picsum.photos/seed/av4/100'
];

const AvatarDynamicSource = () => {
  const [index, setIndex] = useState(0);

  return (
    <View className="items-start gap-3 bg-background p-4">
      <Avatar
        fallback="?"
        size="2xl"
        src={GALLERY[index]}
      />
      <Text className="text-sm text-muted-foreground">
        当前图片:{index + 1} / {GALLERY.length}
        {GALLERY[index] === BROKEN ? '(坏图,下一张应恢复)' : ''}
      </Text>
      <Button
        variant="outline"
        onPress={() => setIndex(previous => (previous + 1) % GALLERY.length)}
      >
        切换图片
      </Button>
    </View>
  );
};

export { AvatarDynamicSource };

判定"src 变没变"用的是序列化后的值而不是引用,因此写成 src={{ uri }} 这类内联字面量也不会因父组件重渲染而闪一次占位。

头像组

AvatarGroup 把子 Avatar 横向叠压排列,并为相邻头像补一圈与页面底色同色的描边,让重叠的圆形分得开。

AvatarGroupBasic.tsx
Loading…
import { Avatar, AvatarGroup } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 固定 seed,避免每次刷新拿到不同的示例图片 */
const FACES = [
  'https://picsum.photos/seed/av1/100',
  'https://picsum.photos/seed/av2/100',
  'https://picsum.photos/seed/av3/100',
  'https://picsum.photos/seed/av4/100',
  'https://picsum.photos/seed/av5/100'
];

/** DNS 解析失败的地址,用于稳定触发 fallback */
const BROKEN = 'https://invalid-url.test/broken.jpg';

const AvatarGroupBasic = () => {
  return (
    <View className="items-start gap-5 bg-background p-4">
      <AvatarGroup>
        {FACES.map(face => (
          <Avatar
            key={face}
            src={face}
          />
        ))}
      </AvatarGroup>
      <AvatarGroup>
        <Avatar src={FACES[0]} />
        <Avatar
          classNames={{ fallback: 'bg-primary', fallbackText: 'text-primary-foreground' }}
          fallback="张"
        />
        <Avatar
          fallback="坏"
          src={BROKEN}
        />
        <Avatar src={FACES[3]} />
      </AvatarGroup>
    </View>
  );
};

export { AvatarGroupBasic };

叠压靠逐项包一层 View 施加负 margin(RN 没有 > * + * 选择器,space-x-* 不可用),首项会跳过负 margin,否则整组会整体左移半个头像。

组尺寸

组级 size 通过 Context 下发给子 Avatar,同时决定叠压幅度与描边宽度;子项显式传 size 时以子项为准。

尺寸叠压负 margin描边宽度
xs61
sm82
md10(默认)2
lg122
xl142
2xl162
AvatarGroupSize.tsx
Loading…
import { Avatar, AvatarGroup, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 固定 seed,避免每次刷新拿到不同的示例图片 */
const FACES = [
  'https://picsum.photos/seed/av1/100',
  'https://picsum.photos/seed/av2/100',
  'https://picsum.photos/seed/av3/100',
  'https://picsum.photos/seed/av4/100'
];

const AvatarGroupSize = () => {
  return (
    <View className="items-start gap-4 bg-background p-4">
      {(['sm', 'md', 'lg'] as const).map(size => (
        <View
          className="flex-row items-center gap-3"
          key={size}
        >
          <AvatarGroup size={size}>
            {FACES.map(face => (
              <Avatar
                key={face}
                src={face}
              />
            ))}
          </AvatarGroup>
          <Text className="text-xs text-muted-foreground">size={size}</Text>
        </View>
      ))}
      <View className="flex-row items-center gap-3">
        <AvatarGroup size="sm">
          <Avatar src={FACES[0]} />
          <Avatar
            size="lg"
            src={FACES[1]}
          />
          <Avatar src={FACES[2]} />
        </AvatarGroup>
        <Text className="text-xs text-muted-foreground">子项覆盖为 lg</Text>
      </View>
    </View>
  );
};

export { AvatarGroupSize };

负 margin 约取直径的 25%,叠压比例不随 size 漂移;xs 上 2px 描边占比过重,所以降到 1px。子项覆盖 size 只改自己的直径,叠压幅度仍按组级尺寸算。

数量折叠

max 限制展示几个头像,超出的折叠成一个 +N;不传或 max <= 0 表示全部展示。total 用于"后端只返回前几条、但知道总数"的场景:渲染 3 个头像配 total={20} 就会显示 +17

AvatarGroupMax.tsx
Loading…
import { Avatar, AvatarGroup, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 固定 seed,避免每次刷新拿到不同的示例图片 */
const FACES = [
  'https://picsum.photos/seed/av1/100',
  'https://picsum.photos/seed/av2/100',
  'https://picsum.photos/seed/av3/100',
  'https://picsum.photos/seed/av4/100',
  'https://picsum.photos/seed/av5/100'
];

const AvatarGroupMax = () => {
  return (
    <View className="items-start gap-4 bg-background p-4">
      <View className="flex-row items-center gap-3">
        <AvatarGroup max={2}>
          {FACES.map(face => (
            <Avatar
              key={face}
              src={face}
            />
          ))}
        </AvatarGroup>
        <Text className="text-xs text-muted-foreground">max=2</Text>
      </View>
      <View className="flex-row items-center gap-3">
        <AvatarGroup max={0}>
          {FACES.slice(0, 4).map(face => (
            <Avatar
              key={face}
              src={face}
            />
          ))}
        </AvatarGroup>
        <Text className="text-xs text-muted-foreground">max=0(全部)</Text>
      </View>
      <View className="flex-row items-center gap-3">
        <AvatarGroup total={20}>
          {FACES.slice(0, 3).map(face => (
            <Avatar
              key={face}
              src={face}
            />
          ))}
        </AvatarGroup>
        <Text className="text-xs text-muted-foreground">total=20(+17)</Text>
      </View>
    </View>
  );
};

export { AvatarGroupMax };

+N 的计算式是 (total ?? 子项数) - 实际展示数,结果小于等于 0 时不渲染溢出头像。

自定义溢出

overflowProps 透传给 +N 头像,传 fallback 可整体替换它的内容。它排除了 src——溢出头像表达的是数量,不该再挂一张图片。

AvatarGroupOverflow.tsx
Loading…
import { Avatar, AvatarGroup, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 固定 seed,避免每次刷新拿到不同的示例图片 */
const FACES = [
  'https://picsum.photos/seed/av1/100',
  'https://picsum.photos/seed/av2/100',
  'https://picsum.photos/seed/av3/100',
  'https://picsum.photos/seed/av4/100',
  'https://picsum.photos/seed/av5/100'
];

const AvatarGroupOverflow = () => {
  return (
    <View className="items-start bg-background p-4">
      <AvatarGroup
        max={3}
        overflowProps={{
          classNames: { fallback: 'bg-primary', fallbackText: 'text-primary-foreground' },
          fallback: <Text className="font-bold">•••</Text>
        }}
      >
        {FACES.map(face => (
          <Avatar
            key={face}
            src={face}
          />
        ))}
      </AvatarGroup>
    </View>
  );
};

export { AvatarGroupOverflow };

溢出头像同样在组的 Context 内,尺寸与描边自动跟随组级 size

非默认背景

描边默认取 border-background,头像组放在非页面底色的容器上(卡片、bg-muted 区块)时,用 classNames.ring 换成所在容器的颜色。

AvatarGroupRing.tsx
Loading…
import { Avatar, AvatarGroup, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

/** 固定 seed,避免每次刷新拿到不同的示例图片 */
const FACES = [
  'https://picsum.photos/seed/av1/100',
  'https://picsum.photos/seed/av2/100',
  'https://picsum.photos/seed/av3/100',
  'https://picsum.photos/seed/av4/100',
  'https://picsum.photos/seed/av5/100'
];

const AvatarGroupRing = () => {
  return (
    <View className="gap-4 bg-muted p-4">
      <View className="flex-row items-center gap-3">
        <AvatarGroup max={4}>
          {FACES.map(face => (
            <Avatar
              key={face}
              src={face}
            />
          ))}
        </AvatarGroup>
        <Text className="text-xs text-muted-foreground">默认 ring</Text>
      </View>
      <View className="flex-row items-center gap-3">
        <AvatarGroup
          classNames={{ ring: 'border-muted' }}
          max={4}
        >
          {FACES.map(face => (
            <Avatar
              key={face}
              src={face}
            />
          ))}
        </AvatarGroup>
        <Text className="text-xs text-muted-foreground">border-muted</Text>
      </View>
    </View>
  );
};

export { AvatarGroupRing };

RN 是 border-box,描边向内吃掉图片,不会把头像撑大。

无障碍

alt 透传给底层图片作为可访问描述。注意它只在图片真正渲染时存在:进入 fallback 状态后图片节点被整体移除,alt 也随之失效,读屏器只会读到 fallback 里的文字。首字母之外还需要完整姓名时,请在外层容器上自行补 accessibilityLabel

API

Avatar

属性说明类型默认值
src图片源,为空时直接展示 fallbackImageSource-
fallbacksrc 为空或加载失败时的降级内容,string / number 自动包裹 Text 并套用 fallbackText 字号ReactNode-
size尺寸,同时决定头像直径与 fallback 字号;在 AvatarGroup 内不传则继承组级尺寸'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl''md'
alt无障碍描述,透传给底层图片string-
imageProps透传给内部 Image 的额外属性;在内部属性之后展开,可覆盖 contentFit / radius / showLoadingOmit<ImageProps, 'alt' | 'className' | 'classNames' | 'errorSlot' | 'src'>-
className根节点类名,合并到变体样式之后string-
classNames各 slot 的类名覆盖SlotClassNames<'fallback' | 'fallbackText' | 'image' | 'root'>-

AvatarGroup

属性说明类型默认值
children组内的 Avatar 子项ReactNode-
size组级尺寸,经 Context 下发给子 Avatar,并决定叠压幅度与描边宽度'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl''md'
max最多展示几个头像,超出部分折叠成一个 +N;不传或小于等于 0 表示全部展示number-
total参与计数的总人数,默认取 children 的数量,用于只渲染前几个却声明真实总数的场景number-
overflowProps透传给 +N 头像的属性,传 fallback 可整体替换 +N 的内容Omit<AvatarProps, 'src'>-
className根节点类名,合并到变体样式之后string-
classNames各 slot 的类名覆盖:item 是叠压负 margin,ring 是子头像之间的分隔描边SlotClassNames<'item' | 'ring' | 'root'>-

类型

import type {
  AvatarGroupContextValue,
  AvatarGroupProps,
  AvatarGroupSlots,
  AvatarProps,
  AvatarSize,
  AvatarSlots
} from '@skyroc/native-ui';

AvatarSize

头像尺寸,同时决定直径、fallback 字号,以及组内的叠压幅度与描边宽度。

'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'

AvatarSlots

Avatar 可通过 classNames 覆盖的 slot 名称。

'fallback' | 'fallbackText' | 'image' | 'root'

AvatarGroupSlots

AvatarGroup 可通过 classNames 覆盖的 slot 名称。

'item' | 'ring' | 'root'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 AvatarSlots / AvatarGroupSlots。

Partial<Record<Slots, string>>

AvatarGroupContextValue

AvatarGroup 经 Context 下发给子 Avatar 的共享配置,通常不需要直接使用。

字段类型说明
sizeAvatarSize组内统一尺寸,子 Avatar 显式传 size 时以子项为准。
ringClassNamestring叠压时的分隔描边类名,合并到子 Avatar 的根节点。