壁纸动画轮播引擎——接手实施计划
状态: ✅ 已落地(2026-06-30,分支
feat/wallpaper-animation-engine;落地细节见末尾「实施记录」) 来源: 资源包发现(PR #13)已合入,status/current.md标注「动画轮播引擎留待下一批」;本文为该批的详细实施计划,供下一位 AI 直接接手。 所属 Phase: Phase 13(13_widget_apps.md 壁纸段「只需补轮播」)。 预计规模: 中等(≈2 个新文件 + 1 个扩展 + 配置 + 测试)。 最后更新: 2026-06-30
一、Context(为什么做)
CFDesktop 壁纸的数据/token 层(ImageWallPaperLayer / WallPaperAccessStorage / WallPaperToken)与运行时资源包发现(Wallpapers PathType + filter_target_recursive + make_layer() 兜底)已落地。当前壁纸切换是瞬时换图:WallPaperLayer 的 ImageChangedCallback → WallpaperShellLayerStrategy::onWallpaperChanged() → rescaleImage() → requestRepaint(),无过渡动画、无定时轮播。
本批目标:移植 CCIMXDesktop 的 WallPaperEngine + WallPaperAnimationHandler,实现:
- 三种模式:
Fixed(不切)/Gradient(交叉淡入)/Movement(平移滑入)。 - QTimer 定时自动轮播(默认 20s)+ 缓动曲线(默认 InOutCubic)+ 随机/顺序选择器。
- 配置驱动(
wallpaper.json新 key)。
二、关键约束:必须「重表达」,不能照抄
⚠️ CCIMXDesktop 用 QLabel 双缓冲 + QGraphicsOpacityEffect + QPropertyAnimation 改 QLabel pos 做动画——它耦合 QWidget 渲染。
CFDesktop 的 WallPaperLayer 是后端无关的纯数据层(只吐 QImage),WallpaperShellLayerStrategy 通过 currentBackgroundImage() 返回单张预缩放 QImage 供 shell layer 的 paintEvent 消费(后端是 QPainter 或 RHI,都吃这张 QImage)。
因此动画必须重表达为:
过渡期间,把
cached_scaled_image变成新旧两图的逐帧合成 QImage;QPropertyAnimation驱动t∈[0,1],每个valueChanged重算合成 +requestRepaint();过渡结束丢弃旧图,回到单图。
这套对 QPainter/RHI 都成立(它们都消费 QImage),不引入 QWidget 耦合,保持后端无关。
三、源参考(CCIMXDesktop,只读参照)
路径在
~/CCIMXDesktop/(本机存在)。仅参照语义,不要照抄 QLabel/QWidget 实现。
| 文件 | 行数 | 要点 |
|---|---|---|
core/wallpaper/WallPaperEngine.{h,cpp} | ~146 | SwitchingMode{Fixed,Gradient,Movement}、QTimer(默认 SWITCH_INTERVAL=20000)、ANIMATION_DURATION=2000、QEasingCurve(InOutCubic)、DEF_MODE=Movement、随机选择器、friend WallPaperAnimationHandler |
ui/wallpaperanimationhandler.{h,cpp} | ~154 | process_opacity_switch(双缓冲 QLabel + opacity effect 1→0 淡出旧图)、process_movement_switch(QParallelAnimationGroup:旧图左移出 + 新图右移入) |
注意 CCIMX 的边界:image_list.size() <= 1 时直接 return(不切)——照搬这个守卫。
四、目标架构(CFDesktop)
4.1 渲染缝(已存在,动画插在这里)
desktop/ui/components/shell_layer_impl/WallpaperShellLayerStrategy.cpp:
currentBackgroundImage()(L72)返回d->cached_scaled_image——paintEvent 消费它。rescaleImage()(L83)从d->wallpaper_layer->currentImage()按ScalingMode(Fill/Fit/Stretch)预缩放进cached_scaled_image。onWallpaperChanged()(L132,layer 回调)→rescaleImage()+requestRepaint()——当前是瞬时,这是动画改造点。
4.2 职责划分(推荐)
| 组件 | 职责 | 改动 |
|---|---|---|
WallPaperLayer(接口)+ ImageWallPaperLayer | 数据层:currentImage() / showNextOne/Prev/Target / ImageChangedCallback / ScalingMode | 不改(已具备手动切换) |
WallPaperEngine(新) | 配置(mode/interval/duration/easing)、QTimer 定时、选下一张、发 requestTransition() 给 strategy | 新增 |
WallpaperShellLayerStrategy(扩展) | 过渡状态机:捕获旧图、启动 QPropertyAnimation、每帧合成、帧结束清理 | 扩展 |
为什么过渡状态机放 strategy 而非 engine:strategy 拥有
cached_scaled_image和rescaleImage(),合成必须在这里;engine 只管「何时切、切到哪、用什么模式」。
4.3 过渡状态机(strategy 侧)
新增私有态:QImage previous_scaled、bool transitioning、qreal transition_progress、SwitchingMode transition_mode、std::unique_ptr<QPropertyAnimation>。
过渡流程(timer 触发或手动 next):
engine决定切下一张 → 调strategy.beginTransition(mode)。- strategy:
previous_scaled = cached_scaled_image(捕获当前)。 - strategy 调
wallpaper_layer->showNextOne()→ layer 换currentImage()+ fireImageChangedCallback。 - strategy 在
onWallpaperChanged()里检测transitioning==true→ 不走瞬时路径,改为rescaleImage()把新图缩放进cached_scaled_image(作为 current),保留previous_scaled,启动QPropertyAnimation(0→1, duration, easing)。 - 每个
valueChanged(t):cached_scaled_image = composite(previous_scaled, current_scaled, t, mode)→requestRepaint()。 finished:丢弃previous_scaled、transitioning=false、cached_scaled_image= 纯 current。
合成函数 composite(prev, cur, t, mode)(用 QPainter 画到目标尺寸 QImage):
- Gradient:先画
cur(opacity=1),再画prev(opacity=1-t)——淡出新、露出新。或 prev opacity1-t+ cur opacityt(双向淡入淡出)。选定一种并注释。 - Movement:
prev在x = lerp(0, -W, t)、cur在x = lerp(W, 0, t)(沿用 CCIMX 方向:旧左出 / 新右入),均 opacity=1。
transition_progress作为Q_PROPERTY挂在 strategy 上,QPropertyAnimation绑progress属性;setter 触发合成 +requestRepaint()。
4.4 WallPaperEngine(新,desktop/ui/components/wallpaper/)
namespace cf::desktop::wallpaper {
enum class SwitchingMode { Fixed, Gradient, Movement }; // 对齐 CCIMX
class WallPaperEngine : public QObject {
Q_OBJECT
public:
// 读 ConfigStore "wallpaper" 域,绑定到 layer(非 owning)与 strategy 回调
WallPaperEngine(WallPaperLayer* layer, std::function<void(SwitchingMode)> request_transition, QObject* parent);
void start(); // activate 时:mode!=Fixed 且 layer 张数>1 才 startTimer
void stop(); // deactivate 时
// 可选:配置热更 setMode/setInterval/setDuration
private:
void onTimerTick(); // 选下一张(随机/顺序)→ request_transition(mode)
};
}选择器:默认随机(对齐 CCIMX ImagePoolEngine::default_index);可后续加顺序模式。
4.5 配置(wallpaper.json 新 key)
经 ConfigStore wallpaper 域,KeyView{.group=..., .key=...}(动态 key,无需注册,见 cfconfig.hpp):
switch_mode:"fixed" | "gradient" | "movement"(默认"movement",对齐 CCIMXDEF_MODE)。switch_interval_ms:默认20000。animation_duration_ms:默认2000。- (可选/后续)
switch_easing:默认InOutCubic。
在 desktop_config_init.cpp 的 WALLPAPER_CONFIG_TEMPLATE 补默认值。
4.6 生命周期接线
wallpaper_setup.cpp 的 create_wallpaper_strategy() 现为 make_unique<WallpaperShellLayerStrategy>(make_layer()):
- strategy 内部构造
WallPaperEngine(传 layer 原始指针 +request_transition回调)。 activate()末尾engine->start();deactivate()里engine->stop()。mode==Fixed或storage->size()<=1:start()不启动 timer(照搬 CCIMX 守卫)。
4.7 CMake
新文件加入 desktop/ui/components/wallpaper/CMakeLists.txt 的 cfdesktop_wallpaper STATIC 目标(WallPaperEngine.cpp),或 shell_layer_impl 目标(看 engine 最终落点)。链接 Qt6::Core(QTimer/QPropertyAnimation 已由 Gui/Core 覆盖)。
五、待办步骤(接手 AI 执行)
- [ ] 0. 读本文件 +
WallPaperLayer.h+WallpaperShellLayerStrategy.{h,cpp}+wallpaper_setup.cpp+filter_target.h+ CCIMX 两源文件。 - [ ] 1. 先商量拉分支(纪律见下),开
feat/wallpaper-animation-engine。 - [ ] 2. 配置先行:
WALLPAPER_CONFIG_TEMPLATE补 key + 一个读取 helper(后续 engine/strategy 都依赖)。 - [ ] 3. 实现
WallPaperEngine.{h,cpp}+ 注册 CMake。 - [ ] 4. 扩展
WallpaperShellLayerStrategy:过渡状态机 +Q_PROPERTY(progress)+composite()+beginTransition();改造onWallpaperChanged()区分瞬时/过渡路径。 - [ ] 5. 接线:
create_wallpaper_strategy/activate/deactivate构造与启停 engine。 - [ ] 6. 单元测试(
add_gtest_executable,标"desktop;unit;wallpaper"):engine 模式/定时/>1守卫(用假 layer);strategycomposite()给定两张图 + t 验证输出尺寸与非空;Qt 动画用QSignalSpy。 - [ ] 7. 文档:HandBook 壁纸页 +
status/current.md+ 本文件勾选 +13_widget_apps.md壁纸段状态。 - [ ] 8. 验证(见第七节)。
六、约束(铁律)
- 三层依赖:engine/strategy 在 desktop 层,不得
#include反向。验证:grep -r '#include.*\(ui/\|desktop/\)' base/须空。 - Doxygen:新公共类/函数按
document/DOXYGEN_REQUEST.md;过python3 scripts/doxygen/lint.py。 - Ownership:
WallPaperEngine用std::unique_ptr持有(由 strategy 持有);engine 对WallPaperLayer只持非 owning 指针(strategy 拥有 layer)。避免裸new(除 Qt parent-ownership)。 - No silent fallbacks:配置
switch_mode非法 → 显式log::warn+ 回退默认movement,不要静默吞。 - 性能平衡(见 [[balance-perf-even-when-told-to-ignore]]):过渡期 2000ms/60fps ≈ 120 次全屏 QImage 合成,可接受,但:① 仅过渡窗口内跑,非过渡空闲零开销(仅 timer);② Movement 合成用
drawImage简单 blit,不走SmoothTransformation;③ geometry 变化时若正在过渡,按既有onGeometryChanged重缩放逻辑处理(注意过渡中要同步重缩previous_scaled)。 - 分支/提交纪律:分支上
add+commit可以;绝不 push(用户自己 push);commit message 英文且不加Co-Authored-By(见记忆no-co-authored-by-in-commits、branch-commit-allowed-push-never)。
七、验证(端到端)
- Fixed:不轮播,壁纸静止(行为同今天)。
- Gradient:定时到点 → 交叉淡入,过渡平滑、结束显示新图;日志见 transition start/end。
- Movement:平移滑入(旧左出 / 新右入),方向同 CCIMX。
- 边界:壁纸库 0 张/1 张 → 不崩溃、不启动 timer(
size()<=1守卫);mode非法 → 回退 movement + warn。 - 配置:改
switch_interval_ms/switch_mode(重启或热更,注明支持哪种)→ 生效。 - 回归:
linux_run_tests.sh全绿(含新增动画测试);既有壁纸/发现行为不变(Fixed 模式 ≈ 今天)。 - 门禁:三层依赖 grep +
python3 scripts/doxygen/lint.py通过;linux_fast_develop_build.sh通过。
八、待定决策(接手 AI 与用户确认)
- 手动切换是否也走动画:现有
showNext/Prev/Target是否复用过渡路径?建议:是(timer 自动 + 手动都动画,体验一致)。 - 缓动曲线是否配置化:先固定
InOutCubic,随主题/设置面板(Phase 12)再暴露。 - 低性能板降级:6ULL 等是否默认 Fixed / 关动画?见
desktop-buildout-handoff记忆的 6ULL 铁律;建议加一个 HWTier 判断或配置项。 - 资源包 manifest 解析:CF-Gallery
scripts/manifest.json(带 photographer/width/height)是否本批接入,用于壁纸选择器 UI 的署名/尺寸?建议本批不做,随壁纸选择器 UI(Phase 12/13 后续)。
九、关键文件速查
| 文件 | 角色 |
|---|---|
| WallPaperLayer.h | 数据层接口(不改) |
| ImageWallPaperLayer.h / .cpp | 数据层实现(不改) |
| WallpaperShellLayerStrategy.h / .cpp | 渲染缝,扩展过渡状态机 |
| wallpaper_setup.cpp | 装配点(create_wallpaper_strategy),接线 engine |
| wallpaper/CMakeLists.txt | 注册新 WallPaperEngine.cpp |
| desktop_config_init.cpp | WALLPAPER_CONFIG_TEMPLATE 补 key |
~/CCIMXDesktop/core/wallpaper/WallPaperEngine.{h,cpp} | 源参照(engine 语义) |
~/CCIMXDesktop/ui/wallpaperanimationhandler.{h,cpp} | 源参照(过渡语义,勿抄 QLabel) |
十、实施记录(2026-06-30 落地)
实际落地文件:
| 文件 | 角色 |
|---|---|
desktop/ui/components/wallpaper/TransitionComposer.{h,cpp} | 新:SwitchingMode 枚举 + 纯函数 composeTransitionFrame(Gradient/Movement/Fixed 逐帧合成,可单测) |
desktop/ui/components/wallpaper/WallPaperEngine.{h,cpp} | 新:配置驱动 + QTimer 定时 + Selector{Sequential,Random} + 纯函数 selectNextWallpaper + size()>1/Fixed/disable_animation 守卫 |
WallPaperAccessStorage.{h,cpp} | 加 tokenIds()(Random 选择器所需) |
WallpaperShellLayerStrategy.{h,cpp} | 过渡状态机(QVariantAnimation 驱动逐帧合成)+ triggerNextWallpaper() |
wallpaper_setup.cpp | make_layer() 接 scaling/background_color 配置(原僵尸 key) |
cmake/meta_info/desktop_settings.template.h.in | WALLPAPER_CONFIG_TEMPLATE 加 6 个动画 key |
test/desktop/wallpaper_animation/ | 新:16 例(composer 7 + selector 6 + engine 3),全过 |
对接手文档的纠正(实施时发现):
- 配置模板在
.h.in(cf::desktop::early_stage::WALLPAPER_CONFIG_TEMPLATE),非desktop_config_init.cpp——后者只是写入它。 WallPaperAccessStorage的indexed_vector是 private,原无枚举/按索引接口 → 必须加tokenIds()才能支持 Random。- CFDesktop 原生
showNextOne顺序不 wrap(到边界返回 false),与 CCIMX 随机不同 → 用纯函数selectNextWallpaper统一两种策略,Sequential 自带 wrap,Random 排除当前。
实际决策(与用户确认):
- 选择器:Sequential + Random 两种都做,
switch_selector配置切换(默认 Sequential)。 - 6ULL/低性能降级:仅
disable_animation配置项,不接 HWTier。 - 手动切换也走动画:
triggerNextWallpaper()复用过渡路径(为壁纸选择器 UI 预留)。 - 顺手接
scaling/background_color僵尸 key。 - 缓动曲线配置化:
switch_easing(inoutcubic/outcubic/linear)。
工程选择(偏离原文档):
- 用
QVariantAnimation(valueChanged驱动)而非QPropertyAnimation——strategy 非 QObject,免Q_OBJECT/Q_PROPERTY,改动最小。 - engine 不切图:切图同步触发 layer 回调,必须由 strategy 先设过渡状态;故 engine 只发
request_(mode),strategy 在回调里设状态 + 切图 + 启动动画,时序正确。 - 过渡中 geometry 变化 →
resetTransition()中止(旧图缓存无法重缩),由rescaleImage()处理新尺寸。 - 新图加载失败(
currentImage().isNull())→ abort 过渡、回退旧图 + warn(no-silent-fallback)。
验证(全绿):三层依赖 grep ✅ / python3 scripts/doxygen/lint.py ✅ / linux_fast_develop_build.sh ✅ / linux_run_tests.sh 13/13 ✅(含新增 16 例 wallpaper 测试)。手动端到端验证留待合入后在 WSLg 实机跑(Fixed/Gradient/Movement/边界/配置切换)。