QML 文字入场动画:逐字弹出、逐字飞入、3D 飞入

目录

在上篇文章中介绍的入场效果主要依赖整体文字的位移、缩放和旋转。这篇继续往"逐字控制"和"空间纵深"方向扩展:把一句话拆成单个字符按顺序弹出、让单个字符从四面八方飞入汇成一行、以及让整段文字从远处放大飞入再飞走。这些效果的核心都是把基础属性(位移、缩放、透明度)组合出更丰富的观感,适合在标题页、启动屏、品牌展示等场景里提升视觉冲击力。

涉及的核心知识点:

  • Repeater 拆分文字为单字,实现逐字动画
  • PauseAnimation { duration: index * n } 制造 stagger 交错效果
  • transform: Translate 改变变换原点,控制飞入起点
  • scale + opacity 模拟远近透视

Demo 1 逐字弹出

把一整句话拆成单个字符,每个字按顺序弹出,并带独立的回弹和淡出。这个效果在标题展示、节日祝福、品牌口号里很常见。

以下演示代码来自 qml_text/Demo_TextCharPop.qml,为便于阅读,只展示了关键代码。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Layouts

DemoPage {
    id: root
    title: "逐字弹出"
    description: "每个字符依次蹦入,带回弹缓动,常用于标题展示。"

    property string fullText: "逐字弹出"
    property var chars: fullText.split('')

    // 每个字的动画总时长必须相同,否则多轮循环后会因各自的 loop 周期不同而错位
    property int staggerDelay: 120
    property int loopDuration: 3300

    Row {
        Layout.alignment: Qt.AlignHCenter
        spacing: 2

        Repeater {
            model: root.chars.length

            Text {
                text: root.chars[index]
                font.pointSize: 48
                font.bold: true
                color: "#E91E63"
                scale: 0
                opacity: 0

                SequentialAnimation on scale {
                    loops: Animation.Infinite
                    running: root.visible && root.opacity === 1
                    PauseAnimation { duration: index * root.staggerDelay }
                    NumberAnimation { from: 0; to: 1.4; duration: 200; easing.type: Easing.OutQuad }
                    NumberAnimation { from: 1.4; to: 1.0; duration: 200; easing.type: Easing.OutBounce }
                    PauseAnimation { duration: 1500 }
                    NumberAnimation { from: 1.0; to: 0; duration: 200 }
                    // 用末尾停顿补齐到统一的 loopDuration,保证所有字符循环周期一致
                    PauseAnimation { duration: root.loopDuration - 2100 - index * root.staggerDelay }
                }

                SequentialAnimation on opacity {
                    loops: Animation.Infinite
                    running: root.visible && root.opacity === 1
                    PauseAnimation { duration: index * root.staggerDelay }
                    NumberAnimation { from: 0; to: 1; duration: 100 }
                    PauseAnimation { duration: 1800 }
                    NumberAnimation { from: 1; to: 0; duration: 200 }
                    // 与 scale 动画保持相同的总周期
                    PauseAnimation { duration: root.loopDuration - 2100 - index * root.staggerDelay }
                }
            }
        }
    }
}

关键逻辑解析

核心思路是把 fullText.split('') 拆成字符数组,再用 Repeater 生成等量的 Text。每个 Text 的动画开始处用 PauseAnimation { duration: index * root.staggerDelay },利用 Repeater 给每个子项分配的 index 制造 stagger(交错)效果:第 0 个字先出,第 1 个字延迟 120ms,第 2 个字延迟 240ms,以此类推。

scale 动画先用 OutQuad 快速放大到 1.4 倍,再用 OutBounce 落到 1.0,做出"蹦出来"的感觉。opacity 则简单淡入,保持同步。

容易踩坑的地方 :如果每个字符的循环周期不一样,播放几轮后各字的相位就会错开,出现"乱弹出"的现象。原实现把 index * 120 同时放在循环开头和结尾,导致每个字的总周期是 2100 + index * 240,各不相同。修复方法是引入统一的 loopDuration,并在循环末尾用 loopDuration - 2100 - index * staggerDelay 补齐停顿,让所有字的 SequentialAnimation 周期完全相同,这样每次重新循环时仍然保持初始的交错节奏。


Demo 2 逐字飞入

这个效果把一句话拆成单个字符,每个字符从不同的方向(左上、右下等)飞入到最终位置,汇成一行后再整体淡出。比单纯的逐字弹出多了空间位移,适合做更活泼的标题展示。

