И снова здравствуйте. По результатам прошлой публикации, я пришел к выводу что опять совершаю ошибки. Высокие темп публикации неудобен ни мне, ни вам. И попробую еще подсократить теорию, но приводить больше примеров кода.
Небольшое лирическое отступление. LibGDX в значительной части представляет из себя простую обертку над OpenGL. Просто работа с текстурами. Все что мы делаем — это указываем порядок и способ отрисовки текстур. Базовый инструмент для рисования текстур — Drawable.
Drawable, это такая штука, которая встречается в Scene2d буквально на каждом шагу. Картинки, кнопки, фоны элементов, всякие элементы слайдеров, панелей прокруток и т.д. — все они используют Drawable для отображения себя на экране. С практической точки зрения, нас не сильно заботит как он устроен внутри. Потому что работать мы будем с тремя конкретными реализациями Drawable. Это TextureRegionDrawable, TiledDrawable и NinePatchDrawable. Вот текстура которую мы хотим нарисовать на экране:

Первый вариант — TextureRegionDrawable. Он просто растягивает текстуру под заданные координаты. Второй вариант — TiledDrawable. Текстура повторяется множество раз, при этом масштаб не меняется. И третий вариант, это 9-Box или 9-Patch. Чем же он хорош и когда его следует использовать?
9-Patch сохраняет внешние элементы в том виде, в котором они определены вне зависимости от размера центрального объекта. Широко используется для кнопок, диалоговых окон, панелей и т.п. Представьте если бы у одной панели внешняя рамка была бы в 2 раза толще или тоньше чем соседняя.
Как я упоминал вчера, сцена это иерархический набор элементов (потомки класса Actor). Все акторы делятся на две группы — Widget и WidgetGroup. Widget это листья дерева, которые не могут содержать дочерних элементов. WidgetGroup — это узлы. То есть все их различие заключается в том, что WidgetGroup умеют «раскладывать» дочерние элементы в определенном порядке. Все обучение Scene2d сводится к умению комбинировать эти объекты. К примеру кнопка в LibGDX это WidgetGroup, наследник Table. Она может содержать и текст, и изображение. Ну и любую другую верстку как любая другая таблица.
Код использует Атлас Текстур/Шкурки для улучшения читаемости. Как настраивать лучше посмотреть в репозитории. Описывать принципы их работы это на целую отдельную статью.
Что мы видим в коде:
Все, что внутри .apply применяется к объекту, на котором apply был вызван. Метод setFillParent(true) правильно использовать только один раз при добавлении корневого элемента в сцену. Так как он используется очень редко, я про него постоянно забываю и не сразу понимаю почему сцена у меня пустая.
Самая распространенная ошибка: забыть добавить setFillParent(true) в корневой элемент
Тот же пример на java
Самое важное отличие — отсутствие форматирования кода согласно логике вложения. Вся портянка элемента выровнена по левому краю и очень легко запутаться, т.к. большинство методов общие на уровне Widget/WidgetGroup.
В Kotlin'e я применял к row() функцию сокрытия видимости .let, которую я еще ни разу не видел чтобы использовали как функцию сокрытия видимости. Самый распространенный вариант ее использования — null check. Внутри let поле будет доступно как it и гарантированно не-нулл.

add — добавляет ячейку в ряд. Возвращает Cell к которому можно применять модификаторы
row — добавляет row. Возвращает default Cell для ряда. Модификаторы, примененные к default Cell будут автоматически применены ко все ячейкам этого ряда.
expand/expandX/expandY — «пружинки». Меняют размер ячеек (но не содержимого). По умолчанию содержимое ячеек расположено в центре
width/height — задает размер ячейки фиксировано или в процентном соотношении.
fill/fillX/fillY — заставляет содержимое ячейки принять размер ячейки
left/right/top/bottom — если содержимое ячейки меньше размеров, указывает способ выравнивания
Делаем верстку первого экрана:

