Building LibGDX Scene2D Interfaces with NinePatch and UI Widgets
Standard interface elements—such as text labels, interactive buttons, toggle checkboxes, dropdown selectors, images, text inputs, lists, scroll panes, sliders, and split panes—reside within the com.badlogic.gdx.scenes.scene2d.ui package. Because they all inherit from Actor, they integrate seamlessly into a scene graph managed by a Stage. Examining the source code reveals that most of these elements derive from either Widget or Table, both of which serve as excellant base classes for custom UI components.
NinePatch Mechanics
A NinePatch divides a texture into nine distinct regions, allowing the center to scale while preserving the dimensions of corners and edges. This is particularly useful for resizable button backgrounds. Internally, it stores an array of nine TextureRegion objects.
Instantiation typically occurs by specifying the padding margins:
public NinePatch(Texture tex, int leftPad, int rightPad, int topPad, int bottomPad)
public NinePatch(TextureRegion reg, int leftPad, int rightPad, int topPad, int bottomPad)
The slicing mechanism calculates the center dimensions first, then extracts regions from top-to-bottom, left-to-right:
public NinePatch(TextureRegion reg, int leftPad, int rightPad, int topPad, int bottomPad) {
int centerW = reg.getRegionWidth() - leftPad - rightPad;
int centerH = reg.getRegionHeight() - topPad - bottomPad;
this.regions = new TextureRegion[] {
new TextureRegion(reg, 0, 0, leftPad, topPad),
new TextureRegion(reg, leftPad, 0, centerW, topPad),
new TextureRegion(reg, leftPad + centerW, 0, rightPad, topPad),
new TextureRegion(reg, 0, topPad, leftPad, centerH),
new TextureRegion(reg, leftPad, topPad, centerW, centerH),
new TextureRegion(reg, leftPad + centerW, topPad, rightPad, centerH),
new TextureRegion(reg, 0, topPad + centerH, leftPad, bottomPad),
new TextureRegion(reg, leftPad, topPad + centerH, centerW, bottomPad),
new TextureRegion(reg, leftPad + centerW, topPad + centerH, rightPad, bottomPad)
};
}
During rendering, the engine determines the dynamic boundaries for the columns and rows, drawing the scalable center sections to fit the target dimensions while keeping the corners static.
Text Labels
Labels cache their rendering state. To dynamically update the displayed string, utilize setWrappedText rather than standard setter methods.
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
public class UIDemo implements ApplicationListener {
private Stage uiStage;
private Label fpsCounter;
@Override
public void create() {
uiStage = new Stage(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), true);
BitmapFont font = new BitmapFont(Gdx.files.internal("font.fnt"), Gdx.files.internal("font.png"), false);
fpsCounter = new Label("fps", font, "fpsLabel");
fpsCounter.setPosition(10, Gdx.graphics.getHeight() - fpsCounter.getHeight() - 10);
uiStage.addActor(fpsCounter);
Gdx.input.setInputProcessor(uiStage);
}
@Override
public void render() {
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
fpsCounter.setWrappedText("FPS: " + Gdx.graphics.getFramesPerSecond(), BitmapFont.HAlignment.CENTER);
uiStage.act(Gdx.graphics.getDeltaTime());
uiStage.draw();
}
@Override public void dispose() { uiStage.dispose(); }
@Override public void pause() {}
@Override public void resume() {}
@Override public void resize(int width, int height) {}
}
Interactive Buttons
Buton configuration relies on a ButtonStyle, which encapsulates NinePatch drawings for up, down, and hovered states, offset coordinates for pressed effects, alongside a BitmapFont and Color.
Texture btnTex = new Texture(Gdx.files.internal("btn_bg.png"));
NinePatch btnPatch = new NinePatch(btnTex, 8, 8, 10, 10);
BitmapFont uiFont = new BitmapFont(Gdx.files.internal("font.fnt"), Gdx.files.internal("font.png"), false);
Button.ButtonStyle btnStyle = new Button.ButtonStyle(btnPatch, btnPatch, btnPatch, 0f, 0f, 0f, 0f, uiFont, new Color(1, 1, 0, 0.5f));
Button actionBtn = new Button("action", btnStyle, "actionBtn");
actionBtn.setBounds(20, 20, 120, 40);
actionBtn.setClickListener(new ClickListener() {
@Override
public void click(Actor actor) {
Gdx.app.log("UI_EVENT", "Button was clicked");
}
});
uiStage.addActor(actionBtn);
Checkbox Controls
Checkboxes use a CheckBoxStyle requiring two TextureRegion instances for checked and unchecked visuals, plus a font and color.
Texture uncheckedTex = new Texture(Gdx.files.internal("box_off.png"));
Texture checkedTex = new Texture(Gdx.files.internal("box_on.png"));
CheckBox.CheckBoxStyle chkStyle = new CheckBox.CheckBoxStyle(
new TextureRegion(uncheckedTex),
new TextureRegion(checkedTex),
uiFont,
new Color(1, 1, 1, 1)
);
CheckBox agreeChk = new CheckBox("agree", chkStyle, "agreeCheck");
agreeChk.setBounds(150, 150, 180, 40);
agreeChk.setText("Accept");
agreeChk.setClickListener(new ClickListener() {
@Override
public void click(Actor actor) {
agreeChk.setText(agreeChk.isChecked ? "Accepted" : "Declined");
}
});
uiStage.addActor(agreeChk);
Slider Mechanics and Style Configuration
A SliderStyle demands a NinePatch for the track background and a standard Texture for the draggable knob. While styles are often defined programmatically, they can also be loaded via configuration files. For instance, NinePatch regions can be declared using coordinate and dimension mappings:
"customPatch": [ { "height": 13, "width": 9, "x": 761, "y": 78 }, { "height": 13, "width": 1, "x": 770, "y": 78 }, { "height": 13, "width": 9, "x": 771, "y": 78 }, { "height": 1, "width": 9, "x": 761, "y": 91 }, { "height": 1, "width": 1, "x": 770, "y": 91 }, { "height": 1, "width": 9, "x": 771, "y": 91 }, { "height": 13, "width": 9, "x": 761, "y": 92 }, { "hieght": 13, "width": 1, "x": 770, "y": 92 }, { "height": 13, "width": 9, "x": 771, "y": 92 } ]
Always ensure that explicit width and height parameters are set for dimension-dependent widgets to prevent rendering anomalies.