以下演示代码来自 qml_text/Demo_TextCharFlyIn.qml,为便于阅读,只展示了关键代码。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Layouts

DemoPage {
    id: root
    title: "逐字飞入"
    description: "每个字符从不同方向飞入汇成一行,动感十足。"

    property string fullText: "逐字飞入"
    property var chars: fullText.split('')

    // 每个字符的飞入方向(固定,保证循环一致)
    property var fromXs: [180, -160, 130, -120]
    property var fromYs: [-140, 130, -100, 110]

    // 统一循环周期,避免不同 index 的字因 loop 时长不同而逐渐错位
    property int staggerDelay: 100
    property int loopDuration: 3300

    Row {
        Layout.alignment: Qt.AlignHCenter
        spacing: 0

        Repeater {
            model: root.chars.length

            Text {
                id: charItem
                text: root.chars[index]
                font.pointSize: 48
                font.bold: true
                color: "#00BCD4"
                opacity: 0

                transform: Translate {
                    id: charTranslate
                    x: 0
                    y: 0
                }

                SequentialAnimation {
                    running: root.visible && root.opacity === 1
                    loops: Animation.Infinite

                    ScriptAction {
                        script: {
                            charTranslate.x = root.fromXs[index % root.fromXs.length]
                            charTranslate.y = root.fromYs[index % root.fromYs.length]
                            charItem.opacity = 0
                        }
                    }

                    PauseAnimation { duration: index * root.staggerDelay }

                    ParallelAnimation {
                        NumberAnimation { target: charTranslate; property: "x"; to: 0; duration: 500; easing.type: Easing.OutCubic }
                        NumberAnimation { target: charTranslate; property: "y"; to: 0; duration: 500; easing.type: Easing.OutCubic }
                        NumberAnimation { target: charItem; property: "opacity"; to: 1; duration: 300 }
                    }

                    PauseAnimation { duration: 1500 }

                    NumberAnimation { target: charItem; property: "opacity"; to: 0; duration: 200 }

                    // 用末尾停顿补齐到统一的 loopDuration,保证所有字符循环周期一致
                    PauseAnimation { duration: root.loopDuration - 2200 - index * root.staggerDelay }
                }
            }
        }
    }
}

关键逻辑解析

和第 1 篇的逐字弹出一样,这里也是用 Repeater 拆分字符。区别在于每个字符额外挂载了一个 Translate 变换,用来控制飞入的起点。fromXsfromYs 是两个固定数组,存了四个方向;通过 index % root.fromXs.length 让每个字轮流取一个方向,保证循环时方向不变、效果稳定。

动画分成三步:先 ScriptAction 复位到起点,再 PauseAnimationindex * root.staggerDelay 交错等待,最后 ParallelAnimation 同时把 x、y 归 0 并把 opacity 提到 1。Easing.OutCubic 让飞行末段减速,落地感更自然。

容易踩坑的地方 :和逐字弹出一样,如果每个字符的循环总时长不一致,运行一段时间后 stagger 就会错位,出现有的字消失慢、动画不连贯的问题。这里同样用统一的 loopDuration,并在循环末尾用 loopDuration - 2200 - index * staggerDelay 补齐停顿,确保所有字符周期相同。如果你想让飞入方向更随机或更可控,可以把 fromXs/fromYs 改成函数生成,或者暴露成组件属性让外部传入。


Demo 3 飞入文字

这个 demo 模拟的是文字从远处朝屏幕飞来的效果:文字先很小、很透明,然后快速放大并变清晰,停留片刻后再继续放大飞走。配合底部进度条,形成一组轮播标题。

以下演示代码来自 qml_text/Demo_TextFlyIn3D.qml,为便于阅读,只展示了关键代码。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Layouts

