4. 动画控制与组合

在 Qt Quick 中,单个 PropertyAnimation 只能驱动一个属性在一段时间内变化。真实界面动效往往需要多个动画按顺序衔接、同时并行、中途停顿,甚至在特定时刻触发业务逻辑。Qt Quick 为此提供了四种“动画编排器”:SequentialAnimation(顺序)、ParallelAnimation(并行)、PauseAnimation(停顿)、ScriptAction(脚本动作)。它们本身不直接改变属性,而是作为容器或占位节点,把子动画组织成一条可预测的时间线。

一、核心原理:四种编排器的职责边界

理解它们的关键在于区分“时间轴控制”与“属性驱动”。PropertyAnimation、NumberAnimation 等是真正改变属性值的执行者;而 SequentialAnimation 和 ParallelAnimation 是时间轴容器,决定子动画是串行还是并行;PauseAnimation 是纯时间占位,不改变任何属性;ScriptAction 则在时间轴到达它的那一刻执行一段 JavaScript,常用于切换状态、打印日志或触发外部逻辑。

  • SequentialAnimation:子动画依次执行,前一个结束后下一个才开始,总时长等于各子项之和。
  • ParallelAnimation:所有子动画同时启动,总时长等于最长子项,适合“位移+缩放+透明度”同步变化。
  • PauseAnimation:插入一段空白等待,duration 以毫秒计,常用于节奏控制。
  • ScriptAction:在时间轴指定位置执行脚本,script 属性为要执行的 JS 代码。

四者可以任意嵌套:SequentialAnimation 里放 ParallelAnimation,ParallelAnimation 里放 PauseAnimation,从而构建出复杂但清晰的时间线。下面这张图展示了本节最典型的编排结构:一个顺序动画内部先并行执行位移与缩放,再停顿,最后触发脚本动作。

SequentialAnimation 时间线编排示例 SequentialAnimation(总时长 = 并行段 + 停顿 + 脚本) 子项按顺序执行,前一项 finished 后下一项才开始 ParallelAnimation NumberAnimation x NumberAnimation scale PauseAnimation duration: 300 纯时间占位,不改属性 ScriptAction script: root.state = "done" 到达即执行 JS 逻辑 时间轴:并行段(最长 400ms) → 停顿 300ms → 脚本触发,总时长约 700ms

二、方法流程:如何组织一条可维护的时间线

编排动画时,建议遵循“先拆解、再分组、后嵌套”的流程。第一步,把动效拆成原子动作,例如“右移”“放大”“等待”“切换状态”。第二步,判断哪些动作必须同时发生,把它们放进 ParallelAnimation;哪些必须先后发生,放进 SequentialAnimation。第三步,用 PauseAnimation 调整节奏,用 ScriptAction 在关键节点插入逻辑。第四步,给动画设置 running 或 start()/stop(),并考虑是否使用 alwaysRunToEnd 保证停止时动画完整收尾。

SequentialAnimation {
    id: cardAnim
    running: false

    ParallelAnimation {
        NumberAnimation { target: card; property: "x"; to: 320; duration: 400; easing.type: Easing.OutCubic }
        NumberAnimation { target: card; property: "scale"; to: 1.15; duration: 400 }
    }
    PauseAnimation { duration: 300 }
    ScriptAction { script: root.state = "done" }
}

调用时只需 cardAnim.start()。若希望中途停止但让当前动画跑完,可设置 alwaysRunToEnd: true,这在用户快速点击、需要避免动画“跳变”的场景中非常有用。

三、实战案例:卡片展开动效

假设有一个卡片,点击后需要“向右移动并放大 → 停顿 300ms → 切换状态显示详情”。用 SequentialAnimation 包裹 ParallelAnimation、PauseAnimation 和 ScriptAction 即可清晰表达。相比用 Timer 手动串联,这种声明式编排更易读、易改、易复用。

Rectangle {
    id: card
    width: 120; height: 80; color: "#7fb3d5"

    MouseArea {
        anchors.fill: parent
        onClicked: cardAnim.start()
    }

    SequentialAnimation {
        id: cardAnim
        ParallelAnimation {
            NumberAnimation { target: card; property: "x"; to: 320; duration: 400; easing.type: Easing.OutCubic }
            NumberAnimation { target: card; property: "scale"; to: 1.15; duration: 400 }
        }
        PauseAnimation { duration: 300 }
        ScriptAction { script: console.log("动画完成,切换状态"); root.state = "done" }
    }
}
如果多个动画需要共享同一套缓动曲线,可以在父级 SequentialAnimation 或 ParallelAnimation 上统一设置 easing,子动画会继承,减少重复配置。

四、误区与复盘

常见误区有三个。第一,把 SequentialAnimation 当成“同时执行”,实际上它是严格串行,若把两个位移动画放进去,会看到明显的先后顺序。第二,在 ParallelAnimation 中放入 PauseAnimation,误以为会“暂停整个并行组”,实际上它只是并行组中的一个占位分支,不会阻塞其他分支。第三,ScriptAction 中执行耗时操作,会阻塞渲染线程,导致动画卡顿,应只做轻量状态切换或信号发射。

编排器 执行方式 是否改变属性 典型用途
SequentialAnimation 串行 否(容器) 多段动画依次衔接
ParallelAnimation 并行 否(容器) 位移、缩放、透明度同步
PauseAnimation 占位等待 否 节奏控制、延迟触发
ScriptAction 瞬时执行 否 切换状态、发信号、打日志
不要在 ScriptAction 中直接修改正在被动画驱动的属性,否则会与动画产生竞争,导致值抖动。正确做法是先停止动画,再修改属性,或通过状态机间接切换。

复盘时可以用“时间轴三问”检查:总时长是否等于各段之和?并行段是否真的需要同时开始?脚本动作是否放在正确的时刻?把这三问答清楚,动画编排就从“能跑”走向“可控、可维护”。