Перейти к основному содержанию

Анимация элементов

Анимация позволяет сделать игровые интерфейсы более живыми и динамичными: будь то плавное появление и исчезновение окон, перемещение отдельных элементов или изменение их свойств с течением времени. В этой статье мы рассмотрим базовые подходы к созданию анимаций, работу с элементами вне основного потока и некоторые подводные камни.

Инструменты для анимации

Для реализации анимаций можно использовать встроенные средства Android (например, android.animation.ValueAnimator), события обновления (Updatable) или создание отдельных фоновых потоков (Threading). В данном руководстве мы остановимся на использовании потоков, так как этот способ предоставляет наибольший контроль над процессом обновления элементов интерфейса и часто применяется на практике (дополнительно ознакомьтесь со статьей о потоках, если вы еще этого не сделали).

Обновление элементов

Для достижения плавной анимации необходимо быстро и многократно изменять параметры элементов (позицию, размер, прозрачность), избегая полной перерисовки всего окна.

Прямое обращение к элементам

Исторически параметры элементов изменялись через прямое обращение к объекту описания интерфейса с последующим вызовом функции обновления, например:

окно.content.elements.название_элемента.x = новое_значение;

В потоке для анимаций такой способ работает недостаточно быстро и может приводить к заметному мерцанию или просадке кадров.

Вместо этого рекомендуется использовать методы интерфейса UI.Element, позволяющие манипулировать отрисованным элементом напрямую нативными средствами.

Методы интерфейса UI.Element

Для начала необходимо получить объект самого элемента из окна:

const elements = окно.getElements();
const element = elements.get("идентификатор_элемента");

После этого мы можем применять специальные методы для изменения его состояния, которые не требуют перерисовки:

// Изменение позиции элемента (работает быстро, идеально для анимации движения)
element.setPosition(x, y);

// Установка значения для конкретного свойства элемента
element.setBinding("название_ключа", значение);

// Получение текущего значения свойства
const value = element.getBinding("название_ключа");

Для работы с самим окном, например, для изменения его прозрачности, используется объект компоновки (layout):

// Установка прозрачности окна (число от 0.0 до 1.0)
окно.layout.setAlpha(0.5);

// Получение текущей прозрачности
const alpha = окно.layout.getAlpha();
Исключения в setBinding

Не все значения элементов можно изменить через setBinding. Если какое-то свойство не реагирует на изменения таким способом, используйте классический вариант с изменением ключа в объекте окна, но старайтесь не делать этого каждый кадр анимации.

Анимация траты средств

В качестве практики реализуем анимацию списания денег: текст с суммой и иконка будут появляться, плавно опускаться вниз и постепенно исчезать.

Подготовка интерфейса

Определим объект, который будет хранить состояние анимации, настройки и сам интерфейс:

const animator = {
// Константы для позиционирования
MAX_HEIGHT: 270, // Путь, который проделают элементы по оси Y
TEXT_X: 812, TEXT_Y: 210,
ICON_X: 858, ICON_Y: 192,

// Переменные состояния
isRunning: false,
queue: []
};

animator.window = new UI.Window({
drawing: [{
type: "background",
color: android.graphics.Color.TRANSPARENT
}],
elements: {
balanceIcon: {
type: "image",
x: animator.ICON_X, y: animator.ICON_Y,
width: 40, height: 40,
bitmap: "default_icon"
},
balanceText: {
type: "text",
x: animator.TEXT_X, y: animator.TEXT_Y,
text: "",
font: { size: 15, color: android.graphics.Color.LTGRAY }
}
}
});

// Настраиваем свойства окна
animator.window.setDynamic(true);
animator.window.setTouchable(false); // Пропускаем клики сквозь окно
animator.window.setAsGameOverlay(true); // Отображаем как игровой оверлей

Вспомогательные методы

Добавим в объект animator методы для инкапсуляции работы с прозрачностью и позициями. Обратите внимание на проверки того, открыто ли окно — это защитит от ошибок, если анимация запустится в неожиданный момент или игрок внезапно выйдет в меню.

animator.setAlpha = function(alpha) {
if (this.window.isOpened()) {
this.window.layout.setAlpha(alpha);
}
};

