feat.淡入淡出动画。

This commit is contained in:
lincube
2026-04-15 10:49:04 +08:00
parent 9c529f2992
commit c2cc62b58b
11 changed files with 498 additions and 4 deletions

View File

@@ -0,0 +1,117 @@
# 骨架页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

@@ -0,0 +1,24 @@
* [x] AppSettingsSnapshot 包含 EnableSlideTransition 字段且默认为 false
* [x] DesktopPage 拥有名为 DesktopPageSlideTransform 的 TranslateTransform
* [x] DesktopPage.Transitions 包含 Opacity 和 TranslateTransform.X 两个 DoubleTransition
* [x] 点击"回到 Windows"时播放退场动画Opacity 淡出 或 Opacity+滑动),动画完成后再最小化
* [x] 从最小化恢复时 DesktopPage 先以 Opacity=0 遮住 Normal 中间态FullScreen 生效后播放入场动画
* [x] 动画期间 DesktopPage.IsHitTestVisible 为 false动画完成后恢复
* [x] 动画期间 OnWindowPropertyChanged 不执行强制全屏纠正
* [x] 快速连续操作不会导致动画冲突
* [x] GeneralSettingsPage 在 Windows 平台显示"滑入滑出过渡效果"开关
* [x] GeneralSettingsPage 在非 Windows 平台不显示该开关
* [x] EnableSlideTransition 设置持久化到 AppSettingsSnapshot 且立即生效
* [x] dotnet build 无编译错误

View File