Я сделал набор иконок которые поясняют примененные модификаторы к ячейкам
Пружинки — expand/expandX/expandY (раздвигают ячейку)
Стрелки — fill/fillX/fillY (содержимое ячейки заполняет ячейку)
Швеллер — фиксированный размер ширины/высоты (фиксирует размеры ячейки по ширине/высоте)
Контейнер может иметь только один Widget. Имеет Drawable background. Поэтому мы будем использовать его чтобы нарисовать на экране header и footer (панель ресурсов/командная панель).
Попробуем подойти аналогичным образом к верстке загрузочного экрана:
Пример кода:
Вроде даже работает. Но работает не так как нам хотелось бы. Проблема заключается в том, что устройства с разным соотношением сторон будут плющить или растягивать текстуру. Как же будет правильно?
Допустим вот такой вариант. Мы используем изображение, и говорим что масштабировать его надо сохраняя пропорции до тех пор пока меньшая сторона упрется в край. При этом большая сторона будет обрезана. Другой вариант — Scaling.fit. Масштабирование будет идти пока большая часть не упрется в край, меньшая часть будет иметь незаполненные участки (letterbox).
Но что делать если мы, к примеру, хотим разместить Progress Bar где-то в 20% пространства снизу и чтобы он занимал 60% экрана. Никто не запрещает добавить в сцену несколько actor'ов верхнего уровня. Будет так:
На этом на сегодня все. Пожалуйста оставляйте комментарии что бы вы хотели увидеть более подробно и/или предложения как улучшить подачу материала.
P.S. На итоговом экране есть командная панель с 4 кнопками. Используя материал из данной статьи вы можете самостоятельно ее реализовать. Ответ в репозитории. Следующая статья через неделю.
Небольшое лирическое отступление. LibGDX в значительной части представляет из себя простую обертку над OpenGL. Просто работа с текстурами. Все что мы делаем — это указываем порядок и способ отрисовки текстур. Базовый инструмент для рисования текстур — Drawable.
Drawable
Drawable, это такая штука, которая встречается в Scene2d буквально на каждом шагу. Картинки, кнопки, фоны элементов, всякие элементы слайдеров, панелей прокруток и т.д. — все они используют Drawable для отображения себя на экране. С практической точки зрения, нас не сильно заботит как он устроен внутри. Потому что работать мы будем с тремя конкретными реализациями Drawable. Это TextureRegionDrawable, TiledDrawable и NinePatchDrawable. Вот текстура которую мы хотим нарисовать на экране:

А вот три варианта Drawable на основе этой текстуры
Первый вариант — TextureRegionDrawable. Он просто растягивает текстуру под заданные координаты. Второй вариант — TiledDrawable. Текстура повторяется множество раз, при этом масштаб не меняется. И третий вариант, это 9-Box или 9-Patch. Чем же он хорош и когда его следует использовать?
9-Patch

