# Skeleton 骨架屏
## 概述
骨架屏(Skeleton)在数据加载完成前展示页面的大致结构,给予用户「数据正在加载」的感知。它用灰色占位块模拟真实内容的布局,避免页面在加载过程中呈现空白,有效减少用户的等待焦虑。
AtomUI 的 Skeleton 控件完整实现了 [Ant Design 5.0 Skeleton](https://ant.design/components/skeleton-cn) 的设计规范,在 .NET / Avalonia 平台上提供与 Ant Design React 版一致的视觉体验和流光动画效果。
---
## 设计原理
### Ant Design 的骨架屏设计哲学
Ant Design 对骨架屏的定位是:**「在需要等待加载内容的位置提供一个占位图形组合」**。骨架屏的核心价值不是替代 Loading Spinner,而是提供内容结构的**预期暗示**——让用户在数据到达之前就能预判页面布局,降低认知负荷。
**两种使用模式:**
| 模式 | 设计意图 | 典型用途 |
|---|---|---|
| **组合骨架屏(Skeleton)** | 整体占位,模拟头像 + 标题 + 段落的典型内容结构。加载完成后切换为真实内容 | 列表项加载、卡片内容加载、文章详情初始化 |
| **独立元素骨架屏** | 单个 UI 元素占位,模拟按钮/输入框/头像/图片的外形。通常嵌入自定义布局 | 表单加载、操作栏加载、头像列表加载 |
**独立元素类型:**
| 元素 | 对应控件 | 模拟对象 |
|---|---|---|
| **按钮骨架** | `SkeletonButton` | 按钮占位(支持方形/圆形/胶囊形) |
| **头像骨架** | `SkeletonAvatar` | 头像占位(支持圆形/方形) |
| **输入框骨架** | `SkeletonInput` | 输入框占位 |
| **图片骨架** | `SkeletonImage` | 图片占位(内嵌图片图标) |
| **自定义骨架** | `SkeletonNode` | 自定义内容占位(可放入任意子元素) |
**流光动画:**
当 `IsActive=True` 时,骨架块呈现从左到右的渐变流光效果(shimmer),暗示数据正在加载中。动画通过三阶段渐变色(Start → Middle → End)的无限循环实现。
### Avalonia 基础
Skeleton 家族的所有控件均继承自 Avalonia 的 `TemplatedControl`,不依赖 Avalonia 内置的 Skeleton 控件(Avalonia 没有内置骨架屏组件)。所有骨架屏控件均为 AtomUI 从零构建。
### AtomUI 的架构设计
AtomUI Skeleton 采用**控件族**设计模式,通过抽象基类和继承体系实现代码复用:
| 设计决策 | 实现方式 | 设计动机 |
|---|---|---|
| **统一动画基础** | `AbstractSkeleton` 抽象基类 | 所有骨架控件共享流光动画逻辑,避免重复实现 |
| **动画同步** | `Follow/UnFollow` 机制 | 组合骨架屏中的子元素(头像、标题、段落)跟随父级动画节奏,确保视觉一致 |
| **组合模式** | `Skeleton` 内嵌 `SkeletonAvatar` + `SkeletonTitle` + `SkeletonParagraph` | 对齐 Ant Design 的组合骨架屏结构 |
| **独立元素基类** | `SkeletonElement` 抽象类 | `SkeletonButton` / `SkeletonInput` 共享尺寸和 Block 逻辑 |
| **内容切换** | `IsLoading` + `Content` 属性 | `Skeleton` 在加载态显示骨架,加载完成后显示真实内容 |
| **灵活尺寸** | `Dimension` 结构体(支持像素和百分比) | 标题宽度、段落行宽支持绝对值和相对值 |
| **Design Token** | `SkeletonToken` + `RegisterTokenResourceScope` | 所有视觉值从全局/组件级 Token 派生 |
---
## 功能详解
### 组合骨架屏(Skeleton)
`Skeleton` 控件是最常用的骨架屏形式,它内部组合了头像、标题和段落三个部分:
- **头像区域**(`SkeletonAvatar`):通过 `IsShowAvatar` 控制是否显示,支持 `AvatarShape`(Circle/Square)和 `AvatarSizeType`(Large/Middle/Small/Custom)
- **标题区域**(`SkeletonTitle`):通过 `IsShowTitle` 控制是否显示,宽度由 `TitleWidth` 控制(默认 50%)
- **段落区域**(`SkeletonParagraph`):通过 `IsShowParagraph` 控制是否显示,行数由 `ParagraphRows` 控制,最后一行宽度由 `ParagraphLastLineWidth` 控制
主题会根据头像、标题、段落的显示组合自动调整默认值:
- 有头像 + 有段落:标题宽度默认 50%
- 无头像 + 有段落:标题宽度默认 38%
- 无头像 + 无标题:最后一行宽度默认 61%
- 无头像 + 有标题:段落行数默认 3
### 内容切换(IsLoading)
`Skeleton` 控件支持包裹真实内容。当 `IsLoading=True` 时显示骨架占位,`IsLoading=False` 时显示 `Content` 中的真实内容:
```xml
真实内容
```
### 流光动画(IsActive)
所有骨架控件均支持 `IsActive` 属性。启用后,骨架块呈现从左到右的渐变流光效果:
- 动画使用三阶段 `LinearGradientBrush`(Start → Middle → End)
- 动画时长由 `MotionDuration` Token 控制(默认 1.4 秒)
- 使用 `CubicEaseOut` 缓动曲线
- 动画无限循环直到 `IsActive` 设为 `false` 或控件离开视觉树
### 动画同步(Follow 机制)
在组合骨架屏中,`Skeleton` 作为动画主控,子组件(`SkeletonAvatar`、`SkeletonTitle`、`SkeletonParagraph`)通过 `Follow(this)` 绑定到父级的 `AnimationLayerFill` 和 `IsActive` 属性,确保所有骨架块的流光动画保持同步。
### 独立元素骨架屏
独立元素骨架屏用于自定义布局中单独使用,每个元素各自管理动画:
| 控件 | 默认宽度 | 默认高度 | 形状支持 |
|---|---|---|---|
| `SkeletonButton` | `Height × 2` | `ControlHeight*` | Square / Round / Circle |
| `SkeletonInput` | `Height × 5` | `ControlHeight*` | — |
| `SkeletonAvatar` | `ControlHeight*` | `ControlHeight*` | Circle / Square |
| `SkeletonImage` | `ImageContainerSize` | `ImageContainerSize` | — |
| `SkeletonNode` | `ImageContainerSize` | `ImageContainerSize` | —(可放入自定义内容) |
> `*` 高度根据 `SizeType`(Large/Middle/Small)从 `ControlHeightLG`/`ControlHeight`/`ControlHeightSM` 获取。
### 圆角控制(IsRound)
`Skeleton` 和 `SkeletonParagraph` 的 `IsRound` 属性控制段落行的圆角样式:
- `false`(默认):使用 `BlockRadius`(`BorderRadiusSM`)
- `true`:使用 `ParagraphLineRoundCornerRadius`(行高的一半,形成胶囊形)
### 尺寸系统(Dimension)
`TitleWidth`、`ParagraphLastLineWidth`、`ParagraphLineWidths` 等属性使用 `Dimension` 结构体,支持两种单位:
- **百分比**:`new Dimension(50, DimensionUnitType.Percentage)` 或 AXAML 中写 `"50%"`
- **像素**:`new Dimension(200, DimensionUnitType.Pixel)` 或 AXAML 中写 `"200"`
---
## 与 Ant Design 规范的对齐程度
| 特性 | Ant Design (React) | AtomUI (Avalonia) | 对齐情况 |
|---|---|---|---|
| 基本骨架屏 | ✅ `` | ✅ `` | ✅ 完全对齐 |
| 头像 + 标题 + 段落 | ✅ `avatar` / `title` / `paragraph` | ✅ `IsShowAvatar` / `IsShowTitle` / `IsShowParagraph` | ✅ 完全对齐 |
| 流光动画 | ✅ `active` | ✅ `IsActive` | ✅ 完全对齐 |
| 内容包裹 | ✅ `loading` + `children` | ✅ `IsLoading` + `Content` | ✅ 完全对齐 |
| 段落行数 | ✅ `paragraph.rows` | ✅ `ParagraphRows` | ✅ 完全对齐 |
| 段落行宽 | ✅ `paragraph.width` | ✅ `ParagraphLineWidths` / `ParagraphLastLineWidth` | ✅ 完全对齐 |
| 标题宽度 | ✅ `title.width` | ✅ `TitleWidth` | ✅ 完全对齐 |
| 头像形状/尺寸 | ✅ `avatar.shape` / `avatar.size` | ✅ `AvatarShape` / `AvatarSizeType` / `AvatarSize` | ✅ 完全对齐 |
| 圆角模式 | ✅ `round` | ✅ `IsRound` | ✅ 完全对齐 |
| 按钮骨架 | ✅ `Skeleton.Button` | ✅ `SkeletonButton` | ✅ 完全对齐 |
| 头像骨架 | ✅ `Skeleton.Avatar` | ✅ `SkeletonAvatar` | ✅ 完全对齐 |
| 输入框骨架 | ✅ `Skeleton.Input` | ✅ `SkeletonInput` | ✅ 完全对齐 |
| 图片骨架 | ✅ `Skeleton.Image` | ✅ `SkeletonImage` | ✅ 完全对齐 |
| 自定义节点 | ✅ `Skeleton.Node` | ✅ `SkeletonNode` | ✅ 完全对齐 |
| Block 模式 | ✅ `block` | ✅ `IsBlock` | ✅ 完全对齐 |
---
## 继承关系
```
Avalonia.Controls.Primitives.TemplatedControl
└── AbstractSkeleton ← 流光动画基类(所有骨架控件共享)
├── Skeleton ← 组合骨架屏(头像 + 标题 + 段落 + 内容切换)
├── SkeletonLine ← 单行骨架线条(段落的组成单元)
│ └── SkeletonTitle ← 标题骨架(SkeletonLine 子类,默认宽度 50%)
├── SkeletonParagraph ← 段落骨架(动态生成多个 SkeletonLine)
├── SkeletonAvatar ← 头像骨架(支持圆形/方形 + 三种尺寸)
├── SkeletonElement ← 独立元素骨架基类(尺寸 + Block)
│ ├── SkeletonButton ← 按钮骨架(方形/圆形/胶囊形)
│ └── SkeletonInput ← 输入框骨架
└── SkeletonImage ← 图片骨架(内嵌图片图标)
└── SkeletonNode ← 自定义节点骨架(可放入任意内容)
```
**各层级职责划分:**
| 层级 | 提供的能力 |
|---|---|
| `TemplatedControl` | Avalonia 模板化控件基础 |
| `AbstractSkeleton` | 流光动画逻辑(`IsActive`、`MotionDuration`、动画构建/启停)、Follow/UnFollow 同步机制、动画渐变色属性 |
| `Skeleton` | 组合骨架屏:内嵌 `SkeletonAvatar` + `SkeletonTitle` + `SkeletonParagraph`,`IsLoading` + `Content` 内容切换,Token 注册 |
| `SkeletonElement` | 独立元素基类:`SizeType`(可自定义尺寸)、`IsBlock`(撑满宽度) |
| `SkeletonButton` | 按钮骨架:`Shape`(Square/Round/Circle),自动计算宽度和圆角 |
| `SkeletonInput` | 输入框骨架:默认宽度 = 高度 × 5 |
| `SkeletonAvatar` | 头像骨架:`Shape`(Circle/Square)、`Size`(自定义尺寸)、`SizeType` |
| `SkeletonLine` | 单行骨架:`LineWidth`(Dimension)、`IsRound`,由 `SkeletonParagraph` 动态创建 |
| `SkeletonTitle` | 标题骨架:继承 `SkeletonLine`,默认 `LineWidth=50%` |
| `SkeletonParagraph` | 段落骨架:动态创建 `Rows` 个 `SkeletonLine`,支持 `LastLineWidth` 和 `LineWidths` |
| `SkeletonImage` | 图片骨架:固定尺寸 + 内嵌 `ImageFilled` 图标 |
| `SkeletonNode` | 自定义节点:继承 `SkeletonImage`,支持 `Content` / `ContentTemplate` |
**实现的共享接口:**
| 接口 | 实现控件 | 定义位置 | 作用 |
|---|---|---|---|
| `ICustomizableSizeTypeAware` | `SkeletonElement`、`SkeletonAvatar` | `AtomUI.Controls.Shared` | 支持 `CustomizableSizeType`(Large/Middle/Small/Custom)四种尺寸 |
> 注意:Skeleton 系列控件全部定义在 `AtomUI.Desktop.Controls` 中,不存在 `AtomUI.Controls` 基类层的设备无关抽象。这是因为骨架屏的渲染和动画行为与桌面平台紧密相关。
---
## 源码位置
| 层级 | 文件路径 | 说明 |
|---|---|---|
| 动画基类 | `src/AtomUI.Desktop.Controls/Skeleton/AbstractSkeleton.cs` | 流光动画逻辑、Follow 机制 |
| 组合骨架 | `src/AtomUI.Desktop.Controls/Skeleton/Skeleton.cs` | 主骨架屏控件 |
| 元素基类 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonElement.cs` | 独立元素基类(SizeType + IsBlock) |
| 按钮骨架 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonButton.cs` | 按钮占位 |
| 输入框骨架 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonInput.cs` | 输入框占位 |
| 头像骨架 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonAvatar.cs` | 头像占位 |
| 行骨架 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonLine.cs` | 单行占位线条 |
| 标题骨架 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonTitle.cs` | 标题占位 |
| 段落骨架 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonParagraph.cs` | 段落占位(多行) |
| 图片骨架 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonImage.cs` | 图片占位 |
| 节点骨架 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonNode.cs` | 自定义节点占位 |
| Token 定义 | `src/AtomUI.Desktop.Controls/Skeleton/SkeletonToken.cs` | 组件级 Design Token |
| 主题模板 | `src/AtomUI.Desktop.Controls/Skeleton/Themes/SkeletonTheme.axaml` | Skeleton ControlTheme |
| 抽象主题 | `src/AtomUI.Desktop.Controls/Skeleton/Themes/AbstractSkeletonTheme.axaml` | 通用骨架主题基类 |
| 主题常量 | `src/AtomUI.Desktop.Controls/Skeleton/Themes/SkeletonThemeConstants.cs` | 模板部件名称常量 |
| Gallery 示例 | `controlgallery/AtomUIGallery/ShowCases/Views/Feedback/SkeletonShowCase.axaml` | 使用范例 |
---
## 模板结构
### Skeleton(组合骨架屏)
```
Panel
├── DockPanel (IsVisible = !IsContentVisible) ← 骨架占位层
│ ├── SkeletonAvatar#PART_Avatar (DockPanel.Dock=Left) ← 头像区域
│ └── StackPanel#PART_Content (Orientation=Vertical) ← 内容区域
│ ├── SkeletonTitle#PART_Title ← 标题行
│ └── SkeletonParagraph#PART_Paragraph ← 段落区域
└── ContentPresenter (IsVisible = IsContentVisible) ← 真实内容层
```
### AbstractSkeleton(通用骨架基础模板)
```
Panel#PART_RootLayout
├── Border#PART_ActiveAnimationLayer ← 流光动画层(IsActive 时可见)
│ Background = AnimationLayerFill ← 渐变画刷(动画驱动)
└── Border#PART_ContentLayer ← 静态背景层(!IsActive 时可见)
Background = GradientFromColor ← 纯色填充
```
### 模板部件常量
| 常量 | 值 | 所属 | 说明 |
|---|---|---|---|
| `AbstractSkeletonThemeConstants.RootLayoutPart` | `"PART_RootLayout"` | 通用基础 | 根布局面板 |
| `AbstractSkeletonThemeConstants.ActiveAnimationLayerPart` | `"PART_ActiveAnimationLayer"` | 通用基础 | 流光动画层 |
| `AbstractSkeletonThemeConstants.ContentLayerPart` | `"PART_ContentLayer"` | 通用基础 | 静态背景层 |
| `SkeletonThemeConstants.AvatarPart` | `"PART_Avatar"` | Skeleton | 头像组件 |
| `SkeletonThemeConstants.TitlePart` | `"PART_Title"` | Skeleton | 标题组件 |
| `SkeletonThemeConstants.ContentPart` | `"PART_Content"` | Skeleton | 内容面板 |
| `SkeletonThemeConstants.ParagraphPart` | `"PART_Paragraph"` | Skeleton | 段落组件 |
| `SkeletonParagraphThemeConstants.LineLayoutPart` | `"PART_LineLayout"` | SkeletonParagraph | 行布局面板 |