QML

Lottie-анимации в Qt Quick: интеграция сторонних библиотек

Введение

Lottie — популярный формат векторных анимаций (на основе JSON), экспортируемых из Adobe After Effects и широко используемых в мобильной и веб-разработке для сложных, профессионально созданных дизайнерами анимаций (анимированные иконки, иллюстрации, индикаторы состояния), которые было бы непрактично воссоздавать вручную через QML-примитивы. В статье разберём интеграцию Lottie-анимаций в Qt Quick приложение через сторонние библиотеки.

Концепция

Qt не предоставляет встроенной поддержки Lottie — для воспроизведения таких анимаций используются сторонние библиотеки, например rlottie (компактная, без зависимостей C++ библиотека рендеринга Lottie, разработанная изначально в Samsung) в сочетании с собственным тонким QML-компонентом-обёрткой, который использует rlottie для рендеринга каждого кадра анимации в QImage, а затем отображает результат как обычное QML-изображение, обновляемое по таймеру в соответствии с частотой кадров анимации.

Пример кода: Lottie-анимации в Qt Quick

# CMakeLists.txt — подключение rlottie как зависимости
find_package(rlottie REQUIRED)
target_link_libraries(MyApp PRIVATE rlottie::rlottie)
// lottieitem.h — QML-компонент, оборачивающий rlottie для использования в QML-сцене
#pragma once
#include <QQuickPaintedItem>
#include <QPainter>
#include <QTimer>
#include <rlottie.h>
#include <memory>

class LottieItem : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged)
    Q_PROPERTY(bool playing READ isPlaying WRITE setPlaying NOTIFY playingChanged)

public:
    explicit LottieItem(QQuickItem *parent = nullptr) : QQuickPaintedItem(parent)
    {
        connect(&m_frameTimer, &QTimer::timeout, this, &LottieItem::advanceFrame);
    }

    QString source() const { return m_source; }
    void setSource(const QString &source)
    {
        m_source = source;
        m_animation = rlottie::Animation::loadFromFile(source.toStdString());
        if (m_animation) {
            const double frameRate = m_animation->frameRate();
            m_frameTimer.setInterval(static_cast<int>(1000.0 / frameRate));
            m_totalFrames = static_cast<int>(m_animation->totalFrame());
        }
        emit sourceChanged();
        update();
    }

    bool isPlaying() const { return m_playing; }
    void setPlaying(bool playing)
    {
        m_playing = playing;
        if (playing) {
            m_frameTimer.start();
        } else {
            m_frameTimer.stop();
        }
        emit playingChanged();
    }

    void paint(QPainter *painter) override
    {
        if (!m_animation) return;

        rlottie::Surface surface(
            reinterpret_cast<uint32_t *>(m_buffer.bits()),
            width(), height(), m_buffer.bytesPerLine());

        m_animation->renderSync(m_currentFrame, surface);
        painter->drawImage(0, 0, m_buffer);
    }

private slots:
    void advanceFrame()
    {
        m_currentFrame = (m_currentFrame + 1) % m_totalFrames; // цикличное воспроизведение
        update(); // запрос на повторный вызов paint()
    }

signals:
    void sourceChanged();
    void playingChanged();

private:
    QString m_source;
    std::unique_ptr<rlottie::Animation> m_animation;
    QTimer m_frameTimer;
    QImage m_buffer{width(), height(), QImage::Format_ARGB32_Premultiplied};
    int m_currentFrame = 0;
    int m_totalFrames = 1;
    bool m_playing = false;
};
// Использование зарегистрированного C++ компонента в QML
import MyApp 1.0

LottieItem {
    width: 200
    height: 200
    source: ":/animations/success-checkmark.json"
    playing: true
}

Пояснения к коду

LottieItem наследует QQuickPaintedItem — базовый класс для QML-элементов с кастомной отрисовкой через QPainter, что подходит именно для случая, когда содержимое генерируется не стандартными QML-примитивами, а внешней библиотекой рендеринга. setSource() загружает Lottie-файл через rlottie::Animation::loadFromFile() и считывает частоту кадров и общее количество кадров анимации, используемые для настройки таймера и логики циклического воспроизведения.

paint() вызывается каждый раз, когда QML Scene Graph решает перерисовать элемент — внутри он создаёт rlottie::Surface, указывающую на буфер QImage, и просит rlottie синхронно отрендерить конкретный кадр анимации (m_currentFrame) непосредственно в этот буфер, после чего результат отрисовывается через обычный QPainter::drawImage(). advanceFrame(), вызываемый таймером с интервалом, соответствующим частоте кадров анимации, увеличивает текущий кадр (с цикличным переходом к началу через %) и вызывает update(), что запрашивает у Qt Quick повторный вызов paint() на следующем цикле рендеринга.

Подводные камни

  • Синхронный рендеринг кадра (renderSync) внутри paint(), выполняемого в потоке рендеринга Qt Quick, может занимать заметное время для сложных Lottie-анимаций с большим количеством векторных слоёв — если рендеринг одного кадра занимает больше времени, чем доступный бюджет кадра (статья 274), это может привести к просадке FPS всего приложения, а не только самой анимации; для сложных Lottie-файлов стоит профилировать время рендеринга одного кадра отдельно.
  • Несовпадение частоты кадров Lottie-анимации (часто 24, 25 или 30 FPS, типичные для анимации, экспортированной из After Effects) с частотой обновления экрана (обычно 60 Гц) может создать едва заметную неравномерность темпа воспроизведения — для большинства декоративных анимаций эта разница незаметна, но для очень точных, синхронизированных с другими элементами интерфейса анимаций стоит явно учитывать эту разницу в частотах.
  • Размер буфера QImage, заданный единожды в инициализаторе члена класса (m_buffer{width(), height(), ...}) до того, как реальные размеры QML-элемента установлены через привязки width/height в QML, может привести к неверному, нулевому или некорректному начальному размеру буфера — для надёжной работы нужно явно пересоздавать буфер при изменении геометрии элемента (переопределяя geometryChange()), а не полагаться только на единократную инициализацию в конструкторе.
  • Зависимость от стороннего, не входящего в сам Qt SDK пакета (rlottie) требует отдельного управления этой зависимостью в системе сборки проекта (через vcpkg/Conan, основной цикл статей про CMake, статьи 33, 40) и означает, что обновления самой библиотеки rlottie (исправления багов, новые возможности формата Lottie) происходят независимо от цикла обновлений Qt, и команде нужно самостоятельно следить за этой зависимостью отдельно.