9-Patch сохраняет внешние элементы в том виде, в котором они определены вне зависимости от размера центрального объекта. Широко используется для кнопок, диалоговых окон, панелей и т.п. Представьте если бы у одной панели внешняя рамка была бы в 2 раза толще или тоньше чем соседняя.
Table Layout
Как я упоминал вчера, сцена это иерархический набор элементов (потомки класса Actor). Все акторы делятся на две группы — Widget и WidgetGroup. Widget это листья дерева, которые не могут содержать дочерних элементов. WidgetGroup — это узлы. То есть все их различие заключается в том, что WidgetGroup умеют «раскладывать» дочерние элементы в определенном порядке. Все обучение Scene2d сводится к умению комбинировать эти объекты. К примеру кнопка в LibGDX это WidgetGroup, наследник Table. Она может содержать и текст, и изображение. Ну и любую другую верстку как любая другая таблица.
Изображение
Kotlin
class TableStage : Stage() { init { val stageLayout = Table() addActor(stageLayout.apply { debugAll() setFillParent(true) pad(AppConstants.PADDING) defaults().expand().space(AppConstants.PADDING) row().let { add(Image(uiSkin.getDrawable("sample"))) add(Image(uiSkin.getDrawable("sample"))).top().right() add(Image(uiSkin.getDrawable("sample"))).fill() } row().let { add(Image(uiSkin.getTiledDrawable("sample"))).fillY().left().colspan(2) add(Image(uiSkin.getTiledDrawable("sample"))).width(64f).height(64f).right().bottom() } row().let { add(Image(uiSkin.getDrawable("sample"))) add(Image(uiSkin.getTiledDrawable("sample"))).fill().pad(AppConstants.PADDING) add(Image(uiSkin.getDrawable("sample"))).width(64f).height(64f) } }) } }
Код использует Атлас Текстур/Шкурки для улучшения читаемости. Как настраивать лучше посмотреть в репозитории. Описывать принципы их работы это на целую отдельную статью.
Что мы видим в коде:
... val stageLayout = Table() addActor(stageLayout.apply { // добавление таблицы в сцену debugAll() // Включаем дебаг для всех элементов таблицы setFillParent(true) // Указываем что таблица принимает размеры родителя pad(AppConstants.PADDING) defaults().expand().space(AppConstants.PADDING) row().let { add(Image(uiSkin.getDrawable("sample"))) add(Image(uiSkin.getDrawable("sample"))).top().right() add(Image(uiSkin.getDrawable("sample"))).fill() }
Все, что внутри .apply применяется к объекту, на котором apply был вызван. Метод setFillParent(true) правильно использовать только один раз при добавлении корневого элемента в сцену. Так как он используется очень редко, я про него постоянно забываю и не сразу понимаю почему сцена у меня пустая.
Самая распространенная ошибка: забыть добавить setFillParent(true) в корневой элемент
Тот же пример на java
... Table stageLayout = new Table(); stageLayout.debugAll(); stageLayout.setFillParent(true); stageLayout.pad(AppConstants.PADDING); stageLayout.defaults().expand().space(AppConstants.PADDING); stageLayout.row(); stageLayout.add(Image(uiSkin.getDrawable("sample"))); stageLayout.add(Image(uiSkin.getDrawable("sample"))).top().right(); stageLayout.add(Image(uiSkin.getDrawable("sample"))).fill(); addActor(stageLayout);
Самое важное отличие — отсутствие форматирования кода согласно логике вложения. Вся портянка элемента выровнена по левому краю и очень легко запутаться, т.к. большинство методов общие на уровне Widget/WidgetGroup.
В Kotlin'e я применял к row() функцию сокрытия видимости .let, которую я еще ни разу не видел чтобы использовали как функцию сокрытия видимости. Самый распространенный вариант ее использования — null check. Внутри let поле будет доступно как it и гарантированно не-нулл.
var name: String? = ... name?.let { if (it == "Alex") ... }
Методы разметки таблицы

add — добавляет ячейку в ряд. Возвращает Cell к которому можно применять модификаторы
row — добавляет row. Возвращает default Cell для ряда. Модификаторы, примененные к default Cell будут автоматически применены ко все ячейкам этого ряда.
expand/expandX/expandY — «пружинки». Меняют размер ячеек (но не содержимого). По умолчанию содержимое ячеек расположено в центре
width/height — задает размер ячейки фиксировано или в процентном соотношении.
.width(40f) .width(Value.percentWidth(.4f, stageLayout)
fill/fillX/fillY — заставляет содержимое ячейки принять размер ячейки
left/right/top/bottom — если содержимое ячейки меньше размеров, указывает способ выравнивания
Делаем верстку первого экрана:
Я сделал набор иконок которые поясняют примененные модификаторы к ячейкам
Пружинки — expand/expandX/expandY (раздвигают ячейку)
Стрелки — fill/fillX/fillY (содержимое ячейки заполняет ячейку)
Швеллер — фиксированный размер ширины/высоты (фиксирует размеры ячейки по ширине/высоте)
Container<> Layout
Контейнер может иметь только один Widget. Имеет Drawable background. Поэтому мы будем использовать его чтобы нарисовать на экране header и footer (панель ресурсов/командная панель).
val stageLayout = Table() addActor(stageLayout.apply { ... row().let { val headerContainer = Container<WidgetGroup>() add(headerContainer.apply { background = TextureRegionDrawable(TextureRegion(Texture("images/status-bar-background.png"))) // здесь в следующей части мы добавим панель ресурсов }).height(100f).expandX() }
Полный код главной сцены
val stageLayout = Table() addActor(stageLayout.apply { setFillParent(true) defaults().fill() row().let { val headerContainer = Container<WidgetGroup>() add(headerContainer.apply { background = TextureRegionDrawable(TextureRegion(Texture("images/status-bar-background.png"))) }).height(100f).expandX() } row().let { add(Image(Texture("backgrounds/main-screen-background.png")).apply { setScaling(Scaling.fill) }).expand() } row().let { val footerContainer = Container<WidgetGroup>() add(footerContainer.apply { background = TextureRegionDrawable(TextureRegion(Texture("images/status-bar-background.png"))) fill() actor = CommandPanel() }).height(160f).expandX() } })
Верстка Loading Screen
Попробуем подойти аналогичным образом к верстке загрузочного экрана:
Прототип верстки
Пример кода:
val stageLayout = Table() addActor(stageLayout.apply { setFillParent(true) background = TextureRegionDrawable(TextureRegion(Texture("backgrounds/loading-logo.png"))) })
Вроде даже работает. Но работает не так как нам хотелось бы. Проблема заключается в том, что устройства с разным соотношением сторон будут плющить или растягивать текстуру. Как же будет правильно?
val stageLayout = Table() val backgroundImage = Image(Texture("backgrounds/loading-logo.png")) addActor(backgroundImage.apply { setFillParent(true) setScaling(Scaling.fill) })
Допустим вот такой вариант. Мы используем изображение, и говорим что масштабировать его надо сохраняя пропорции до тех пор пока меньшая сторона упрется в край. При этом большая сторона будет обрезана. Другой вариант — Scaling.fit. Масштабирование будет идти пока большая часть не упрется в край, меньшая часть будет иметь незаполненные участки (letterbox).
Но что делать если мы, к примеру, хотим разместить Progress Bar где-то в 20% пространства снизу и чтобы он занимал 60% экрана. Никто не запрещает добавить в сцену несколько actor'ов верхнего уровня. Будет так:
Экран
Код
init { val backgroundImage = Image(Texture("backgrounds/loading-logo.png")) addActor(backgroundImage.apply { setFillParent(true) setScaling(Scaling.fill) }) val stageLayout = Table() addActor(stageLayout.apply { setFillParent(true) row().let { add().width(Value.percentWidth(.6f, stageLayout)).height(Value.percentHeight(.8f, stageLayout)) } row().let { add(progressBar).height(40f).fill() // про progressBar будет в следующих частях } }) }
На этом на сегодня все. Пожалуйста оставляйте комментарии что бы вы хотели увидеть более подробно и/или предложения как улучшить подачу материала.
P.S. На итоговом экране есть командная панель с 4 кнопками. Используя материал из данной статьи вы можете самостоятельно ее реализовать. Ответ в репозитории. Следующая статья через неделю.
Результат части 1
