LibGDX + Scene2d (programmed in Kotlin). Part 1
Hello again. According to the results of the last publication, I came to the conclusion that again I make mistakes. The high pace of publication is inconvenient to me and you. And I will try to shorten the theory, but give more code examples.
A slight lyrical digression. To a large extent, LibGDX is a simple wrapper over OpenGL. Just work with textures. All we do is specify the order and method of rendering the textures. The basic tool for drawing textures is Drawable.
Drawable, this is such a thing that is found in Scene2d literally at every step. Pictures, buttons, backgrounds of elements, all sorts of sliders, scrollbars, etc. - They all use Drawable to display themselves on the screen. From a practical point of view, we are not very worried about how it is arranged inside. Because we will work with three specific implementations of Drawable. These are TextureRegionDrawable, TiledDrawable and NinePatchDrawable. Here is the texture we want to draw on the screen:
The first option is TextureRegionDrawable. It just stretches the texture to the given coordinates. The second option is TiledDrawable. The texture is repeated many times, while the scale does not change. And the third option is a 9-Box or 9-Patch. What is it good for and when should it be used?
9-Patch saves external elements in the form in which they are defined, regardless of the size of the central object. Widely used for buttons, dialogs, panels, etc. Imagine if for one panel the outer frame was 2 times thicker or thinner than the neighboring one.
As I mentioned yesterday, a scene is a hierarchical set of elements (descendants of the Actor class). All actors are divided into two groups - Widget and WidgetGroup. A widget is a tree leaf that cannot contain any child elements. WidgetGroup are nodes. That is, all of their difference lies in the fact that WidgetGroup can "lay out" children in a certain order. All Scene2d training comes down to the ability to combine these objects. For example, a button in LibGDX is a WidgetGroup, a descendant of Table. It can contain both text and image. Well, any other layout like any other table.
The code uses an Atlas of Textures / Skins to improve readability. How to configure it is better to look in the repository. Describe the principles of their work is a whole separate article.
What we see in the code:
Everything inside .apply applies to the object on which apply was called. The setFillParent (true) method is correctly used only once when adding the root element to the scene. Since it is used very rarely, I constantly forget about it and do not immediately understand why my scene is empty.
The most common mistake: forget to add setFillParent (true) to the root element.
The same example in java
The most important difference is the lack of code formatting according to the embedding logic. The entire footcloth of the element is aligned to the left and it is very easy to get confused, because most methods are common at the Widget / WidgetGroup level.
In Kotlin, I applied the .let visibility hiding function to row (), which I had never seen before used as a hiding visibility function. The most common use case for it is null check. Inside let, the field will be accessible as it and guaranteed to be non-zero.
add - adds a cell to a row. Returns Cell to which
row modifiers can be applied - adds row. Returns default Cell for a row. Modifiers applied to default Cell will be automatically applied to all cells in this row.
expand / expandX / expandY - "springs". Change the size of the cells (but not the contents). By default, the contents of the cells are located in the center of
width / height - sets the size of the cell fixed or in percentage.
fill / fillX / fillY - forces the contents of the cell to accept the size of the cell
left / right / top / bottom - if the contents of the cell is smaller than the sizes, indicates the alignment method
We do the layout of the first screen:
I made a set of icons that explain the applied modifiers to the cells
Springs - expand / expandX / expandY (expand the cell)
Arrows - fill / fillX / fillY (the contents of the cell fill the cell)
Channel - a fixed width / height size (fixes the cell dimensions in width / height)
A container can have only one Widget. Has a drawable background. Therefore, we will use it to draw the header and footer (resource panel / command panel) on the screen.
Let's try to approach the layout of the boot screen in the same way:
Code example:
It seems to even work. But it does not work as we would like. The problem is that devices with different aspect ratios will flatten or stretch the texture. How will it be right?
Let's say this option. We use the image, and we say that it is necessary to scale it while maintaining the proportions until the smaller side rests against the edge. In this case, the big side will be cut off. Another option is Scaling.fit. Scaling will go until most of it rests against the edge, a smaller part will have blank sections (letterbox).
But what if we, for example, want to place the Progress Bar somewhere in 20% of the space below and that it occupies 60% of the screen. No one forbids adding several top-level actors to the scene. It will be like this:
That's all for today. Please leave comments so that you would like to see in more detail and / or suggestions how to improve the presentation of the material.
PS There is a command panel with 4 buttons on the final screen. Using the material from this article, you can independently implement it. The answer is in the repository . Next article in a week.
A slight lyrical digression. To a large extent, LibGDX is a simple wrapper over OpenGL. Just work with textures. All we do is specify the order and method of rendering the textures. The basic tool for drawing textures is Drawable.
Drawable
Drawable, this is such a thing that is found in Scene2d literally at every step. Pictures, buttons, backgrounds of elements, all sorts of sliders, scrollbars, etc. - They all use Drawable to display themselves on the screen. From a practical point of view, we are not very worried about how it is arranged inside. Because we will work with three specific implementations of Drawable. These are TextureRegionDrawable, TiledDrawable and NinePatchDrawable. Here is the texture we want to draw on the screen:
And here are three options for Drawable based on this texture
The first option is TextureRegionDrawable. It just stretches the texture to the given coordinates. The second option is TiledDrawable. The texture is repeated many times, while the scale does not change. And the third option is a 9-Box or 9-Patch. What is it good for and when should it be used?
9-patch
9-Patch saves external elements in the form in which they are defined, regardless of the size of the central object. Widely used for buttons, dialogs, panels, etc. Imagine if for one panel the outer frame was 2 times thicker or thinner than the neighboring one.
Table layout
As I mentioned yesterday, a scene is a hierarchical set of elements (descendants of the Actor class). All actors are divided into two groups - Widget and WidgetGroup. A widget is a tree leaf that cannot contain any child elements. WidgetGroup are nodes. That is, all of their difference lies in the fact that WidgetGroup can "lay out" children in a certain order. All Scene2d training comes down to the ability to combine these objects. For example, a button in LibGDX is a WidgetGroup, a descendant of Table. It can contain both text and image. Well, any other layout like any other table.
Picture
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)
}
})
}
}
The code uses an Atlas of Textures / Skins to improve readability. How to configure it is better to look in the repository. Describe the principles of their work is a whole separate article.
What we see in the code:
...
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()
}
Everything inside .apply applies to the object on which apply was called. The setFillParent (true) method is correctly used only once when adding the root element to the scene. Since it is used very rarely, I constantly forget about it and do not immediately understand why my scene is empty.
The most common mistake: forget to add setFillParent (true) to the root element.
The same example in 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);
The most important difference is the lack of code formatting according to the embedding logic. The entire footcloth of the element is aligned to the left and it is very easy to get confused, because most methods are common at the Widget / WidgetGroup level.
In Kotlin, I applied the .let visibility hiding function to row (), which I had never seen before used as a hiding visibility function. The most common use case for it is null check. Inside let, the field will be accessible as it and guaranteed to be non-zero.
var name: String? = ...
name?.let {
if (it == "Alex") ...
}
Table layout methods
add - adds a cell to a row. Returns Cell to which
row modifiers can be applied - adds row. Returns default Cell for a row. Modifiers applied to default Cell will be automatically applied to all cells in this row.
expand / expandX / expandY - "springs". Change the size of the cells (but not the contents). By default, the contents of the cells are located in the center of
width / height - sets the size of the cell fixed or in percentage.
.width(40f)
.width(Value.percentWidth(.4f, stageLayout)
fill / fillX / fillY - forces the contents of the cell to accept the size of the cell
left / right / top / bottom - if the contents of the cell is smaller than the sizes, indicates the alignment method
We do the layout of the first screen:
I made a set of icons that explain the applied modifiers to the cells
Springs - expand / expandX / expandY (expand the cell)
Arrows - fill / fillX / fillY (the contents of the cell fill the cell)
Channel - a fixed width / height size (fixes the cell dimensions in width / height)
Container <> Layout
A container can have only one Widget. Has a drawable background. Therefore, we will use it to draw the header and footer (resource panel / command panel) on the screen.
val stageLayout = Table()
addActor(stageLayout.apply {
...
row().let {
val headerContainer = Container()
add(headerContainer.apply {
background = TextureRegionDrawable(TextureRegion(Texture("images/status-bar-background.png")))
// здесь в следующей части мы добавим панель ресурсов
}).height(100f).expandX()
}
Full main scene code
val stageLayout = Table()
addActor(stageLayout.apply {
setFillParent(true)
defaults().fill()
row().let {
val headerContainer = Container()
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()
add(footerContainer.apply {
background = TextureRegionDrawable(TextureRegion(Texture("images/status-bar-background.png")))
fill()
actor = CommandPanel()
}).height(160f).expandX()
}
})
Layout Loading Screen
Let's try to approach the layout of the boot screen in the same way:
Layout prototype
Code example:
val stageLayout = Table()
addActor(stageLayout.apply {
setFillParent(true)
background = TextureRegionDrawable(TextureRegion(Texture("backgrounds/loading-logo.png")))
})
It seems to even work. But it does not work as we would like. The problem is that devices with different aspect ratios will flatten or stretch the texture. How will it be right?
val stageLayout = Table()
val backgroundImage = Image(Texture("backgrounds/loading-logo.png"))
addActor(backgroundImage.apply {
setFillParent(true)
setScaling(Scaling.fill)
})
Let's say this option. We use the image, and we say that it is necessary to scale it while maintaining the proportions until the smaller side rests against the edge. In this case, the big side will be cut off. Another option is Scaling.fit. Scaling will go until most of it rests against the edge, a smaller part will have blank sections (letterbox).
But what if we, for example, want to place the Progress Bar somewhere in 20% of the space below and that it occupies 60% of the screen. No one forbids adding several top-level actors to the scene. It will be like this:
Screen
The code
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 будет в следующих частях
}
})
}
That's all for today. Please leave comments so that you would like to see in more detail and / or suggestions how to improve the presentation of the material.
PS There is a command panel with 4 buttons on the final screen. Using the material from this article, you can independently implement it. The answer is in the repository . Next article in a week.
Result of Part 1