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

Расширение компонентов

Помимо стандартных элементов и компонентов фона, могут быть созданы новые, основываясь на специальном компоненте. Рассмотрим компоненты и события, которые могут быть обработаны с помощью них.

Фоновая компоновка

Не будем останавливаться на теории и сразу перейдем к практической части. Допустим, у нас есть изображение небольшого размера. Есть задача растянуть его на весь экран, не растягивая саму текстуру. Как это можно сделать?

Наверняка, тем кто знаком с холстом (канвой) и рисованием на нем, захочется нарисовать текстуру несколько раз по нескольким направлениям:

const bitmap = UI.TextureSource.get("icon_menu_innercore")
const canvas = new android.graphics.Canvas();
const paint = new android.graphics.Paint();
const source = android.graphics.Bitmap.createBitmap(
Packages.com.zhekasmirnov.innercore.utils.UIUtils.screenWidth,
Packages.com.zhekasmirnov.innercore.utils.UIUtils.screenHeight,
android.graphics.Bitmap.Config.ARGB_8888
);
canvas.setBitmap(source);
const rx = source.getWidth() / bitmap.getWidth();
const ry = source.getHeight() / bitmap.getHeight();
for (let x = 0; x < rx; x++) {
for (let y = 0; y < ry; y++) {
canvas.drawBitmap(bitmap, bitmap.getWidth() * x, bitmap.getHeight() * y, paint);
}
}
UI.TextureSource.put("innercore_background", source);

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

Здесь на помощь приходит расширение компонентов. Создадим компонент с собственным свойством bitmap, определяющим текстуру, которая будет повторяться за счет шейдера, значительно ускоряющего обработку:

{
type: "custom",
bitmap: "icon_menu_innercore",
onDraw: function(canvas, scale) {
const bitmap = UI.TextureSource.get(this.bitmap);
const scaled = android.graphics.Bitmap.createScaledBitmap(
bitmap,
bitmap.getWidth() * scale,
bitmap.getHeight() * scale,
false
);
const paint = new android.graphics.Paint();
paint.setShader(
new android.graphics.BitmapShader(
scaled,
android.graphics.Shader.TileMode.REPEAT,
android.graphics.Shader.TileMode.REPEAT
)
);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
scaled.recycle();
}
}

Не думаю, что стоит обсуждать то, какое влияние такой подход окажет на производительность. Прямое рисование на этом листе интерфейса — наилучший вариант во всех смыслах. Помимо канваса, параметр scale здесь определяет масштаб юнита относительно пикселя.

Это самый обычный объект описания компонента. Для фоновой компоновки предусмотрен только метод onDraw(canvas, scale), рассмотрите жизненный цикл для получения подробностей. В остальном, базовое взаимодействие с холстом описано в обработке ресурсов.

Элементы

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

{
type: "custom",
// создание компонента на основе объекта описания элемента,
// здесь необходимо установить необходимые кисти и свойства
onSetup: function(component) {
component.setSize(1000, UI.getScreenHeight());
},
// место где происходит магия, холст в вашем распоряжении
onDraw: function(component, canvas, scale) {
...
},
// вызывается при прикреплении элемента с помощью контейнера,
// обычно во время открытия окна или при добавлении элемента
// (когда окно уже было открыто)
onContainerInit: function(component, container, elementName) {
...
},
// биндинг может быть изменен напрямую из компонента, но
// обычно принимает новое значение изменением в контейнере;
// здесь можно пересчитать величины, изменить размеры и т.п.
onBindingUpdated: function(component, key, value) {
...
},
// вызывается всякий раз, как только взаимодействие с
// компонентом прекращено, также есть в onTouchEvent
onTouchReleased: function(component) {
...
},
// каждое закрытие окна сопровождается сбросом состояния
// компонентов, обычно здесь забывается выделение
onReset: function(component) {
...
},
// конец жизнедеятельности компонента, здесь необходимо
// сбросить или переработать используемые ресурсы
onRelease: function(component) {
...
}
}

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

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

