QML

Анимированные переходы между экранами (StackView, transitions)

Введение

StackView — стандартный компонент Qt Quick Controls для организации навигации между экранами по принципу стека (новый экран «накладывается» сверху, возврат снимает верхний экран) — аналогично навигации в большинстве мобильных приложений. В статье разберём настройку анимированных переходов между экранами через StackView, дополняя более экзотический shader-based подход из статьи 271 стандартным, встроенным механизмом.

Концепция

StackView поддерживает настройку переходов через свойство pushEnter/pushExit/popEnter/popExit (анимации для появляющегося и исчезающего экрана при переходе «вперёд» через push() и «назад» через pop()) или через более комплексный replaceEnter/replaceExit для полной замены текущего экрана. Каждое из этих свойств принимает Transition, описывающий, что происходит с соответствующим экраном (новым или старым) во время перехода — например, сдвиг по горизонтали в сочетании с изменением прозрачности.

Пример кода: Анимированные переходы между

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 400
    height: 600
    visible: true

    StackView {
        id: stackView
        anchors.fill: parent
        initialItem: homeScreen

        // Переход при push() — новый экран появляется
        pushEnter: Transition {
            ParallelAnimation {
                NumberAnimation { property: "x"; from: stackView.width; to: 0; duration: 300; easing.type: Easing.OutCubic }
                NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 200 }
            }
        }

        // Переход при push() — старый экран уходит назад и затемняется
        pushExit: Transition {
            ParallelAnimation {
                NumberAnimation { property: "x"; from: 0; to: -stackView.width * 0.3; duration: 300; easing.type: Easing.OutCubic }
                NumberAnimation { property: "opacity"; from: 1; to: 0.5; duration: 300 }
            }
        }

        // Переход при pop() — обратное движение к предыдущему экрану
        popEnter: Transition {
            ParallelAnimation {
                NumberAnimation { property: "x"; from: -stackView.width * 0.3; to: 0; duration: 300; easing.type: Easing.OutCubic }
                NumberAnimation { property: "opacity"; from: 0.5; to: 1; duration: 300 }
            }
        }

        popExit: Transition {
            ParallelAnimation {
                NumberAnimation { property: "x"; from: 0; to: stackView.width; duration: 300; easing.type: Easing.OutCubic }
                NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 200 }
            }
        }
    }

    Component {
        id: homeScreen
        Page {
            title: "Главная"
            Button {
                anchors.centerIn: parent
                text: "Открыть детали"
                onClicked: stackView.push(detailsScreen)
            }
        }
    }

    Component {
        id: detailsScreen
        Page {
            title: "Детали"
            Button {
                anchors.centerIn: parent
                text: "Назад"
                onClicked: stackView.pop()
            }
        }
    }
}

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

Четыре свойства перехода (pushEnter, pushExit, popEnter, popExit) полностью независимы и контролируют разные стороны одного и того же визуального перехода — pushEnter определяет, как ведёт себя новый экран при переходе вперёд (вход справа со сдвигом и нарастанием прозрачности), а pushExit — как ведёт себя старый, уходящий экран в тот же самый момент (лёгкий уход влево с частичным затемнением, создающий эффект «параллакса» глубины). ParallelAnimation объединяет одновременное изменение нескольких свойств (x и opacity) в рамках одного перехода, синхронизированных по продолжительности.

Симметричное определение popEnter/popExit как обратного движения (старый экран, который при push уходил влево, при pop возвращается обратно справа) создаёт ощущение логически согласованной, «обратимой» навигации — пользователь интуитивно воспринимает переход назад как зеркальное отражение перехода вперёд, что усиливает ощущение предсказуемости и качества навигации в приложении.

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

  • stackView.width внутри определения Transition, использованного как начальное/конечное значение анимации, может быть не определено корректно в момент инициализации StackView, если переходы определены до того, как сам компонент получил итоговый размер — для надёжности стоит убедиться, что геометрия StackView стабильна к моменту первого использования переходов, либо использовать привязки, которые пересчитываются динамически, а не захватывают значение width единожды в момент создания.
  • Слишком долгая продолжительность переходов (duration значительно больше 300-400 мс) может создать ощущение медленного, «тормозящего» приложения, особенно если пользователь часто и быстро перемещается между экранами — переходы должны быть достаточно заметными для визуальной обратной связи, но не настолько долгими, чтобы заставлять пользователя ждать завершения анимации перед тем, как продолжить взаимодействие с приложением.
  • Несогласованность между анимацией перехода и реальным содержимым экрана, если содержимое страницы (например, список с собственной анимацией прокрутки) продолжает собственную независимую анимацию одновременно с анимацией перехода StackView — это может создать визуально перегруженный, сбивающий с толку эффект; для чистоты восприятия перехода стоит по возможности приостанавливать или сбрасывать независимые анимации содержимого экрана на время самого перехода между экранами.
  • Повторное нажатие кнопки навигации во время еще не завершённого предыдущего перехода (например, быстрые повторные нажатия «Открыть детали») может поставить в очередь несколько одновременных push-операций — StackView обычно справляется с этим корректно на уровне базовой логики стека, но стоит явно протестировать пользовательский опыт при таком «нетерпеливом» взаимодействии, возможно, временно блокируя повторные нажатия кнопок навигации на время активного перехода.