@@ -0,0 +1,138 @@
# 窗口过渡动画 Spec
## Why
当前全屏窗口在"回到 Windows"(最小化)和"恢复应用"时存在严重的视觉问题:
1. 恢复时经历 `Minimized → Normal → FullScreen` 两步跳变,用户会短暂看到无框小窗口
2. 状态切换无任何过渡动画,体验生硬
3. `OnWindowPropertyChanged` 使用 `Dispatcher.UIThread.Post` 延迟纠正,进一步延长了 Normal 中间态的可见时间
## What Changes
-`MainWindow.axaml``DesktopPage` 上添加 `TranslateTransform``TranslateTransform.X` 过渡动画
- 修改 `MainWindow.axaml.cs``OnMinimizeClick`,实现退场动画(滑出/淡出 → 最小化)
- 修改 `App.axaml.cs``RestoreOrCreateMainWindow`,实现入场动画(全屏 → 滑入/淡入)
- 修改 `MainWindow.axaml.cs``OnWindowPropertyChanged`,在动画期间暂停强制全屏逻辑
-`AppSettingsSnapshot` 中添加 `EnableSlideTransition` 设置项(默认关闭)
-`GeneralSettingsPageViewModel` 中添加对应 ViewModel 属性
-`GeneralSettingsPage.axaml` 中添加开关 UI仅 Windows 平台显示)
- 添加平台检测逻辑Windows 且开启设置时使用滑入滑出,其他情况使用 Opacity 淡入淡出
## Impact
- Affected specs: 窗口生命周期过渡动画
- Affected code:
- `LanMountainDesktop/Views/MainWindow.axaml` - DesktopPage 添加 TranslateTransform
- `LanMountainDesktop/Views/MainWindow.axaml.cs` - OnMinimizeClick、OnWindowPropertyChanged、新增动画方法
- `LanMountainDesktop/App.axaml.cs` - RestoreOrCreateMainWindow、OnMainWindowPropertyChanged
- `LanMountainDesktop/Models/AppSettingsSnapshot.cs` - 新增 EnableSlideTransition 字段
- `LanMountainDesktop/ViewModels/SettingsViewModels.cs` - GeneralSettingsPageViewModel 新增属性
- `LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml` - 新增开关 UI
---
## ADDED Requirements
### Requirement: 窗口退场过渡动画
系统 SHALL 在主窗口最小化/隐藏时播放退场过渡动画,消除窗口状态跳变的视觉闪烁。
#### Scenario: Opacity 淡出退场(所有平台默认)
- **WHEN** 用户点击"回到 Windows"或触发最小化
- **THEN** 系统将 `DesktopPage.Opacity` 设为 0触发淡出动画
- **AND THEN** 动画完成后执行 `WindowState = Minimized`
- **AND THEN** 最小化完成后重置 `DesktopPage.Opacity = 1`(窗口已不可见)
#### Scenario: 滑出退场Windows + 开启设置)
- **WHEN** 用户点击"回到 Windows"且运行在 Windows 平台且已开启滑入滑出设置
- **THEN** 系统同时将 `DesktopPage.Opacity` 设为 0 且 `DesktopPageSlideTransform.X` 设为屏幕宽度
- **AND THEN** 动画完成后执行 `WindowState = Minimized`
- **AND THEN** 最小化完成后重置 `DesktopPageSlideTransform.X = 0``DesktopPage.Opacity = 1`
### Requirement: 窗口入场过渡动画
系统 SHALL 在主窗口恢复时播放入场过渡动画,消除 Normal 中间态的视觉闪烁。
#### Scenario: Opacity 淡入入场(所有平台默认)
- **WHEN** 主窗口从最小化/隐藏状态恢复
- **THEN** 系统先将 `DesktopPage.Opacity` 设为 0遮住 Normal 中间态)
- **AND THEN** 完成 `Minimized → Normal → FullScreen` 状态切换
- **AND THEN** 等 FullScreen 状态生效后将 `DesktopPage.Opacity` 设为 1触发淡入动画
#### Scenario: 滑入入场Windows + 开启设置)
- **WHEN** 主窗口从最小化/隐藏状态恢复且运行在 Windows 平台且已开启滑入滑出设置
- **THEN** 系统先将 `DesktopPage.Opacity` 设为 0 且 `DesktopPageSlideTransform.X` 设为屏幕宽度
- **AND THEN** 完成 `Minimized → Normal → FullScreen` 状态切换
- **AND THEN** 等 FullScreen 状态生效后同时将 `DesktopPage.Opacity` 设为 1 且 `DesktopPageSlideTransform.X` 设为 0触发滑入+淡入组合动画
### Requirement: 动画期间交互保护
系统 SHALL 在过渡动画播放期间防止用户交互和状态冲突。
#### Scenario: 动画期间禁止交互
- **WHEN** 退场或入场动画正在播放
- **THEN** `DesktopPage.IsHitTestVisible` 设为 `false`
- **AND THEN** 动画完成后恢复为 `true`
#### Scenario: 动画期间暂停强制全屏
- **WHEN** 入场动画正在播放且窗口临时处于 Normal 状态
- **THEN** `OnWindowPropertyChanged` 不执行强制全屏纠正
- **AND THEN** 入场动画完成后恢复正常强制全屏逻辑
#### Scenario: 防止快速连续操作
- **WHEN** 用户在动画播放期间再次触发最小化或恢复
- **THEN** 系统忽略重复操作,避免动画冲突
### Requirement: 滑入滑出设置项
系统 SHALL 在基本设置页面提供"滑入滑出过渡效果"开关,仅 Windows 平台可见。
#### Scenario: 设置项可见性
- **WHEN** 用户在 Windows 平台打开基本设置页面
- **THEN** 显示"滑入滑出过渡效果"开关
- **WHEN** 用户在非 Windows 平台打开基本设置页面
- **THEN** 不显示该开关
#### Scenario: 设置项默认值
- **WHEN** 用户首次安装应用
- **THEN** `EnableSlideTransition` 默认为 `false`
#### Scenario: 设置持久化
- **WHEN** 用户切换"滑入滑出过渡效果"开关
- **THEN** 设置值立即持久化到 `AppSettingsSnapshot.EnableSlideTransition`
- **AND THEN** 下次窗口过渡时立即生效,无需重启
### Requirement: DesktopPage TranslateTransform 声明
系统 SHALL 在 `DesktopPage` 上声明 `TranslateTransform` 和对应的过渡动画。
#### Scenario: XAML 声明
- **WHEN** MainWindow 初始化
- **THEN** `DesktopPage` 拥有名为 `DesktopPageSlideTransform``TranslateTransform`
- **AND THEN** `DesktopPage.Transitions` 包含 `Opacity``TranslateTransform.X` 两个过渡
- **AND THEN** 过渡时长使用 `FluttermotionToken.Duration.Page`320ms`FluttermotionToken.Duration.Intro`400ms
- **AND THEN** 缓动函数使用 `0.05,0.75,0.10,1.00`DecelerateBezier
## MODIFIED Requirements
### Requirement: OnMinimizeClick 行为
**当前**: 直接设置 `WindowState = WindowState.Minimized`,无动画
**修改后**: 先播放退场动画,动画完成后再设置 `WindowState = WindowState.Minimized`
### Requirement: RestoreOrCreateMainWindow 行为
**当前**: `Show() → Normal → FullScreen`,无过渡动画,用户可见 Normal 中间态
**修改后**: 先将 `DesktopPage` 设为不可见Opacity=0 + 可选滑出位),再执行状态切换,最后播放入场动画
### Requirement: OnWindowPropertyChanged 强制全屏逻辑
**当前**: 任何非 Minimized/FullScreen 状态立即纠正为 FullScreen
**修改后**: 动画期间允许临时 Normal 状态存在,动画完成后恢复强制全屏逻辑
## REMOVED Requirements
无移除的需求。

