|
| 1 | +# Анимация элементов |
| 2 | + |
| 3 | +Анимация позволяет сделать игровые интерфейсы более живыми и динамичными: будь то плавное появление и исчезновение окон, перемещение отдельных элементов или изменение их свойств с течением времени. В этой статье мы рассмотрим базовые подходы к созданию анимаций, работу с элементами вне основного потока и некоторые подводные камни. |
| 4 | + |
| 5 | +## Инструменты для анимации |
| 6 | + |
| 7 | +Для реализации анимаций можно использовать встроенные средства Android (например, `android.animation.ValueAnimator`), события обновления (`Updatable`) или создание отдельных фоновых потоков (`Threading`). В данном руководстве мы остановимся на использовании потоков, так как этот способ предоставляет наибольший контроль над процессом обновления элементов интерфейса и часто применяется на практике (дополнительно ознакомьтесь со статьей о потоках, если вы еще этого не сделали). |
| 8 | + |
| 9 | +## Обновление элементов |
| 10 | + |
| 11 | +Для достижения плавной анимации необходимо быстро и многократно изменять параметры элементов (позицию, размер, прозрачность), избегая полной перерисовки всего окна. |
| 12 | + |
| 13 | +### Прямое обращение к элементам |
| 14 | + |
| 15 | +Исторически параметры элементов изменялись через прямое обращение к объекту описания интерфейса с последующим вызовом функции обновления, например: |
| 16 | + |
| 17 | +```js |
| 18 | +окно.content.elements.название_элемента.x = новое_значение; |
| 19 | +``` |
| 20 | + |
| 21 | +В потоке для анимаций такой способ работает недостаточно быстро и может приводить к заметному мерцанию или просадке кадров. |
| 22 | + |
| 23 | +Вместо этого рекомендуется использовать методы интерфейса `UI.Element`, позволяющие манипулировать отрисованным элементом напрямую нативными средствами. |
| 24 | + |
| 25 | +### Методы интерфейса UI.Element |
| 26 | + |
| 27 | +Для начала необходимо получить объект самого элемента из окна: |
| 28 | + |
| 29 | +```js |
| 30 | +const elements = окно.getElements(); |
| 31 | +const element = elements.get("идентификатор_элемента"); |
| 32 | +``` |
| 33 | + |
| 34 | +После этого мы можем применять специальные методы для изменения его состояния, которые не требуют перерисовки: |
| 35 | + |
| 36 | +```js |
| 37 | +// Изменение позиции элемента (работает быстро, идеально для анимации движения) |
| 38 | +element.setPosition(x, y); |
| 39 | + |
| 40 | +// Установка значения для конкретного свойства элемента |
| 41 | +element.setBinding("название_ключа", значение); |
| 42 | + |
| 43 | +// Получение текущего значения свойства |
| 44 | +const value = element.getBinding("название_ключа"); |
| 45 | +``` |
| 46 | + |
| 47 | +Для работы с самим окном, например, для изменения его прозрачности, используется объект компоновки (`layout`): |
| 48 | + |
| 49 | +```js |
| 50 | +// Установка прозрачности окна (число от 0.0 до 1.0) |
| 51 | +окно.layout.setAlpha(0.5); |
| 52 | + |
| 53 | +// Получение текущей прозрачности |
| 54 | +const alpha = окно.layout.getAlpha(); |
| 55 | +``` |
| 56 | + |
| 57 | +:::warning Исключения в `setBinding` |
| 58 | + |
| 59 | +Не все значения элементов можно изменить через `setBinding`. Если какое-то свойство не реагирует на изменения таким способом, используйте классический вариант с изменением ключа в объекте окна, но старайтесь не делать этого каждый кадр анимации. |
| 60 | + |
| 61 | +::: |
| 62 | + |
| 63 | +## Анимация траты средств |
| 64 | + |
| 65 | +В качестве практики реализуем анимацию списания денег: текст с суммой и иконка будут появляться, плавно опускаться вниз и постепенно исчезать. |
| 66 | + |
| 67 | +### Подготовка интерфейса |
| 68 | + |
| 69 | +Определим объект, который будет хранить состояние анимации, настройки и сам интерфейс: |
| 70 | + |
| 71 | +```js |
| 72 | +const animator = { |
| 73 | + // Константы для позиционирования |
| 74 | + MAX_HEIGHT: 270, // Путь, который проделают элементы по оси Y |
| 75 | + TEXT_X: 812, TEXT_Y: 210, |
| 76 | + ICON_X: 858, ICON_Y: 192, |
| 77 | + |
| 78 | + // Переменные состояния |
| 79 | + isRunning: false, |
| 80 | + queue: [] |
| 81 | +}; |
| 82 | + |
| 83 | +animator.window = new UI.Window({ |
| 84 | + drawing: [{ |
| 85 | + type: "background", |
| 86 | + color: android.graphics.Color.TRANSPARENT |
| 87 | + }], |
| 88 | + elements: { |
| 89 | + balanceIcon: { |
| 90 | + type: "image", |
| 91 | + x: animator.ICON_X, y: animator.ICON_Y, |
| 92 | + width: 40, height: 40, |
| 93 | + bitmap: "default_icon" |
| 94 | + }, |
| 95 | + balanceText: { |
| 96 | + type: "text", |
| 97 | + x: animator.TEXT_X, y: animator.TEXT_Y, |
| 98 | + text: "", |
| 99 | + font: { size: 15, color: android.graphics.Color.LTGRAY } |
| 100 | + } |
| 101 | + } |
| 102 | +}); |
| 103 | + |
| 104 | +// Настраиваем свойства окна |
| 105 | +animator.window.setDynamic(true); |
| 106 | +animator.window.setTouchable(false); // Пропускаем клики сквозь окно |
| 107 | +animator.window.setAsGameOverlay(true); // Отображаем как игровой оверлей |
| 108 | +``` |
| 109 | + |
| 110 | +### Вспомогательные методы |
| 111 | + |
| 112 | +Добавим в объект `animator` методы для инкапсуляции работы с прозрачностью и позициями. Обратите внимание на проверки того, открыто ли окно — это защитит от ошибок, если анимация запустится в неожиданный момент или игрок внезапно выйдет в меню. |
| 113 | + |
| 114 | +```js |
| 115 | +animator.setAlpha = function(alpha) { |
| 116 | + if (this.window.isOpened()) { |
| 117 | + this.window.layout.setAlpha(alpha); |
| 118 | + } |
| 119 | +}; |
| 120 | + |
| 121 | +animator.getAlpha = function() { |
| 122 | + if (this.window.isOpened()) { |
| 123 | + return this.window.layout.getAlpha(); |
| 124 | + } |
| 125 | + return 0; |
| 126 | +}; |
| 127 | + |
| 128 | +animator.setHeightOffset = function(offset) { |
| 129 | + const elements = this.window.getElements(); |
| 130 | + const balanceText = elements.get("balanceText"); |
| 131 | + const balanceIcon = elements.get("balanceIcon"); |
| 132 | + |
| 133 | + if (balanceText && balanceIcon) { |
| 134 | + balanceText.setPosition(this.TEXT_X, this.TEXT_Y + offset); |
| 135 | + balanceIcon.setPosition(this.ICON_X, this.ICON_Y + offset); |
| 136 | + } |
| 137 | +}; |
| 138 | + |
| 139 | +animator.reset = function() { |
| 140 | + this.setAlpha(1); |
| 141 | + this.setHeightOffset(0); |
| 142 | +}; |
| 143 | +``` |
| 144 | + |
| 145 | +### Логика потока анимации |
| 146 | + |
| 147 | +При написании потока для анимации всегда необходима задержка (`Thread.sleep()`). Для приемлемой плавности (около 60 кадров в секунду) достаточно задержки в 16 миллисекунд. Меньшие значения (например, 2-3 мс) могут перегрузить процессор устройства и привести к нестабильной работе игры. |
| 148 | + |
| 149 | +```js |
| 150 | +animator.update = function() { |
| 151 | + let offset = 0; |
| 152 | + while (true) { |
| 153 | + java.lang.Thread.sleep(16); |
| 154 | + |
| 155 | + const alpha = this.getAlpha(); |
| 156 | + |
| 157 | + // Если анимация завершена или окно было закрыто извне |
| 158 | + if ((offset >= this.MAX_HEIGHT && alpha <= 0) || !this.window.isOpened()) { |
| 159 | + this.isRunning = false; |
| 160 | + |
| 161 | + // Проверяем очередь на наличие новых анимаций |
| 162 | + if (this.queue.length > 0) { |
| 163 | + this.play(this.queue.shift()); |
| 164 | + return; // Завершаем текущий поток, play() запустит новый |
| 165 | + } |
| 166 | + |
| 167 | + this.window.close(); |
| 168 | + return; |
| 169 | + } |
| 170 | + |
| 171 | + // Начинаем плавно уменьшать прозрачность, когда прошли половину пути |
| 172 | + if (offset >= (this.MAX_HEIGHT / 2) && alpha > 0) { |
| 173 | + this.setAlpha(Math.max(0, alpha - 0.05)); |
| 174 | + } |
| 175 | + |
| 176 | + // Опускаем элементы вниз (на 5 юнитов за кадр) |
| 177 | + if (offset < this.MAX_HEIGHT) { |
| 178 | + offset += 5; |
| 179 | + this.setHeightOffset(offset); |
| 180 | + } |
| 181 | + } |
| 182 | +}; |
| 183 | +``` |
| 184 | + |
| 185 | +### Запуск анимации |
| 186 | + |
| 187 | +Теперь напишем публичный метод для инициализации анимации. Мы будем сохранять значения в очередь, чтобы при нескольких быстрых вызовах они воспроизводились последовательно, а не накладывались друг на друга или ломали поток. |
| 188 | + |
| 189 | +```js |
| 190 | +animator.play = function(amount) { |
| 191 | + if (this.isRunning) { |
| 192 | + this.queue.push(amount); |
| 193 | + return; |
| 194 | + } |
| 195 | + |
| 196 | + this.isRunning = true; |
| 197 | + this.window.content.elements.balanceText.text = "-" + amount; |
| 198 | + |
| 199 | + if (!this.window.isOpened()) { |
| 200 | + this.window.open(); |
| 201 | + } |
| 202 | + // Важно вызывать сброс после открытия окна, иначе элементы могут быть не инициализированы |
| 203 | + this.reset(); |
| 204 | + |
| 205 | + Threading.initThread("mod_animatorThread", function() { |
| 206 | + animator.update(); |
| 207 | + }); |
| 208 | +}; |
| 209 | +``` |
| 210 | + |
| 211 | +Проверим наш код: при любом клике в мире будет появляться наша анимация. Если кликнуть несколько раз подряд, они проиграются по очереди. |
| 212 | + |
| 213 | +```js |
| 214 | +Callback.addCallback("ItemUseLocal", function() { |
| 215 | + animator.play(10); |
| 216 | +}); |
| 217 | +``` |
| 218 | + |
| 219 | +## Рекомендации и частые ошибки |
| 220 | + |
| 221 | +1. **Не анимируйте позицию самого окна**. Изменение координат всего окна через `window.getLocation().set(...)` в потоке нередко приводит к сильному мерцанию фона и медленной отрисовке. Вместо этого двигайте элементы внутри окна. |
| 222 | +2. **Контролируйте потоки**. Убедитесь, что для одной и той же анимации не запускается несколько потоков одновременно. В нашем примере это решено проверкой переменной `isRunning` и использованием очереди. |
| 223 | +3. **Обеспечивайте безопасное завершение**. Игрок может закрыть мир или меню в любой момент, пока поток всё ещё работает. Регулярная проверка `window.isOpened()` гарантирует, что код не будет пытаться обращаться к несуществующим элементам, что в противном случае привело бы к вылету игры. |
| 224 | +4. **Комбинируйте подходы при необходимости**. Бывают ситуации, когда позиции элементов внезапно сбрасываются (например, при изменении размеров растягивающихся фреймов). В таком случае можно комбинировать старый и новый подходы или принудительно обновлять макет через `window.forceRefresh()`. |
| 225 | + |
| 226 | +Создание качественных анимаций часто требует экспериментов с таймингами, задержками и шагами изменения значений. Но результат стоит того: интерфейс становится значительно отзывчивее и приятнее для пользователя. |
| 227 | + |
| 228 | +## Готовые примеры анимаций |
| 229 | + |
| 230 | +Хотя написание собственных анимаций позволяет лучше понять работу потоков, никто не заставляет делать их с нуля. Вы можете воспользоваться [библиотекой Notification](https://github.com/ArtemKot4/libraries/tree/main/release/Notification) для более простого создания анимаций, или [ScrutinyAPI](https://github.com/Reider745/libs/tree/main/ScrutinyAPI) для создания анимаций, подобным изучениям и квестам. |
0 commit comments