Skip to content

壁纸动画轮播引擎——接手实施计划

状态: ✅ 已落地(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() 兜底)已落地。当前壁纸切换是瞬时换图:WallPaperLayerImageChangedCallbackWallpaperShellLayerStrategy::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}~146SwitchingMode{Fixed,Gradient,Movement}、QTimer(默认 SWITCH_INTERVAL=20000)、ANIMATION_DURATION=2000QEasingCurve(InOutCubic)DEF_MODE=Movement、随机选择器、friend WallPaperAnimationHandler
ui/wallpaperanimationhandler.{h,cpp}~154process_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_imagerescaleImage(),合成必须在这里;engine 只管「何时切、切到哪、用什么模式」。

4.3 过渡状态机(strategy 侧)

新增私有态:QImage previous_scaledbool transitioningqreal transition_progressSwitchingMode transition_modestd::unique_ptr<QPropertyAnimation>

过渡流程(timer 触发或手动 next):

  1. engine 决定切下一张 → 调 strategy.beginTransition(mode)
  2. strategy:previous_scaled = cached_scaled_image(捕获当前)。
  3. strategy 调 wallpaper_layer->showNextOne() → layer 换 currentImage() + fire ImageChangedCallback
  4. strategy 在 onWallpaperChanged() 里检测 transitioning==true → 不走瞬时路径,改为 rescaleImage()图缩放进 cached_scaled_image(作为 current),保留 previous_scaled,启动 QPropertyAnimation(0→1, duration, easing)
  5. 每个 valueChanged(t):cached_scaled_image = composite(previous_scaled, current_scaled, t, mode)requestRepaint()
  6. finished:丢弃 previous_scaledtransitioning=falsecached_scaled_image = 纯 current。

合成函数 composite(prev, cur, t, mode)(用 QPainter 画到目标尺寸 QImage):

  • Gradient:先画 cur(opacity=1),再画 prev(opacity=1-t)——淡出新、露出新。或 prev opacity 1-t + cur opacity t(双向淡入淡出)。选定一种并注释。
  • Movement:prevx = lerp(0, -W, t)curx = lerp(W, 0, t)(沿用 CCIMX 方向:旧左出 / 新右入),均 opacity=1。

transition_progress 作为 Q_PROPERTY 挂在 strategy 上,QPropertyAnimationprogress 属性;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",对齐 CCIMX DEF_MODE)。
  • switch_interval_ms:默认 20000
  • animation_duration_ms:默认 2000
  • (可选/后续)switch_easing:默认 InOutCubic

desktop_config_init.cppWALLPAPER_CONFIG_TEMPLATE 补默认值。

4.6 生命周期接线

wallpaper_setup.cppcreate_wallpaper_strategy() 现为 make_unique<WallpaperShellLayerStrategy>(make_layer()):

  • strategy 内部构造 WallPaperEngine(传 layer 原始指针 + request_transition 回调)。
  • activate() 末尾 engine->start();deactivate()engine->stop()
  • mode==Fixedstorage->size()<=1:start() 不启动 timer(照搬 CCIMX 守卫)。

4.7 CMake

新文件加入 desktop/ui/components/wallpaper/CMakeLists.txtcfdesktop_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);strategy composite() 给定两张图 + 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:WallPaperEnginestd::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-commitsbranch-commit-allowed-push-never)。

七、验证(端到端)

  1. Fixed:不轮播,壁纸静止(行为同今天)。
  2. Gradient:定时到点 → 交叉淡入,过渡平滑、结束显示新图;日志见 transition start/end。
  3. Movement:平移滑入(旧左出 / 新右入),方向同 CCIMX。
  4. 边界:壁纸库 0 张/1 张 → 不崩溃、不启动 timer(size()<=1 守卫);mode 非法 → 回退 movement + warn。
  5. 配置:改 switch_interval_ms/switch_mode(重启或热更,注明支持哪种)→ 生效。
  6. 回归:linux_run_tests.sh 全绿(含新增动画测试);既有壁纸/发现行为不变(Fixed 模式 ≈ 今天)。
  7. 门禁:三层依赖 grep + python3 scripts/doxygen/lint.py 通过;linux_fast_develop_build.sh 通过。

八、待定决策(接手 AI 与用户确认)

  1. 手动切换是否也走动画:现有 showNext/Prev/Target 是否复用过渡路径?建议:是(timer 自动 + 手动都动画,体验一致)。
  2. 缓动曲线是否配置化:先固定 InOutCubic,随主题/设置面板(Phase 12)再暴露。
  3. 低性能板降级:6ULL 等是否默认 Fixed / 关动画?见 desktop-buildout-handoff 记忆的 6ULL 铁律;建议加一个 HWTier 判断或配置项。
  4. 资源包 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.cppWALLPAPER_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.cppmake_layer()scaling/background_color 配置(原僵尸 key)
cmake/meta_info/desktop_settings.template.h.inWALLPAPER_CONFIG_TEMPLATE 加 6 个动画 key
test/desktop/wallpaper_animation/:16 例(composer 7 + selector 6 + engine 3),全过

对接手文档的纠正(实施时发现):

  1. 配置模板在 .h.in(cf::desktop::early_stage::WALLPAPER_CONFIG_TEMPLATE), desktop_config_init.cpp——后者只是写入它。
  2. WallPaperAccessStorageindexed_vector 是 private,原无枚举/按索引接口 → 必须加 tokenIds() 才能支持 Random。
  3. 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/边界/配置切换)。

Built with VitePress