Анимация в QML

Добавлено 7 марта 2022 в 20:02

Анимации применяются к изменениям свойств. Анимация определяет кривую интерполяции от одного значения к другому при изменении свойства. Эти анимационные кривые создают плавные переходы от одного значения к другому.

Анимация определяется рядом целевых свойств, которые необходимо анимировать, кривой плавности для кривой интерполяции и продолжительностью. Все анимации в 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
    }
}
начнем с последовательной анимации для двух перемещений по оси y

Этот код указывает, что 40% общей продолжительности анимации приходится на анимацию вверх и 60% – на анимацию вниз, при этом каждая анимация выполняется последовательно за другой. Преобразования анимированы по линейному пути, но кривая плавности пока не указана. Кривые плавности будут добавлены позже, в данный момент мы концентрируемся на анимации преобразований.

Далее нам нужно добавить перемещение по оси x. Перемещение по оси x должно выполняться параллельно с перемещением по оси y, поэтому нам нужно инкапсулировать последовательность перемещений по оси y в параллельную анимацию вместе с перемещением по оси x.

ParallelAnimation {
    id: anim
    SequentialAnimation {
        // ... наши анимации Y1, Y2
    }
    NumberAnimation { // анимация X1
        target: ball
        properties: "x"
        to: 400
        duration: root.duration
    }
}
добавление перемещения по оси x

И в конце, мы хотели бы, чтобы мяч вращался. Для этого нам нужно добавить еще одну анимацию к параллельной анимации. Мы выбираем 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
    }
}

Теги

GUI / Графический интерфейс пользователяQMLQtQtQuickАнимацияОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.