The Problem
Transitions handle state changes, but not all animations are state-driven. A loading spinner spins continuously. A notification dot pulses when a new alert arrives. A volume bar animates smoothly when you change the level. These are standalone animations — they run on their own schedule, not in response to state changes.
The Naive Approach
You could use a Timer to manually update property values:
Timer {
interval: 16; repeat: true
onTriggered: {
spinner.rotation = (spinner.rotation + 6) % 360
}
}This works but reinvents the wheel. You're writing animation logic that QML's animation framework already provides — with better performance and less code.
Standalone animations are like ceiling fans: they run continuously at a set speed, independent of anything else. State-driven animations (transitions) are like automatic doors: they only move when triggered by a state change.
The Idea
QML provides animation types that control property values over time:
NumberAnimation— animates numeric propertiesColorAnimation— animates color propertiesOpacityAnimator— specialized opacity animation (runs on the rendering thread)RotationAnimator— specialized rotation animation (runs on the rendering thread)SequentialAnimation— runs child animations one after anotherParallelAnimation— runs child animations simultaneouslyPropertyAnimation— base type for any property animation
Let's Build It
import QtQuick
import QtQuick.Window
Window {
width: 500
height: 400
visible: true
title: "Animations"
Row {
anchors.centerIn: parent
spacing: 40
// 1. Spinning loader
Rectangle {
id: spinner
width: 60; height: 60; radius: 30
color: "transparent"
border.color: "#4ecdc4"; border.width: 4
Rectangle {
anchors.top: parent.top; anchors.horizontalCenter: parent.horizontalCenter
width: 6; height: 16; radius: 3
color: "#4ecdc4"
}
// Continuous rotation
NumberAnimation on rotation {
from: 0; to: 360
duration: 1000
loops: Animation.Infinite
}
}
// 2. Pulsing dot
Rectangle {
id: dot
width: 40; height: 40; radius: 20
color: "#ff6b6b"
Rectangle {
anchors.fill: parent; radius: parent.radius
color: "#ff6b6b"; opacity: 0.3
NumberAnimation on scale {
from: 0.8; to: 1.8
duration: 800
loops: Animation.Infinite
easing.type: Easing.InOutQuad
}
NumberAnimation on opacity {
from: 0.5; to: 0
duration: 800
loops: Animation.Infinite
easing.type: Easing.InOutQuad
}
}
}
// 3. Bouncing ball
Column {
spacing: 6
Text { text: "Bounce"; color: "#888"; font.pixelSize: 12; anchors.horizontalCenter: parent.horizontalCenter }
Rectangle {
id: ball
width: 40; height: 40; radius: 20
color: "#ffe66d"
y: 0
SequentialAnimation on y {
loops: Animation.Infinite
NumberAnimation {
to: 80
duration: 400
easing.type: Easing.OutQuad
}
NumberAnimation {
to: 0
duration: 400
easing.type: Easing.InQuad
}
PauseAnimation { duration: 200 }
}
}
}
}
// 4. Color-cycling bar
Rectangle {
anchors { bottom: parent.bottom; left: parent.left; right: parent.right; bottomMargin: 20; margins: 20 }
height: 8
radius: 4
ColorAnimation on color {
from: "#ff6b6b"
to: "#4ecdc4"
duration: 2000
loops: Animation.Infinite
}
}
// 5. Property animation via Behavior
Rectangle {
id: animatedBox
x: 20; y: 20
width: 100; height: 60; radius: 8
color: "#45b7d1"
Behavior on x {
NumberAnimation { duration: 300; easing.type: Easing.OutBounce }
}
Behavior on y {
NumberAnimation { duration: 300; easing.type: Easing.OutBounce }
}
Text { text: "Click"; anchors.centerIn: parent; color: "white"; font.bold: true }
MouseArea {
anchors.fill: parent
onClicked: {
animatedBox.x = Math.random() * (parent.width - 100)
animatedBox.y = Math.random() * (parent.height - 80)
}
}
}
}Animation patterns:
NumberAnimation on rotation { loops: Animation.Infinite }— infinite looping animation.fromandtodefine the range.SequentialAnimation— runs child animations in order. Here, bouncing down withOutQuadeasing, then up withInQuadeasing, then pause.Behavior on x { NumberAnimation { ... } }— wheneverxchanges, animate to the new value. The animation plays for any change, not just state changes.ColorAnimation— cycles the bar's color between red and teal continuously.
Easing Curves
The easing type dramatically changes how an animation feels:
| Easing | Feel |
|---|---|
Linear | Mechanical, constant speed |
InQuad / OutQuad | Gentle acceleration/deceleration |
InOutQuad | Smooth start and end (most natural) |
OutBounce | Bounces at the end (playful) |
OutElastic | Overshoots and snaps back |
InBack | Pulls back before moving forward |
Common Mistake
Animating on the UI thread causing jank. OpacityAnimator and RotationAnimator run on the scene graph's render thread, not the UI thread. Use them for performance-critical animations. NumberAnimation runs on the UI thread and can jank if the UI thread is busy.
Forgetting alwaysRunToEnd. If you stop an animation (by setting running: false), it jumps to the final value immediately. Set alwaysRunToEnd: true on the animation to let it finish gracefully.
Exercises
- Standalone animations run continuously or on-demand via NumberAnimation, ColorAnimation, etc.
- Use SequentialAnimation and ParallelAnimation to compose complex animations
- Behavior animates property changes wherever they originate
- Animator types (OpacityAnimator, RotationAnimator) run on the render thread for better performance