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

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

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

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

Для реализации анимаций можно использовать встроенные средства 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 для создания анимаций, подобным изучениям и квестам.