И снова здравствуйте. По результатам прошлой публикации, я пришел к выводу что опять совершаю ошибки. Высокие темп публикации неудобен ни мне, ни вам. И попробую еще подсократить теорию, но приводить больше примеров кода.
Небольшое лирическое отступление. 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