Введение
Пока данные загружаются (с сервера, из базы данных, из медленного хранилища), пустой экран или статичный индикатор загрузки создают ощущение «зависшего» приложения. Skeleton-загрузка — показ упрощённых, анимированных placeholder-блоков, повторяющих форму будущего реального содержимого — создаёт ощущение, что приложение уже активно работает, и часто воспринимается пользователями как более быстрая загрузка, даже при объективно той же реальной задержке. В статье разберём реализацию skeleton-эффекта и нескольких связанных микро-анимаций в QML.
Концепция
Skeleton-блок — обычно прямоугольник или другая простая форма с приглушённым цветом, поверх которого движется анимированный градиент («блик»), создающий эффект «мерцания» или «пробегающей волны» — это базовая, легко реализуемая в QML анимация через LinearGradient/Shape или более простой подход через анимацию позиции полупрозрачного прямоугольника поверх базового. Микро-анимации (небольшие, едва заметные движения при наведении, клике, появлении элемента) дополняют ощущение отзывчивости интерфейса, давая мгновенную визуальную обратную связь на действия пользователя.
Пример кода: Skeleton-загрузка и микро-анимации
// SkeletonLoader.qml — переиспользуемый компонент skeleton-плейсхолдера
import QtQuick
Rectangle {
id: skeleton
color: "#e5e7eb"
radius: 4
// Полупрозрачный "блик", непрерывно движущийся слева направо поверх базового прямоугольника
Rectangle {
id: shimmer
width: parent.width * 0.4
height: parent.height
anchors.verticalCenter: parent.verticalCenter
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#00ffffff" }
GradientStop { position: 0.5; color: "#40ffffff" }
GradientStop { position: 1.0; color: "#00ffffff" }
}
SequentialAnimation on x {
loops: Animation.Infinite
NumberAnimation { from: -shimmer.width; to: skeleton.width; duration: 1200; easing.type: Easing.Linear }
PauseAnimation { duration: 300 } // короткая пауза перед следующим "пробегом" блика
}
}
}
// Использование набора скелетонов, имитирующих будущую структуру карточки контента
import QtQuick
Column {
spacing: 12
visible: isLoading // переключение между скелетоном и реальным содержимым по флагу загрузки
Row {
spacing: 12
SkeletonLoader { width: 60; height: 60; radius: 30 } // аватар
Column {
spacing: 6
SkeletonLoader { width: 150; height: 16 } // имитация строки заголовка
SkeletonLoader { width: 100; height: 12 } // имитация строки подзаголовка
}
}
SkeletonLoader { width: parent.width; height: 80 } // имитация блока основного текста
}
// Микро-анимация — лёгкое увеличение элемента при наведении, мгновенная обратная связь
Rectangle {
id: card
width: 200; height: 100
radius: 8
color: "white"
scale: hoverHandler.hovered ? 1.03 : 1.0
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
HoverHandler { id: hoverHandler }
}
Пояснения к коду
SkeletonLoader реализует эффект мерцания через дочерний Rectangle с горизонтальным градиентом — прозрачный по краям, едва заметно более светлый в центре, что создаёт визуальный «блик» — и SequentialAnimation, непрерывно перемещающий этот блик от левого края за пределы правого края родительского прямоугольника, с короткой паузой перед повторением цикла. Композиция нескольких SkeletonLoader разной формы и размера (круглый для аватара, узкие прямоугольники для текстовых строк, широкий блок для основного текста) создаёт визуальное «предвосхищение» реальной структуры контента, который появится после завершения загрузки.
Пример микро-анимации с HoverHandler и Behavior on scale показывает простой, но эффективный паттерн интерактивной обратной связи — небольшое (всего 3%) увеличение карточки при наведении курсора, плавно анимированное через Behavior, создаёт ощущение «отзывчивости» элемента без необходимости в сложной логике — пользователь интуитивно воспринимает элемент как интерактивный благодаря этой едва заметной, но ощутимой реакции на наведение.
Подводные камни
- Skeleton-загрузка, показываемая слишком долго (если реальная загрузка данных занимает значительно больше пары секунд), начинает восприниматься так же негативно, как и обычный статичный индикатор загрузки — преимущество skeleton-эффекта максимально для коротких задержек (доли секунды — несколько секунд), и для действительно долгих операций (полноценная загрузка большого объёма данных) стоит дополнительно показывать более информативный индикатор прогресса, а не только мерцающие плейсхолдеры.
- Несоответствие формы и размера skeleton-блоков реальному содержимому, которое появится после загрузки — если skeleton показывает один большой блок текста, а реальное содержимое оказывается заметно короче или организовано совершенно иначе, происходит заметный, не плавный «скачок» интерфейса в момент завершения загрузки, что снижает воспринимаемое качество перехода; skeleton должен быть спроектирован максимально близко к реальной ожидаемой структуре контента.
- Слишком интенсивная или слишком быстрая микро-анимация может восприниматься как раздражающая, навязчивая, а не приятная и полезная — масштаб изменений (как в примере, всего 3% увеличения) и скорость анимации (150 мс — достаточно быстро для ощущения отзывчивости, но не настолько резко, чтобы быть дёргающимся) должны быть тщательно откалиброваны; слишком драматичные микро-анимации часто хуже, чем их полное отсутствие.
- Большое количество одновременно анимируемых skeleton-блоков на экране (например, длинный список из множества карточек, каждая со своим независимым
SkeletonLoader) создаёт множество одновременно работающих бесконечных анимаций, что может заметно нагружать рендеринг, особенно на менее мощных устройствах — для длинных списков стоит рассмотреть синхронизацию всех skeleton-эффектов через единую, общую анимацию (привязанную к общему состоянию, а не независимую для каждого элемента), вместо полностью независимых, рассинхронизированных таймеров на каждый отдельный блок.