View File

@@ -0,0 +1,52 @@
# Tasks
- [x] Task 1: 在 `AppSettingsSnapshot` 中添加 `EnableSlideTransition` 字段
- [x] 添加 `public bool EnableSlideTransition { get; set; } = false;`
- [x]`Clone()` 方法中无需特殊处理bool 是值类型)
- [x] Task 2: 在 `MainWindow.axaml``DesktopPage` 上添加 `TranslateTransform` 和过渡动画
- [x] 添加 `<TranslateTransform />`
- [x]`Grid.Transitions` 中添加 `TranslateTransform.X``DoubleTransition`,使用 `FluttermotionToken.Duration.Intro` 和 DecelerateBezier 缓动
- [x] Task 3: 在 `MainWindow.axaml.cs` 中实现退场动画逻辑
- [x] 添加 `_isSlideAnimationActive` 标志位
- [x] 修改 `OnMinimizeClick`,调用新的 `SlideOutAndMinimizeAsync` 方法
- [x] 实现 `SlideOutAndMinimizeAsync`:读取设置 → 播放退场动画Opacity + 可选滑动)→ 等动画完成 → 最小化 → 重置位置
- [x] 动画期间设置 `DesktopPage.IsHitTestVisible = false`
- [x] Task 4: 在 `MainWindow.axaml.cs` 中实现入场动画逻辑
- [x] 添加 `public void PrepareEnterAnimation()` 方法:禁用过渡 → 设置初始位置Opacity=0, X=屏幕宽度或0→ 重新启用过渡
- [x] 添加 `public void PlayEnterAnimation()` 方法触发入场动画Opacity=1, X=0
- [x] 添加 `private bool IsSlideTransitionEnabled()` 方法,从设置中读取
- [x] Task 5: 修改 `App.axaml.cs``RestoreOrCreateMainWindow`
- [x] 在窗口状态切换前调用 `mainWindow.PrepareEnterAnimation()`
- [x] 在 FullScreen 状态生效后调用 `mainWindow.PlayEnterAnimation()`
- [x] Task 6: 修改 `MainWindow.axaml.cs``OnWindowPropertyChanged`
- [x]`_isSlideAnimationActive` 为 true 时跳过强制全屏逻辑
- [x] Task 7: 在 `GeneralSettingsPageViewModel` 中添加 `EnableSlideTransition` 属性
- [x] 添加 `[ObservableProperty] private bool _enableSlideTransition;`
- [x] 添加 `OnEnableSlideTransitionChanged` 持久化方法
- [x] 在构造函数和 `OnSettingsChanged` 中加载/同步该设置
- [x] 添加 `IsSlideTransitionAvailable` 平台检测属性
- [x] Task 8: 在 `GeneralSettingsPage.axaml` 中添加"滑入滑出过渡效果"开关
- [x] 在"运行时设置"分组中添加 `SettingsExpander`
- [x] 仅 Windows 平台显示(使用 `IsVisible` 绑定到 `IsSlideTransitionAvailable`
- [x] 图标使用 `ArrowRight`
- [x] Task 9: 构建验证
- [x] 执行 `dotnet build` 确保无编译错误
# Task Dependencies
- [Task 2] depends on [Task 1]
- [Task 3] depends on [Task 1, Task 2]
- [Task 4] depends on [Task 1, Task 2]
- [Task 5] depends on [Task 4]
- [Task 6] depends on [Task 3]
- [Task 7] depends on [Task 1]
- [Task 8] depends on [Task 7]
- [Task 9] depends on [Task 3, Task 4, Task 5, Task 6, Task 7, Task 8]

View File

@@ -566,13 +566,14 @@ public partial class App : Application
try try
{ {
// 先隐藏透明覆盖层窗口
if (_transparentOverlayWindow is not null && _transparentOverlayWindow.IsVisible) if (_transparentOverlayWindow is not null && _transparentOverlayWindow.IsVisible)
{ {
_transparentOverlayWindow.Hide(); _transparentOverlayWindow.Hide();
} }
var mainWindow = GetOrCreateMainWindow(desktop, source); var mainWindow = GetOrCreateMainWindow(desktop, source);
mainWindow.PrepareEnterAnimation();
mainWindow.ShowInTaskbar = true; mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible) if (!mainWindow.IsVisible)
@@ -593,6 +594,12 @@ public partial class App : Application
mainWindow.Activate(); mainWindow.Activate();
mainWindow.Topmost = true; mainWindow.Topmost = true;
mainWindow.Topmost = false; mainWindow.Topmost = false;
Dispatcher.UIThread.Post(() =>
{
mainWindow.PlayEnterAnimation();
}, DispatcherPriority.Background);
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}"); SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
AppLogger.Info( AppLogger.Info(
"DesktopShell", "DesktopShell",

View File

@@ -152,6 +152,8 @@ public sealed class AppSettingsSnapshot
public bool EnableThreeFingerSwipe { get; set; } = false; public bool EnableThreeFingerSwipe { get; set; } = false;
public bool EnableSlideTransition { get; set; } = false;
public bool EnableFusedDesktop { get; set; } = false; public bool EnableFusedDesktop { get; set; } = false;
public List<string> DisabledPluginIds { get; set; } = []; public List<string> DisabledPluginIds { get; set; } = [];

View File

@@ -201,6 +201,7 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
SelectedRenderMode = RenderModes.FirstOrDefault(option => SelectedRenderMode = RenderModes.FirstOrDefault(option =>
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase)) string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
?? RenderModes[0]; ?? RenderModes[0];
EnableSlideTransition = appSnapshot.EnableSlideTransition;
_isInitializing = false; _isInitializing = false;
RefreshPreview(); RefreshPreview();
@@ -232,6 +233,11 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
{ {
return; return;
} }
if (changedKeys.Contains(nameof(AppSettingsSnapshot.EnableSlideTransition)))
{
EnableSlideTransition = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).EnableSlideTransition;
}
} }
public event Action? RestartRequested; public event Action? RestartRequested;
@@ -251,6 +257,11 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
[ObservableProperty] [ObservableProperty]
private SelectionOption _selectedRenderMode = new(AppRenderingModeHelper.Default, "Default"); private SelectionOption _selectedRenderMode = new(AppRenderingModeHelper.Default, "Default");
[ObservableProperty]
private bool _enableSlideTransition;
public bool IsSlideTransitionAvailable => System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
[ObservableProperty] [ObservableProperty]
private string _pageTitle = string.Empty; private string _pageTitle = string.Empty;
@@ -350,6 +361,24 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
} }
} }
partial void OnEnableSlideTransitionChanged(bool value)
{
if (_isInitializing) return;
SaveField(nameof(AppSettingsSnapshot.EnableSlideTransition), value);
}
private void SaveField<T>(string key, T value)
{
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var property = typeof(AppSettingsSnapshot).GetProperty(key);
if (property is not null && property.CanWrite)
{
property.SetValue(snapshot, value);
}
_settingsFacade.Settings.SaveSnapshot(SettingsScope.App, snapshot, changedKeys: [key]);
}
private IReadOnlyList<SelectionOption> CreateLanguageOptions() private IReadOnlyList<SelectionOption> CreateLanguageOptions()
{ {
return return

View File

@@ -77,7 +77,9 @@ public partial class MainWindow
string.Equals(key, nameof(AppSettingsSnapshot.UpdateChannel), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.UpdateChannel), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateMode), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.UpdateMode), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadSource), StringComparison.OrdinalIgnoreCase) || string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadSource), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadThreads), StringComparison.OrdinalIgnoreCase))) string.Equals(key, nameof(AppSettingsSnapshot.UpdateDownloadThreads), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.EnableThreeFingerSwipe), StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, nameof(AppSettingsSnapshot.EnableSlideTransition), StringComparison.OrdinalIgnoreCase)))
{ {
return; return;
} }

