目录Demo 1 逐字弹出演示代码关键逻辑解析Demo 2 逐字飞入演示代码关键逻辑解析Demo 3 飞入文字演示代码关键逻辑解析运行验证扩展复用方向在上篇文章中介绍的入场效果主要依赖整体文字的位移、缩放和旋转。这篇继续往逐字控制和空间纵深方向扩展把一句话拆成单个字符按顺序弹出、让单个字符从四面八方飞入汇成一行、以及让整段文字从远处放大飞入再飞走。这些效果的核心都是把基础属性位移、缩放、透明度组合出更丰富的观感适合在标题页、启动屏、品牌展示等场景里提升视觉冲击力。涉及的核心知识点Repeater拆分文字为单字实现逐字动画PauseAnimation { duration: index * n }制造 stagger 交错效果transform: Translate改变变换原点控制飞入起点scaleopacity模拟远近透视Demo 1 逐字弹出把一整句话拆成单个字符每个字按顺序弹出并带独立的回弹和淡出。这个效果在标题展示、节日祝福、品牌口号里很常见。以下演示代码来自qml_text/Demo_TextCharPop.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为便于阅读只展示了关键代码。演示代码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变换用来控制飞入的起点。fromXs和fromYs是两个固定数组存了四个方向通过index % root.fromXs.length让每个字轮流取一个方向保证循环时方向不变、效果稳定。动画分成三步先ScriptAction复位到起点再PauseAnimation按index * 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为便于阅读只展示了关键代码。演示代码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 变换而是用scale和opacity的变化模拟透视远处的东西看起来小且淡飞近后变大变清晰飞过头后又变大变淡直到消失。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做过渡。运行验证用 Qt Creator 打开qml_text/CMakeLists.txt按CtrlR运行项目在主界面找到逐字弹出“逐字飞入”飞入文字三个卡片观察循环效果。扩展复用方向把Demo_TextCharPop的fullText和staggerDelay提取为组件属性做成可配置的口号展示组件。把Demo_TextCharFlyIn的fromXs/fromYs改为组件属性让外部自定义每个字的飞入方向。把Demo_TextFlyIn3D的words数组暴露成接口做成可配置的广告语轮播组件。已验证环境Qt 版本Qt 6.8.2、Qt 6.11.1操作系统Windows 11
QML 文字入场动画:逐字弹出、逐字飞入、3D 飞入
目录Demo 1 逐字弹出演示代码关键逻辑解析Demo 2 逐字飞入演示代码关键逻辑解析Demo 3 飞入文字演示代码关键逻辑解析运行验证扩展复用方向在上篇文章中介绍的入场效果主要依赖整体文字的位移、缩放和旋转。这篇继续往逐字控制和空间纵深方向扩展把一句话拆成单个字符按顺序弹出、让单个字符从四面八方飞入汇成一行、以及让整段文字从远处放大飞入再飞走。这些效果的核心都是把基础属性位移、缩放、透明度组合出更丰富的观感适合在标题页、启动屏、品牌展示等场景里提升视觉冲击力。涉及的核心知识点Repeater拆分文字为单字实现逐字动画PauseAnimation { duration: index * n }制造 stagger 交错效果transform: Translate改变变换原点控制飞入起点scaleopacity模拟远近透视Demo 1 逐字弹出把一整句话拆成单个字符每个字按顺序弹出并带独立的回弹和淡出。这个效果在标题展示、节日祝福、品牌口号里很常见。以下演示代码来自qml_text/Demo_TextCharPop.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为便于阅读只展示了关键代码。演示代码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变换用来控制飞入的起点。fromXs和fromYs是两个固定数组存了四个方向通过index % root.fromXs.length让每个字轮流取一个方向保证循环时方向不变、效果稳定。动画分成三步先ScriptAction复位到起点再PauseAnimation按index * 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为便于阅读只展示了关键代码。演示代码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 变换而是用scale和opacity的变化模拟透视远处的东西看起来小且淡飞近后变大变清晰飞过头后又变大变淡直到消失。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做过渡。运行验证用 Qt Creator 打开qml_text/CMakeLists.txt按CtrlR运行项目在主界面找到逐字弹出“逐字飞入”飞入文字三个卡片观察循环效果。扩展复用方向把Demo_TextCharPop的fullText和staggerDelay提取为组件属性做成可配置的口号展示组件。把Demo_TextCharFlyIn的fromXs/fromYs改为组件属性让外部自定义每个字的飞入方向。把Demo_TextFlyIn3D的words数组暴露成接口做成可配置的广告语轮播组件。已验证环境Qt 版本Qt 6.8.2、Qt 6.11.1操作系统Windows 11