Анимация в QML
Анимации применяются к изменениям свойств. Анимация определяет кривую интерполяции от одного значения к другому при изменении свойства. Эти анимационные кривые создают плавные переходы от одного значения к другому.
Анимация определяется рядом целевых свойств, которые необходимо анимировать, кривой плавности для кривой интерполяции и продолжительностью. Все анимации в Qt Quick управляются одним и тем же таймером и поэтому синхронизируются. Это улучшает производительность и визуальное качество анимации.
Анимации управляют изменением свойств с помощью интерполяции значений.
Это фундаментальная концепция. QML основана на элементах, свойствах и сценариях. Каждый элемент имеет десятки свойств, и каждое свойство ждет, когда вы его анимируете. В данной книге вы увидите, что это пространство для захватывающих экспериментов.
Вы поймаете себя на том, что смотрите на некоторые анимации и просто восхищаетесь их красотой, а также своей творческой гениальностью. Пожалуйста, помните: анимация управляет изменениями свойств, и каждый элемент предоставляет в ваше распоряжение десятки свойств.
Разблокируем эту мощь!
// animation.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
постепенно перемещает координату x
от исходного положения объекта до 240px. Анимация вращения запускается от текущего угла до 360 градусов. Обе анимации выполняются параллельно и запускаются сразу после загрузки пользовательского интерфейса.
Вы можете поэкспериментировать с анимацией, изменив свойства to
и duration
, или можете добавить другую анимацию (например, для свойства opacity
или даже для scale
). Комбинируя их, может показаться, что объект исчезает в глубоком космосе. Попробуйте!
Элементы анимации
Существует несколько типов элементов анимации, каждый из которых оптимизирован для конкретного случая использования. Вот список самых известных анимаций:
PropertyAnimation
– анимирует изменения значений свойств.NumberAnimation
– анимирует изменения в значениях типаqreal
.ColorAnimation
– анимирует изменения значений цвета.RotationAnimation
– анимирует изменения значений вращения.
Помимо этих основных и широко используемых элементов анимации, Qt Quick также предоставляет более специализированные анимации для конкретных случаев использования:
PauseAnimation
– останавливает анимацию.SequentialAnimation
– позволяет запускать анимации последовательно.ParallelAnimation
– позволяет запускать анимации параллельно.AnchorAnimation
– анимирует изменения значений привязки.ParentAnimation
– анимирует изменения в родительских значениях.SmoothedAnimation
– позволяет свойству плавно отслеживать значение.SpringAnimation
– позволяет свойству отслеживать значение в пружинном движении.PathAnimation
– анимирует элемент вдоль пути.Vector3dAnimation
– анимирует изменения значенийQVector3d
.
Позже мы научимся создавать последовательность анимаций. При работе над более сложной анимацией иногда возникает необходимость изменить свойство или запустить скрипт во время текущей анимации. Для этого Qt Quick предлагает элементы действий, которые можно использовать везде, где можно использовать другие элементы анимации:
PropertyAction
– определяет немедленные изменения свойств во время анимации.ScriptAction
– определяет скрипты, которые будут запускаться во время анимации.
Основные типы анимации будут обсуждаться в этой главе на конкретных небольших примерах.
Применение анимации
Анимацию можно применять несколькими способами:
- Анимация свойства – запускается автоматически после полной загрузки элемента.
- Поведение свойства – запускается автоматически при изменении значения свойства.
- Автономная анимация – запускается, когда анимация запускается явно с помощью
start()
, или для ее запуска установлено значениеtrue
(например, привязкой свойства).
Позже мы также увидим, как анимацию можно использовать внутри переходов между состояниями.
Кликабельное изображение V2
Чтобы продемонстрировать использование анимации, мы повторно использовали наш компонент ClickableImage
из предыдущей главы и расширили его текстовым элементом.
// ClickableImageV2.qml
// Простое изображение, на которое можно кликнуть
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
столбца. Мы выставили свойства источника текста и изображения, а также сигнал клика. Мы также хотели, чтобы текст был такой же ширины, как и изображение, и чтобы он переносился по словам на новую строку. Последнего мы достигаем, используя свойство wrapMode
элемента Text
.
Зависимость геометрии родительского и дочернего элементов
Из-за инверсии зависимости геометрии (геометрия родительского элемента зависит от геометрии дочернего элемента) мы не можем установить width
/height
для ClickableImageV2
, так как это нарушит нашу привязку width
/height
.
Вы должны предпочесть, чтобы геометрия дочернего элемента зависела от геометрии родителя, если элемент больше похож на контейнер для других элементов и должен адаптироваться к геометрии родителя.
Подъем объектов
Все три объекта находятся в одном и том же положении по оси y (y=200
). Всем им нужно добраться до y=40
, каждый использует свой метод с разными побочными эффектами и функциями.
Первый объект
ClickableImageV2 {
id: greenBox
x: 40; y: root.height-height
source: "assets/box_green.png"
text: "animation on property"
NumberAnimation on y {
to: 40; duration: 4000
}
}
Первый объект перемещается с использованием стратегии «Анимация свойства». Анимация начинается немедленно.
При клике по объекту, его y-позиция сбрасывается до начальной позиции, и это применяется ко всем объектам. На первый объект этот сброс не оказывает никакого влияния, пока выполняется анимация.
Это может визуально мешать, так как y-координата устанавливается в новое значение за долю секунды до начала анимации. Таких конкурирующих изменений свойств следует избегать.
Второй объект
ClickableImageV2 {
id: blueBox
x: (root.width-width)/2; y: root.height-height
source: "assets/box_blue.png"
text: "behavior on property"
Behavior on y {
NumberAnimation { duration: 4000 }
}
onClicked: y = 40
// случайное значение y при каждом клике
// onClicked: y = 40 + Math.random() * (205-40)
}
Второй объект перемещается с помощью анимации поведения. Это поведение сообщает свойству, что оно должно анимировать каждое изменение значения. Поведение можно отключить, установив enable: false
для элемента Behavior
.
Объект начнет двигаться, когда вы кликните на него (его 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: "standalone animation"
NumberAnimation {
id: anim
target: redBox
properties: "y"
to: 40
duration: 4000
}
}
Третий объект использует автономную анимацию. Эта анимация определяется как отдельный элемент и может находиться практически в любом месте документа.
Клик запустит анимацию с помощью функции анимации start()
. Каждая анимация имеет функции start()
, stop()
, resume()
и restart()
. Эта анимация содержит гораздо больше информации, чем предыдущие типы анимации.
Нам нужно определить цель (target
), то есть элемент, который нужно анимировать, а также имена свойств, которые мы хотим анимировать. Нам также необходимо определить значение to
и, в данном случае, значение from
, что позволит перезапустить анимацию.
Клик по фону вернет все объекты в исходное положение. Первый объект не может быть перезапущен, кроме как перезапуском программы, который запускает повторную загрузку элемента.
Другие способы управления анимацией
Другой способ запустить/остановить анимацию – привязать свойство к свойству анимации running
. Это особенно полезно, когда свойства управляются пользовательским вводом:
NumberAnimation {
...
// анимация запускается при нажатии клавиши мыши
running: area.pressed
}
MouseArea {
id: area
}
Кривые плавности
Изменением значения свойства можно управлять с помощью анимации. Атрибуты easing
позволяют влиять на интерполяционную кривую изменения свойства.
Все анимации, которые мы уже определили, используют линейную интерполяцию, потому что первоначальный тип плавности анимации – Easing.Linear
. Это лучше всего визуализировать с помощью небольшого графика, где ось y – это свойство, которое нужно анимировать, а ось x – это время (длительность). Линейная интерполяция нарисует прямую линию от значения from
в начале анимации до значения to
в конце анимации. Таким образом, тип плавности определяет кривую изменений.
Типы плавности должны выбираться тщательно, чтобы поддерживать естественность движущегося объекта. Например, когда страница перелистывается, сначала она должна двигаться медленно, а затем набирать скорость и, наконец, двигаться с высокой скоростью, подобно перелистыванию страницы книги.
Не следует злоупотреблять анимацией.
Как и в случае с другими аспектами дизайна пользовательского интерфейса, анимация должна быть тщательно проработана, чтобы поддерживать взаимодействие с пользовательским интерфейсом, а не доминировать над ним. Глаз очень чувствителен к движущимся объектам, а анимация может легко отвлечь пользователя.
В следующем примере мы попробуем несколько кривых плавности. Каждая кривая плавности отображается кликабельным изображением, при клике на который, устанавливается новый тип плавности, а затем вызывается функция restart()
для запуска анимации с новой кривой.
Код для этого примера немного усложнен. Сначала мы создаем сетку EasingTypes
и блок Box
, который контролируется типами плавности. Объект EasingType
просто отображает кривую, которую блок будет использовать для своей анимации. Когда пользователь нажимает на кривую плавности, блок перемещается в соответствии с этой кривой плавности. Сама анимация представляет собой автономную анимацию с целью, установленной на блок и настроенной для анимации свойства 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
, чтобы проверить, влияет ли параметр на кривую плавности.
Выбирайте правильную анимацию
Выбор правильной анимации для элемента в контексте пользовательского интерфейса имеет решающее значение для результата. Помните, что анимация должна поддерживать взаимодействие с пользовательским интерфейсом, а не раздражать пользователя.
Группировка анимаций
Часто анимация будет более сложной, чем анимация просто одного свойства. Возможно, вы захотите запустить несколько анимаций одновременно или одну за другой или даже выполнить скрипт между двумя анимациями.
Для этого можно использовать сгруппированные анимации. Как следует из названия, анимации можно группировать. Группировка может осуществляться двумя способами: параллельным или последовательным. Вы можете использовать элементы SequentialAnimation
и ParallelAnimation
, которые действуют как контейнеры анимаций для других элементов анимации. Эти сгруппированные анимации сами по себе являются анимациями и могут использоваться именно так.
Все прямые дочерние анимации параллельной анимации запускаются при запуске параллельно. Это позволяет одновременно анимировать разные свойства.
// parallelanimation.qml
import QtQuick
BrightSquare {
id: root
width: 600
height: 400
property int duration: 3000
property Item ufo: ufo
Image {
anchors.fill: parent
source: "assets/ufo_background.png"
}
ClickableImageV3 {
id: ufo
x: 20; y: root.height-height
text: '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
width: 600
height: 400
property int duration: 3000
property Item ufo: ufo
Image {
anchors.fill: parent
source: "assets/ufo_background.png"
}
ClickableImageV3 {
id: ufo
x: 20; y: root.height-height
text: 'rocket'
source: "assets/ufo.png"
onClicked: anim.restart()
}
SequentialAnimation {
id: anim
NumberAnimation {
target: ufo
properties: "y"
to: 20
// 60% от продолжительности перемещается вверх
duration: root.duration*0.6
}
NumberAnimation {
target: ufo
properties: "x"
to: 400
// 40% от продолжительности перемещается по горизонтали
duration: root.duration*0.4
}
}
}
Сгруппированные анимации также могут быть вложенными. Например, последовательная анимация может иметь в качестве дочерних две параллельные анимации и т. д. Мы можем визуализировать это на примере футбольного мяча. Идея состоит в том, чтобы бросить мяч слева направо и анимировать его поведение.
Чтобы понять эту анимацию, нам нужно разложить ее на отдельные преобразования объекта. Мы должны помнить, что анимация анимирует изменения свойств. Вот эти преобразования:
- перемещение по оси x слева направо (
X1
); - перемещение по оси y снизу вверх (
Y1
), за которым следует перемещение сверху вниз (Y2
) и далее подпрыгивание; - вращение на 360 градусов на протяжении всей анимации (
ROT1
).
Вся анимация должна занимать три секунды.
Мы начинаем с пустого элемента в качестве корневого элемента шириной 480 и высотой 300.
import QtQuick
Item {
id: root
width: 480
height: 300
property int duration: 3000
...
}
Мы определили общую продолжительность анимации в качестве эталона для лучшей синхронизации частей анимации.
Следующим шагом будет добавление фона, который в нашем случае представляет собой 2 прямоугольника с зеленым и синим градиентами.
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 {
// ... наши анимации Y1, Y2
}
NumberAnimation { // анимация X1
target: ball
properties: "x"
to: 400
duration: root.duration
}
}
И в конце, мы хотели бы, чтобы мяч вращался. Для этого нам нужно добавить еще одну анимацию к параллельной анимации. Мы выбираем RotationAnimation
, так как она специализируется на вращении.
ParallelAnimation {
id: anim
SequentialAnimation {
// ... наши анимации Y1, Y2
}
NumberAnimation { // анимация X1
// анимация X1
}
RotationAnimation {
target: ball
properties: "rotation"
to: 720
duration: root.duration
}
}
Вот и вся последовательность анимаций. Осталось только создать правильные кривые плавности для движений мяча. Для анимации Y1 мы используем кривую Easing.OutCirc
, так как это должно больше походить на круговое движение. Y2 улучшается с помощью Easing.OutBounce
, чтобы дать мячу отскок, и отскок должен произойти в конце (попробуйте с Easing.InBounce
, и вы увидите, что отскок начинается сразу).
Анимации X1 и ROT1 оставлены как есть, с линейной кривой.
Вот окончательный код анимации:
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
}
}