{
type: "custom",
// верхняя левая граница компонента находится
// в четверти ширины экрана и по центру высоты
x: 250, y: UI.getScreenHeight() / 2,
z: -1, // пусть компонент будет на фоне
onSetup: function(component) {
// достаточно одной кисти для рисования линии поршня
this.paint = new android.graphics.Paint();
this.paint.setStyle(android.graphics.Paint.Style.STROKE);
// белый, непрозрачный цвет
this.paint.setARGB(255, 255, 255, 255);
// половина ширины и четверть высоты, отсчитывается
// от местоположения элемента вправо вниз
component.setSize(500, UI.getScreenHeight() / 4);
}
}

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

{
...
onDraw: function(component, canvas, scale) {
// сбросим местоположение правой части ноги, если его нет
if (this.sx === undefined || this.sy === undefined) {
this.sx = 400 * scale;
this.sy = UI.getScreenHeight() / 8 * scale;
}
// кисть маленького размера, масштаб может измениться
this.paint.setStrokeWidth(6 * scale);
// коэффициент вращения левой части ноги, небольшая анимация
const multiplier = Math.abs(10 * Math.sin((
System.currentTimeMillis() % 1000) / 1000
)) / 2;
// обозначим точки ноги поршня квадратами
const rectStart = new android.graphics.Rect(
(100 * multiplier - 8) * scale,
(100 * multiplier - 8) * scale,
(100 * multiplier + 8) * scale,
(100 * multiplier + 8) * scale
);
const rectEnd = new android.graphics.Rect(
this.sx - 8 * scale, this.sy - 8 * scale,
this.sx + 8 * scale, this.sy + 8 * scale
);
// путь, с помощью которого мы будем рисовать ногу поршня
const path = new android.graphics.Path();
// добавим саму линию до правой части ноги
path.moveTo(100 * multiplier * scale, 100 * multiplier * scale);
path.lineTo(this.sx, this.sy);
// путь готов, можно отрисовать его на холсте
canvas.drawPath(path, this.paint);
// отрисуем точки ноги поршня кистью для остальных
// линий, получим полые квадраты вокруг двух точек
canvas.drawRect(rectStart, this.paint);
canvas.drawRect(rectEnd, this.paint);
// пусть анимация не будет останавливаться
component.invalidate();
}
}

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

{
...
onTouchEvent: function(component, event) {
// передвигая мышь или палец по компоненту, переместим
// правую точку ноги поршня в новое место, и запросим
// ближайшее обновление холста (это сделает поток окна)
if (event.type.name() == "MOVE") {
this.sx = event.localX;
this.sy = event.localY;
component.invalidate();
if (component.window) {
component.window.invalidateElements(false);
}
}
},
onReset: function(component) {
delete this.sx;
delete this.sy;
}
}
На что способен элемент

Компоненты элементов имеют множество свойств, вроде прикрепленного окна, контейнера, биндинги и еще несколько полезных методов. Рассмотрите прототип UIElement для получения подробностей.

Встроенные реализации

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

Кнопка закрытия

Как и следует из названия, закрывает интерфейс. Совершенно ничем не отличается от обычной кнопки, за исключением того что не обрабатывает события из объекта описания, так как уже настроена на свое действие.

{
type: "closeButton",
...
}

Слот инвентаря

В отличии от обычного слота, также как и кнопка, не обрабатывает события, но добавляет и изменяет некоторые стандартные свойства. Требует открытия интерфейса через контейнер.

{
type: "invSlot",
index: 0,
...
}

Где index представляет из себя номер слота инвентаря, который будет использован как источник предмета. Значение от 0 до 35, где 0-8 — нижние слоты, видимые на экране; отсчет остальных же идет с левого верхнего слота.

Помимо всего прочего, фоновым изображением становится style:inv_slot (свойство bitmap), по умолчанию с той же текстурой что и обычный слот.

Счетчик кадров

Представляет из себя обычный текст, обновляемый с некоторым промежутком времени. Изменение объекта шрифта не принесет никакого эффекта.

{
type: "fps",
...
}

Добавьте промежуток времени между замерами, измеряющийся в миллисекундах, для более точной настройки отслеживания количества кадров в секунду:

{
...
period: 500
}
Отладочный элемент

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