DemoPage {
    id: root
    title: "飞入文字"
    description: "文字从远处飞来放大、短暂停留后飞走,模拟3D文字逐个入场效果。"

    // 3D 飞入文字:文字从远处飞来、放大、停留、飞走
    property int currentIndex: -1
    property var words: ["飞入文字", "远处飞来放大", "暂停留后飞走", "模拟入场效果"]

    Rectangle {
        Layout.fillWidth: true
        Layout.preferredHeight: 200
        color: "#0a0a0a"
        radius: 8
        clip: true

        Text {
            id: playText
            anchors.centerIn: parent
            font.pointSize: 48
            font.bold: true
            color: "#FFFFFF"
            style: Text.Outline
            styleColor: "#401296FF"
            scale: 0.1
            opacity: 0

            property real depthGlow: 0

            // 阴影层
            layer.enabled: true

            SequentialAnimation {
                id: flyInAnimation
                loops: Animation.Infinite
                running: root.visible && root.opacity === 1
                // onStarted 只在动画启动时触发一次,而 infinite loop 不会重新触发它,
                // 因此每轮循环的初始化必须放在 ScriptAction 里执行
                ScriptAction {
                    script: {
                        if (root.currentIndex < root.words.length - 1)
                            root.currentIndex++
                        else
                            root.currentIndex = 0
                        // console.log("root.currentIndex = " + root.currentIndex)
                        playText.text = root.words[root.currentIndex]
                        playText.scale = 0.1
                        playText.opacity = 0
                    }
                }
                ParallelAnimation {
                    NumberAnimation { target: playText; property: "scale"; to: 1.0; duration: 600; easing.type: Easing.OutBack }
                    NumberAnimation { target: playText; property: "opacity"; to: 1.0; duration: 400 }
                }
                PauseAnimation { duration: 800 }
                ParallelAnimation {
                    NumberAnimation { target: playText; property: "scale"; to: 1.8; duration: 400; easing.type: Easing.InQuad }
                    NumberAnimation { target: playText; property: "opacity"; to: 0; duration: 400 }
                }
                PauseAnimation { duration: 200 }
            }
        }

        // 底部进度指示
        Row {
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 16
            anchors.horizontalCenter: parent.horizontalCenter
            spacing: 8

            Repeater {
                model: root.words.length

                Rectangle {
                    width: 24
                    height: 4
                    radius: 2
                    color: index === root.currentIndex ? "#1296FF" : "#333333"
                    Behavior on color { ColorAnimation { duration: 200 } }
                }
            }
        }
    }
}

关键逻辑解析

这个 demo 没有真正的 3D 变换,而是用 scaleopacity 的变化模拟透视:远处的东西看起来小且淡,飞近后变大变清晰,飞过头后又变大变淡直到消失。Easing.OutBack 让文字到达正常大小时有一个轻微回弹,增强"飞入并停住"的感觉;Easing.InQuad 则让飞走时加速,模拟渐行渐远。

SequentialAnimation 设置 loops: Animation.Infinite 无限循环,并通过 running: root.visible && root.opacity === 1 只在当前 demo 可见时播放。需要注意:onStarted 只在动画真正启动时触发一次,无限循环不会重复触发它,所以每轮循环前的文字切换和状态重置必须放在 ScriptAction 里执行,这样每次循环都会先执行 ScriptAction,再进入飞入/飞出的动画段。底部进度条用 Repeater 生成一组小矩形,color 根据 index === root.currentIndex 切换,配合 Behavior on color 做过渡。


运行验证

  1. 用 Qt Creator 打开 qml_text/CMakeLists.txt
  2. Ctrl+R 运行项目;
  3. 在主界面找到"逐字弹出""逐字飞入""飞入文字"三个卡片,观察循环效果。

扩展复用方向

  • Demo_TextCharPopfullTextstaggerDelay 提取为组件属性,做成可配置的口号展示组件。
  • Demo_TextCharFlyInfromXs/fromYs 改为组件属性,让外部自定义每个字的飞入方向。
  • Demo_TextFlyIn3Dwords 数组暴露成接口,做成可配置的广告语轮播组件。

已验证环境:

  • Qt 版本:Qt 6.8.2、Qt 6.11.1
  • 操作系统:Windows 11
相关推荐
熬夜苦读学习9 小时前
QT_信号和槽
开发语言·qt
豆浆D油条1 天前
QT容器类QList调用erase崩溃
开发语言·qt·rpc
蓝速科技1 天前
蓝速科技 3D 全息舱与 AI 数字人酒店降本增效实战
人工智能·科技·3d
国际学术会议-杨老师1 天前
2026年游戏美学、动画生成与色彩科学国际会议(GEAAC 2026)
游戏·动画·色彩科学
杜子不疼.1 天前
【Qt常用控件】WindowTitle 与 WindowIcon:窗口标题和图标
开发语言·qt
在下胡三汉1 天前
GLB与GLTF:有什么区别——你应该用哪一个?
3d·gltf
玖玥拾1 天前
Unity 3D 笔记(八)ScrollRect 滚动视图、NavMesh 自动寻路系统
笔记·3d·unity
极客侃科技1 天前
如何把真人动作迁移到3D角色上?用V2Fun完成视频动捕和动作应用
3d