View File

@@ -98,9 +98,13 @@
<Grid x:Name="DesktopPage" <Grid x:Name="DesktopPage"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<Grid.RenderTransform>
<TranslateTransform />
</Grid.RenderTransform>
<Grid.Transitions> <Grid.Transitions>
<Transitions> <Transitions>
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" /> <DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Page}" Easing="0.05,0.75,0.10,1.00" />
<DoubleTransition Property="TranslateTransform.X" Duration="{StaticResource FluttermotionToken.Duration.Intro}" Easing="0.05,0.75,0.10,1.00" />
</Transitions> </Transitions>
</Grid.Transitions> </Grid.Transitions>

View File

@@ -132,6 +132,8 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private double _currentDesktopCellGap; private double _currentDesktopCellGap;
private double _currentDesktopEdgeInset; private double _currentDesktopEdgeInset;
private string _gridSpacingPreset = "Relaxed"; private string _gridSpacingPreset = "Relaxed";
private bool _isSlideAnimationActive;
private TranslateTransform? _desktopPageSlideTransform;
private string _statusBarSpacingMode = "Relaxed"; private string _statusBarSpacingMode = "Relaxed";
private int _statusBarCustomSpacingPercent = 12; private int _statusBarCustomSpacingPercent = 12;
private bool _statusBarClockTransparentBackground; private bool _statusBarClockTransparentBackground;
@@ -833,7 +835,103 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
private void OnMinimizeClick(object? sender, RoutedEventArgs e) private void OnMinimizeClick(object? sender, RoutedEventArgs e)
{ {
if (_isSlideAnimationActive)
{
return;
}
SlideOutAndMinimizeAsync();
}
private TranslateTransform GetDesktopPageSlideTransform()
{
if (_desktopPageSlideTransform is not null)
{
return _desktopPageSlideTransform;
}
_desktopPageSlideTransform = DesktopPage.RenderTransform as TranslateTransform;
if (_desktopPageSlideTransform is null)
{
_desktopPageSlideTransform = new TranslateTransform();
DesktopPage.RenderTransform = _desktopPageSlideTransform;
}
return _desktopPageSlideTransform;
}
private async void SlideOutAndMinimizeAsync()
{
_isSlideAnimationActive = true;
DesktopPage.IsHitTestVisible = false;
var useSlide = IsSlideTransitionEnabled();
var slideTransform = GetDesktopPageSlideTransform();
if (useSlide)
{
slideTransform.X = Bounds.Width;
}
DesktopPage.Opacity = 0;
await Task.Delay(useSlide
? FluttermotionToken.Intro
: FluttermotionToken.Page);
if (!_isSlideAnimationActive)
{
return;
}
WindowState = WindowState.Minimized; WindowState = WindowState.Minimized;
slideTransform.X = 0;
DesktopPage.Opacity = 1;
DesktopPage.IsHitTestVisible = true;
_isSlideAnimationActive = false;
}
public void PrepareEnterAnimation()
{
_isSlideAnimationActive = false;
var useSlide = IsSlideTransitionEnabled();
var slideTransform = GetDesktopPageSlideTransform();
var savedTransitions = DesktopPage.Transitions;
DesktopPage.Transitions = null;
DesktopPage.Opacity = 0;
if (useSlide)
{
slideTransform.X = Bounds.Width > 0 ? Bounds.Width : 1920;
}
DesktopPage.Transitions = savedTransitions;
DesktopPage.IsHitTestVisible = false;
_isSlideAnimationActive = true;
}
public void PlayEnterAnimation()
{
var slideTransform = GetDesktopPageSlideTransform();
DesktopPage.Opacity = 1;
slideTransform.X = 0;
DesktopPage.IsHitTestVisible = true;
_isSlideAnimationActive = false;
}
private bool IsSlideTransitionEnabled()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return false;
}
var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
return snapshot.EnableSlideTransition;
} }
private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) private void OnWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
@@ -848,8 +946,18 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
return; return;
} }
if (_isSlideAnimationActive)
{
return;
}
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
if (_isSlideAnimationActive)
{
return;
}
if (WindowState is not (WindowState.Minimized or WindowState.FullScreen)) if (WindowState is not (WindowState.Minimized or WindowState.FullScreen))
{ {
WindowState = WindowState.FullScreen; WindowState = WindowState.FullScreen;

View File

@@ -106,6 +106,17 @@
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
</ui:SettingsExpander> </ui:SettingsExpander>
<ui:SettingsExpander Header="滑入滑出过渡效果"
Description="启用后,进入和退出桌面时使用滑入滑出动画(仅 Windows"
IsVisible="{Binding IsSlideTransitionAvailable}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ArrowRight" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding EnableSlideTransition}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>