Расширение компонентов
Помимо стандартных элементов и компонентов фона, могут быть созданы новые, основываясь на специальном компоненте. Рассмотрим компоненты и события, которые могут быть обработаны с помощью них.
Фоновая компоновка
Не будем останавливаться на теории и сразу перейдем к практической части. Допустим, у нас есть изображение небольшого размера. Есть задача растянуть его на весь экран, не растягивая саму текстуру. Как это можно сделать?
Наверняка, тем кто знаком с холстом (канвой) и рисованием на нем, захочется нарисовать текстуру несколько раз по нескольким направлениям:
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
), по умолчанию с той же текстурой что и обычный слот.