Pull to refresh

LibGDX + Scene2d (программируем на Kotlin). Часть 1

Reading time 6 min
Views 7.9K
И снова здравствуйте. По результатам прошлой публикации, я пришел к выводу что опять совершаю ошибки. Высокие темп публикации неудобен ни мне, ни вам. И попробую еще подсократить теорию, но приводить больше примеров кода.

Небольшое лирическое отступление. 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

Tags:
Hubs:
+6
Comments 2
Comments Comments 2

Articles