feat.网速显示组件引入了一套更好的等距。

This commit is contained in:
lincube
2026-04-15 15:42:11 +08:00
parent c2cc62b58b
commit 03e32ee6cb
3 changed files with 21 additions and 131 deletions

View File

@@ -1,117 +0,0 @@
# 骨架页Skeleton Screen实施计划
## 问题分析
当前首次启动时,用户看到的现象:
1. 全屏窗口立即显示,但状态栏组件全部 `IsVisible="False"`(空白)
2. 头像区域只有 fallback 文字 "U",尺寸未计算(显得巨大)
3. 底部 Dock 任务栏已经渲染但内容为空
4. 壁纸加载完成前,桌面区域是透明/黑色的
5. 整体看起来就是一个"Dock 栏覆盖全屏 + 巨大头像"的半成品状态
**根本原因**`OnOpened` 中有大量同步初始化操作壁纸解码、组件布局、启动器扫描等在它们完成之前UI 元素要么不可见要么处于默认状态。
## 方案概述
`DesktopPage` 层添加一个**骨架遮罩层**,覆盖在真实内容之上,在初始化完成前显示骨架占位,初始化完成后淡出消失。
### 骨架页布局
```
┌──────────────────────────────────────────────────────────────┐
│ [状态栏骨架] │
│ ┌─────────┐ ┌──────────────────┐ ┌─────────┐ │
│ │ ○ 头像 │ │ ████ 时钟 ████ │ │ ○ ○ │ │
│ └─────────┘ └──────────────────┘ └─────────┘ │
│ │
│ (桌面区域 - 壁纸层) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ [Dock 任务栏骨架] │ │
│ │ ██ ████████ ████████ ████████ ████████ ○ │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
### 骨架元素
| 区域 | 骨架元素 | 形状 | 说明 |
| ------- | ------ | ----------------------------------- | ----------------------------- |
| 状态栏-中间 | 时钟骨架 | 圆角矩形(`DesignCornerRadiusComponent` | 模拟 ClockWidget 的胶囊形状 |
| 状态栏-中间 | 文本胶囊骨架 | 圆角矩形(较小) | 模拟 TextCapsuleWidget |
| 底部 Dock | 头像骨架 | 圆形 | 模拟 TaskbarProfileAvatarBorder |
| 底部 Dock | 操作按钮骨架 | 圆角矩形 | 模拟任务栏按钮 |
| 底部 Dock | 分隔线骨架 | 细长矩形 | 模拟按钮间分隔 |
### 骨架样式
* **颜色**:使用 `AdaptiveGlassPanelBackgroundBrush` 作为基础色,叠加一个 **Shimmer 动画**(微光扫过效果)
* **圆角**:与真实组件一致,使用 `DesignCornerRadiusComponent`
* **动画**Shimmer 微光从左到右扫过,周期 2s使用 `FluttermotionToken` 缓动
## 实施步骤
### Step 1: 创建 Shimmer 动画画刷
`GlassModule.axaml` 或新建 `SkeletonStyles.axaml` 中定义:
* 创建 `ShimmerBrush`:一个 `LinearGradientBrush`,包含高光条带
* 创建 `ShimmerAnimation` storyboard让高光条带从左到右移动
* 定义 `skeleton-shimmer` 样式类:应用 ShimmerBrush + 动画
### Step 2: 在 MainWindow\.axaml 中添加骨架遮罩层
`DesktopPage` Grid 内、所有真实内容之上添加一个 `Grid x:Name="SkeletonOverlay"`
* 初始 `IsVisible="True"``ZIndex="999"`
* 包含状态栏骨架区域和底部 Dock 骨架区域
* 使用与真实布局相同的 Grid RowDefinitions确保骨架元素对齐
### Step 3: 在 MainWindow\.axaml.cs 中控制骨架显示/隐藏
*`OnOpened` 开始时,骨架层可见
*`OnOpened` 末尾(所有初始化完成后),调用 `HideSkeletonOverlayAsync()`
* `HideSkeletonOverlayAsync()`:播放淡出动画 → 设置 `IsVisible="False"`
* 如果启用了滑入滑出过渡,骨架层在入场动画期间也应可见,入场动画完成后再淡出
### Step 4: 骨架元素尺寸适配
* 骨架元素需要在 `ApplyTaskbarSettings()` 后更新尺寸(因为 `taskbarCellHeight` 等值在 OnOpened 中才计算)
* 或者在 XAML 中使用相对尺寸(百分比/比例),避免依赖代码计算
### Step 5: 与窗口过渡动画的协调
* 入场动画(`PrepareEnterAnimation` / `PlayEnterAnimation`)期间,骨架层应保持可见
* 入场动画完成后,先短暂显示骨架(\~100ms然后淡出骨架
* 退场动画时,无需特殊处理(骨架已隐藏)
## 涉及文件
| 文件 | 改动类型 |
| --------------------------------- | -------------------- |
| `Styles/SkeletonStyles.axaml`(新建) | Shimmer 画刷 + 骨架样式类 |
| `Views/MainWindow.axaml` | 添加 SkeletonOverlay 层 |
| `Views/MainWindow.axaml.cs` | 骨架显示/隐藏逻辑 |
| `App.axaml` | 引入 SkeletonStyles 资源 |
## 不涉及的文件
* 不修改组件代码ClockWidget、TextCapsuleWidget 等)
* 不修改设置系统
* 不修改 App.axaml.cs 的启动流程

View File

@@ -30,7 +30,10 @@
FontWeight="SemiBold"
Margin="2,0,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"/>
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
FontFamily="Consolas, Courier New, monospace"
MinWidth="42"
TextAlignment="Right"/>
</StackPanel>
<!-- 分隔符 -->
@@ -55,7 +58,10 @@
FontWeight="SemiBold"
Margin="2,0,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"/>
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
FontFamily="Consolas, Courier New, monospace"
MinWidth="42"
TextAlignment="Right"/>
</StackPanel>
<!-- 网络类型图标 -->

View File

@@ -317,31 +317,32 @@ public partial class NetworkSpeedWidget : UserControl, IDesktopComponentWidget
private static string FormatSpeed(long bytesPerSecond)
{
// 根据数值大小决定显示格式始终保持3个字符宽度
// 例如: 1.23, 12.3, 123
// 根据数值大小选择合适的单位确保显示始终在1-99.9范围内
// 当数值达到100时自动切换到更大的单位
return bytesPerSecond switch
{
>= 1024 * 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0 * 1024.0), "G"),
>= 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0), "M"),
>= 1024 => FormatWithThreeDigits(bytesPerSecond / 1024.0, "K"),
>= 100L * 1024 * 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0 * 1024.0), "G"),
>= 100L * 1024 * 1024 => FormatWithThreeDigits(bytesPerSecond / (1024.0 * 1024.0), "M"),
>= 100L * 1024 => FormatWithThreeDigits(bytesPerSecond / 1024.0, "K"),
>= 100 => FormatWithThreeDigits(bytesPerSecond / 1024.0, "K"), // 100B+ 显示为 0.1K
_ => FormatWithThreeDigits(bytesPerSecond, "B")
};
}
/// <summary>
/// 格式化数字始终保持3个有效数字的显示宽度
/// 格式化数字始终保持3位数字+小数点,确保宽度恒定
/// 数值范围始终在1-99.9之间
/// </summary>
private static string FormatWithThreeDigits(double value, string unit)
{
// 根据数值大小决定小数位数,确保总宽度一致
// 始终保持3位数字小数点始终存在
// 数值范围: 0.0 - 99.9
// < 10: 显示两位小数 (如 1.23)
// 10-99: 显示一位小数 (如 12.3)
// >= 100: 显示整数 (如 123)
// >= 10: 显示一位小数 (如 12.3, 99.9)
string formatted = value switch
{
< 10 => $"{value:F2}",
< 100 => $"{value:F1}",
_ => $"{value:F0}"
< 10 => $"{value:F2}", // 1.23
_ => $"{value:F1}" // 12.3, 99.9
};
return formatted + unit;