animator.getAlpha = function() {
if (this.window.isOpened()) {
return this.window.layout.getAlpha();
}
return 0;
};

animator.setHeightOffset = function(offset) {
const elements = this.window.getElements();
const balanceText = elements.get("balanceText");
const balanceIcon = elements.get("balanceIcon");

if (balanceText && balanceIcon) {
balanceText.setPosition(this.TEXT_X, this.TEXT_Y + offset);
balanceIcon.setPosition(this.ICON_X, this.ICON_Y + offset);
}
};

animator.reset = function() {
this.setAlpha(1);
this.setHeightOffset(0);
};

Логика потока анимации

При написании потока для анимации всегда необходима задержка (Thread.sleep()). Для приемлемой плавности (около 60 кадров в секунду) достаточно задержки в 16 миллисекунд. Меньшие значения (например, 2-3 мс) могут перегрузить процессор устройства и привести к нестабильной работе игры.

animator.update = function() {
let offset = 0;
while (true) {
java.lang.Thread.sleep(16);

const alpha = this.getAlpha();

// Если анимация завершена или окно было закрыто извне
if ((offset >= this.MAX_HEIGHT && alpha <= 0) || !this.window.isOpened()) {
this.isRunning = false;

// Проверяем очередь на наличие новых анимаций
if (this.queue.length > 0) {
this.play(this.queue.shift());
return; // Завершаем текущий поток, play() запустит новый
}

this.window.close();
return;
}

// Начинаем плавно уменьшать прозрачность, когда прошли половину пути
if (offset >= (this.MAX_HEIGHT / 2) && alpha > 0) {
this.setAlpha(Math.max(0, alpha - 0.05));
}

// Опускаем элементы вниз (на 5 юнитов за кадр)
if (offset < this.MAX_HEIGHT) {
offset += 5;
this.setHeightOffset(offset);
}
}
};

Запуск анимации

Теперь напишем публичный метод для инициализации анимации. Мы будем сохранять значения в очередь, чтобы при нескольких быстрых вызовах они воспроизводились последовательно, а не накладывались друг на друга или ломали поток.

animator.play = function(amount) {
if (this.isRunning) {
this.queue.push(amount);
return;
}

this.isRunning = true;
this.window.content.elements.balanceText.text = "-" + amount;

if (!this.window.isOpened()) {
this.window.open();
}
// Важно вызывать сброс после открытия окна, иначе элементы могут быть не инициализированы
this.reset();

Threading.initThread("mod_animatorThread", function() {
animator.update();
});
};

Проверим наш код: при любом клике в мире будет появляться наша анимация. Если кликнуть несколько раз подряд, они проиграются по очереди.

Callback.addCallback("ItemUseLocal", function() {
animator.play(10);
});

Рекомендации и частые ошибки

  1. Не анимируйте позицию самого окна. Изменение координат всего окна через window.getLocation().set(...) в потоке нередко приводит к сильному мерцанию фона и медленной отрисовке. Вместо этого двигайте элементы внутри окна.
  2. Контролируйте потоки. Убедитесь, что для одной и той же анимации не запускается несколько потоков одновременно. В нашем примере это решено проверкой переменной isRunning и использованием очереди.
  3. Обеспечивайте безопасное завершение. Игрок может закрыть мир или меню в любой момент, пока поток всё ещё работает. Регулярная проверка window.isOpened() гарантирует, что код не будет пытаться обращаться к несуществующим элементам, что в противном случае привело бы к вылету игры.
  4. Комбинируйте подходы при необходимости. Бывают ситуации, когда позиции элементов внезапно сбрасываются (например, при изменении размеров растягивающихся фреймов). В таком случае можно комбинировать старый и новый подходы или принудительно обновлять макет через window.forceRefresh().

Создание качественных анимаций часто требует экспериментов с таймингами, задержками и шагами изменения значений. Но результат стоит того: интерфейс становится значительно отзывчивее и приятнее для пользователя.

Готовые примеры анимаций

Хотя написание собственных анимаций позволяет лучше понять работу потоков, никто не заставляет делать их с нуля. Вы можете воспользоваться библиотекой Notification для более простого создания анимаций, или ScrutinyAPI для создания анимаций, подобным изучениям и квестам.