# 动画
动画作用于属性变更过程。一个动画定义了属性变化时,从一个值变到另一个值的缓动曲线。这些动画曲线创建了从一个值到另一个值的平滑过渡。
一个动画由一些动态属性,缓动曲线和时长组成。Qt Quick 中的所有动画都由同一个计时器控制,因此是同步的。这提升了动画性能和质量。
动画通过修改属性值控制属性的变化
这是一条基本原则。QML 基于元素,属性和脚本。每个元素都提供了几十个属性,每个属性都能被动画。在本书中,你会看到这个壮观的竞技场。
你可以看看一些动画,鉴赏它们,或者发挥你的创造力。请牢记:动画控制属性变更,每个元素都有几十个属性受您控制。
释放力量吧!
// AnimationExample.qml
import QtQuick
Image {
id: root
source: "assets/background.png"
property int padding: 40
property int duration: 4000
property bool running: false
Image {
id: box
x: root.padding;
y: (root.height-height)/2
source: "assets/box_green.png"
NumberAnimation on x {
to: root.width - box.width - root.padding
duration: root.duration
running: root.running
}
RotationAnimation on rotation {
to: 360
duration: root.duration
running: root.running
}
}
MouseArea {
anchors.fill: parent
onClicked: root.running = true
}
}
以上实例展示了一个针对 x
和 rotation
属性的简单动画。每个动画耗时 4000 毫秒。该动画作用于 x
,将其横坐标渐渐移至 240 px。该动画也作用于角度属性,从当前角度转至 360 度。两个动画是并行运行的,且在点击 MouseArea
后开始的。
你可以通过修改 to
和 duration
属性来处理动画,也可添加新的动画(例如,opacity
或 scale
)。这些组合在一起可以使得对象慢慢消失在深空!
# 动画元素
这里有好几种动画类型,每种都针对特定的用例优化。这是一份重要动画的列表:
PropertyAnimation
- 针对属性值的动态修改NumberAnimation
- 针对实数类型的动态修改ColorAnimation
- 针对颜色的动态修改RotationAnimation
- 针对旋转角度的动态修改
除了这些广泛使用的基础动画元素,Qt Quick 还提供了更多针对特殊用例的动画:
PauseAnimation
- 允许暂停动画SequentialAnimation
- 允许依次执行动画ParallelAnimation
- 允许并行运行动画AnchorAnimation
- 针对锚值得动态修改ParentAnimation
- 针对父值的动态修改SmoothedAnimation
- 允许属性值动态追踪一个值SpringAnimation
- 允许属性值像弹簧一样追踪一个值PathAnimation
- 元素沿一条路径动态前进Vector3dAnimation
- 以 QVector3d 值执行动画
稍后我们会学习如何创建动画序列。在制作更复杂的动画时,有时需要在动画进行时修改属性或运行脚本。Qt Quick 为此提供了动作元素,只要动画元素能用的地方,动作元素就可用:
PropertyAction
- 在动画中立刻修改属性ScriptAction
- 定义动画中运行的脚本
本章会用小而精的例子介绍主要的动画类型。
# 应用动画
有几种应用动画的方法:
Animation on property(属性动画) - 元素完全加载后自动运行
Behavior on property(属性行为) - 属性值修改后自动运行
Standalone Animation(独立动画) - 显式地通过
start()
或running
设置为 true 时运行动画(如,属性绑定)
后续我们会看看如何在 state 变化过程中使用动画。
# 点击图片 V2
为了示范如何使用动画,我们重用了前文的 ClickableImage 组件,并为其添加了一个文本元素。
// ClickableImageV2.qml
// Simple image which can be clicked
import QtQuick
Item {
id: root
width: container.childrenRect.width
height: container.childrenRect.height
property alias text: label.text
property alias source: image.source
signal clicked
Column {
id: container
Image {
id: image
}
Text {
id: label
width: image.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
color: "#ececec"
}
}
MouseArea {
anchors.fill: parent
onClicked: root.clicked()
}
}
为了组织图片下面的元素,我们使用了 Column 定位器,并基于列的 childrenRect 属性计算了宽高。我们导出了 text,图片 souce 属性和点击信号。同时我们期望文本的宽度和图片一样,支持换行。利用文本元素的 wrapMode
属性,我们实现了换行功能。
父级/子级 元素几何依赖
因为翻转了几何依赖(父元素的形状依赖子元素),我们无法对 ClickableImageV2 设置 width
或 height
,因为这会破坏绑定关系。
若父元素仅作为容器,且子元素会适应父元素,则更应该倾向于使子元素的形状依赖于父元素的形状。
# 依次排列的对象
这三个对象的 y 坐标是一样的(y=200
)。都需要移动到 y=40
,每个元素使用了不同的方法实现了此目的,每个方法有不同的副作用和特性。
# 第一个对象
第一个对象使用 Animation on <property>
策略。该动画立刻启用。
ClickableImageV2 {
id: greenBox
x: 40; y: root.height-height
source: "assets/box_green.png"
text: qsTr("animation on property")
NumberAnimation on y {
to: 40; duration: 4000
}
}
当对象被点击时,其 y 坐标会被重置为起始状态,这点对三个对象都有效。对第一个对象来说,动画运行时,点击不会重置 y 坐标。
这在视觉上会令人不适,因为 y 坐标会在动画开始前的几分之一秒被设定为一个新值。应该避免这种属性竞争。
# 第二个对象
第二个对象使用 Behavior on
行为动画。该行为告诉属性值应该渐变。该行为可以设置 Behavior
的属性 enabled: false
来禁用。
ClickableImageV2 {
id: blueBox
x: (root.width-width)/2; y: root.height-height
source: "assets/box_blue.png"
text: qsTr("behavior on property")
Behavior on y {
NumberAnimation { duration: 4000 }
}
onClicked: y = 40
// 每次点击产生随机的 y
// onClicked: y = 40 + Math.random() * (205-40)
}
该对象会在被点击时开始移动(它的 y 坐标会被设置为 40)。重复点击不会重复设置,因为它的值已被设置了。
可以尝试为 y 坐标设置一个随机值(如 40 + (Math.random() \* (205-40)
)。你会看到该对象总会动画至新位置,且因为动画的时长被设置为 4 秒,所以其会自动调整移动速度。
# 第三个对象
第三个对象使用了独立动画。该动画作用在元素上,几乎可出现在文档内的任意位置。
ClickableImageV2 {
id: redBox
x: root.width-width-40; y: root.height-height
source: "assets/box_red.png"
onClicked: anim.start()
// onClicked: anim.restart()
text: qsTr("standalone animation")
NumberAnimation {
id: anim
target: redBox
properties: "y"
to: 40
duration: 4000
}
}
点击动作会通过 start()
函数启动动画。每个动画都有 start(),resume() 和 restart() 函数。此种动画包含了前文所提的其它动画更多的功能。
我们需要通过 target
属性定义动画的目标元素和需要动画的属性。本例中,还需要定义一个 to
值和 from
值,这将允许重启动画。
点击背景图会重置所有对象的位置。第一个对象无法被重启,除非重启程序,重启程序会重新加载该元素。
其它控制动画的方法
另一种启动和停止动画的方法是将一个属性绑定至动画的 running
属性。当用户能控制该属性时显得非常有用:
NumberAnimation {
// [...]
// 鼠标按下时,动画运行
running: area.pressed
}
MouseArea {
id: area
}
# 缓动曲线
动画可以控制属性值的变化过程。缓动属性能够影响属性变更的曲线。
目前我们定义的动画都使用了线性插值,因为动画的缓动属性的初值是 Easing.Linear
。最好是通过图表看看,y 坐标是被动画的属性,x 坐标是时间(时长)。线性插值会在图标的开始位置到结束位置间画出一条直线,起点值为 from
,终点值为 to
。缓和类型定义了变化曲线。
需要谨慎地为移动对象选择一个合适的缓动曲线。例如,当滑出一个页面时,初始的速度应该很慢,然后加速,最后高速滑出,类似翻书。
不应过度使用动画
与UI设计的其他方面一样,动画应该精心设计以支持 UI 流,而不是主导它。眼睛对移动物体非常敏感,所以动画极容易吸引用户的注意。
下个例子中,我们会尝试一些缓动曲线。每个缓动曲线由同一个可点击图片展示,当其被点击时,square
动画会被设置一个新的缓动曲线,并调用 restart()
以新的曲线运行动画。
本例的代码有点复杂。首先创建 EasingTypes
的网格和一个 Box
,后者受缓动曲线控制。每个缓动类型展示了动画将使用的曲线。当用户点击曲线时,盒子会根据该曲线移动。该动画是一个独立动画,目标为盒子,作用于 x 坐标,时长为 2 秒。
提示
EasingType 的内部实时渲染曲线,感兴趣的读者可以在 EasingCurves
示例中查找。
// EasingCurves.qml
import QtQuick
import QtQuick.Layouts
Rectangle {
id: root
width: childrenRect.width
height: childrenRect.height
color: '#4a4a4a'
gradient: Gradient {
GradientStop { position: 0.0; color: root.color }
GradientStop { position: 1.0; color: Qt.lighter(root.color, 1.2) }
}
ColumnLayout {
Grid {
spacing: 8
columns: 5
EasingType {
easingType: Easing.Linear
title: 'Linear'
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.InExpo
title: "InExpo"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.OutExpo
title: "OutExpo"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.InOutExpo
title: "InOutExpo"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.InOutCubic
title: "InOutCubic"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.SineCurve
title: "SineCurve"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.InOutCirc
title: "InOutCirc"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.InOutElastic
title: "InOutElastic"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.InOutBack
title: "InOutBack"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
EasingType {
easingType: Easing.InOutBounce
title: "InOutBounce"
onClicked: {
animation.easing.type = easingType
box.toggle = !box.toggle
}
}
}
Item {
height: 80
Layout.fillWidth: true
Box {
id: box
property bool toggle
x: toggle ? 20 : root.width - width - 20
anchors.verticalCenter: parent.verticalCenter
gradient: Gradient {
GradientStop { position: 0.0; color: "#2ed5fa" }
GradientStop { position: 1.0; color: "#2467ec" }
}
Behavior on x {
NumberAnimation {
id: animation
duration: 500
}
}
}
}
}
}
请试试该实例并观察动画进行时速度的变化。某些动画令人舒适,某些动画令人难受。
除了 duration
和 easing.type
外,你还可以微调动画。例如,常见的 PropertyAnimation
类型(大部分动画继承自它)还支持 easing.amplitude
,easing.overshoot
和 easing.period
属性,这些允许你微调部分动画的行为。
不是所有的缓动曲线都支持这些参数。请查询 PropertyAnimation
文档的 easing table (opens new window) ,看看哪些参数对哪些曲线有影响。
选择正确的曲线
在用户界面中为元素选择正确的动画至关重要。请牢记,动画应该支持 UI 流,而不是激怒用户。
# 组合动画
通常情况下,动画不止作用于一个属性。你可以期望同时运行几个动画,或依次运行动画,或在动画运行间隔执行脚本。
为此,可以使用组合动画。见名知意,它可以组合动画。有两种组合方式:并行或串行。可以将 SequentialAnimation
或 ParallelAnimation
作为其它动画的容器。这些分组动画本身就是动画,所以可以这样用。
# 并行动画
动画启动时,并行动画的直接子动画会并行运行。这使得你可以同时动画不同的属性。
// ParallelAnimationExample.qml
import QtQuick
BrightSquare {
id: root
property int duration: 3000
property Item ufo: ufo
width: 600
height: 400
Image {
anchors.fill: parent
source: "assets/ufo_background.png"
}
ClickableImageV3 {
id: ufo
x: 20; y: root.height-height
text: qsTr('ufo')
source: "assets/ufo.png"
onClicked: anim.restart()
}
ParallelAnimation {
id: anim
NumberAnimation {
target: ufo
properties: "y"
to: 20
duration: root.duration
}
NumberAnimation {
target: ufo
properties: "x"
to: 160
duration: root.duration
}
}
}
# 串行动画
串行动画依据从上到下的顺序依次执行每个子动画。
// SequentialAnimationExample.qml
import QtQuick
BrightSquare {
id: root
property int duration: 3000
property Item ufo: ufo
width: 600
height: 400
Image {
anchors.fill: parent
source: "assets/ufo_background.png"
}
ClickableImageV3 {
id: ufo
x: 20; y: root.height-height
text: qsTr('rocket')
source: "assets/ufo.png"
onClicked: anim.restart()
}
SequentialAnimation {
id: anim
NumberAnimation {
target: ufo
properties: "y"
to: 20
// 60% of time to travel up
duration: root.duration * 0.6
}
NumberAnimation {
target: ufo
properties: "x"
to: 400
// 40% of time to travel sideways
duration: root.duration * 0.4
}
}
}
# 嵌套动画
组合动画也可嵌套。比如,串行动画可以有两个并行动画作为子动画。我们可以用一个足球的例子来可视化。想法是把一个球从左扔到右,并动画它的行为。
要理解此动画,我们需要将其分解成几个必要的变化。需要牢记动画就是动态修改属性。以下是几种不同的变化:
从左到右的 x 变化(
X1
)y 变化,先是从下到上的
Y1
,再是从上到下的Y2
,再加一个弹跳整个动画周期的一个 360 度的旋转
ROT1
完整的动画耗时 3 秒。
先创建一个宽为 480,高为 360 的空元素作为根元素。
import QtQuick
Item {
id: root
property int duration: 3000
width: 480
height: 300
// [...]
}
将完整的动画耗时定义为一个引用,便于在动画中同步时长。
下一步是添加背景色,分别是绿色和蓝色的渐变矩形。
Rectangle {
id: sky
width: parent.width
height: 200
gradient: Gradient {
GradientStop { position: 0.0; color: "#0080FF" }
GradientStop { position: 1.0; color: "#66CCFF" }
}
}
Rectangle {
id: ground
anchors.top: sky.bottom
anchors.bottom: root.bottom
width: parent.width
gradient: Gradient {
GradientStop { position: 0.0; color: "#00FF00" }
GradientStop { position: 1.0; color: "#00803F" }
}
}
上面的蓝色天空矩形的高度是 200 像素,下方的矩形的顶部锚定了天空的底部,底部锚定了根元素的底部。
让我们在绿色部分添加足球。该足球是一张图片,存储在 “assets/soccer_ball.png”。开始的时候,我们将其放在左下角。
Image {
id: ball
x: 0; y: root.height-height
source: "assets/soccer_ball.png"
MouseArea {
anchors.fill: parent
onClicked: {
ball.x = 0
ball.y = root.height-ball.height
ball.rotation = 0
anim.restart()
}
}
}
该图片附加了一个点击区域。若足球被点击,足球将重置位置并重新开始动画。
接下来,我们添加两个 y 变化。
SequentialAnimation {
id: anim
NumberAnimation {
target: ball
properties: "y"
to: 20
duration: root.duration * 0.4
}
NumberAnimation {
target: ball
properties: "y"
to: 240
duration: root.duration * 0.6
}
}
这指定了动画耗时的前 40% 是向上的动画,后 60% 是向下的动画,两个动画依次运行。变化目前是线性的,没有使用缓动曲线,后续我们会为其添加,目前我们只关注动画的运行。
下一步,我们要添加 x 的变化。x 的变化应与 y 的变化同步,所以,我们需要将 y 的变化和 x 的变化封装到并行动画中。
ParallelAnimation {
id: anim
SequentialAnimation {
// ... our Y1, Y2 animation
}
NumberAnimation { // X1 animation
target: ball
properties: "x"
to: 400
duration: root.duration
}
}
最后,我们期望球能转起来。为此,我们需要在并行动画中添加一个新的动画。我们选择 RotationAnimation
,因为它适用于旋转。
ParallelAnimation {
id: anim
SequentialAnimation {
// ... our Y1, Y2 animation
}
NumberAnimation { // X1 animation
// X1 animation
}
RotationAnimation {
target: ball
properties: "rotation"
to: 720
duration: root.duration
}
}
这就是完整的动画序列。还剩一件事,就是为球提供正确的缓动曲线。对于 Y1 动画,我们使用 Easing.OutCirc
曲线,因为它应该看起来更像一个圆形运动。Y2 使用 Easing.OutBounce
来给球一个弹跳,弹跳应该发生在结尾(尝试使用 Easing.InBounce
,你会看到弹跳开始得很快)。
The X1 and ROT1 animation are left as-is, with a linear curve.
以下是完整的动画代码:
ParallelAnimation {
id: anim
SequentialAnimation {
NumberAnimation {
target: ball
properties: "y"
to: 20
duration: root.duration * 0.4
easing.type: Easing.OutCirc
}
NumberAnimation {
target: ball
properties: "y"
to: root.height-ball.height
duration: root.duration * 0.6
easing.type: Easing.OutBounce
}
}
NumberAnimation {
target: ball
properties: "x"
to: root.width-ball.width
duration: root.duration
}
RotationAnimation {
target: ball
properties: "rotation"
to: 720
duration: root.duration
}
}