libgdx comes with a build of TWL, the Themeable Widget Library. This library can be used to build user interfaces including components like buttons, scrolling text areas, lists, clickable image maps, and so on. It allows the programmer to theme elements in a manner similar to the CSS. Neat. libgdx’s TWL class uses the SpriteBatch class to render components to the screen.
In this example I’ll describe how to add buttons and text components to a layout, and how to detect clicks on the components. The result will be a simple click/tap counter. This write-up is based on the TWL tests found in the libgdx source tree.
Create a project pair
As before (with one change) create a pair of projects, one for the desktop and the other for Android. Call them “PlayingWithTWL” and “PlayingWithTWLAndroid” and use the package name “com.example.PlayingWithTWL”. That one change: in each project, include gdx-twl.jar when you add libraries the lib directory and the build path. If you don’t do this at the start you will end up with a mess in your code, as TWL shares some common class names with the already-included AWT. Create a directories in your desktop project root and your Android project’s assets directory called “data”. This directory will hold your font and your theme XML.
Font files
You’ll need to include these two files in your data directories. droidserif.fnt and droidserif.png. These were generated using Hiero and the official Droid Serif font (Apache licensed). Its use is outside the scope of this post, however, something worth noting: every font size you wish to use in your application will need to be present as a .fnt and .png file, or you’ll need to scale your fonts up and down within your application.
PlayingWithTWL.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | package com.example.PlayingWithTWL; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Files.FileType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.twl.Layout; import com.badlogic.gdx.twl.TWL; import de.matthiasmann.twl.Button; import de.matthiasmann.twl.TextArea; import de.matthiasmann.twl.textarea.SimpleTextAreaModel; public class PlayingWithTWL implements ApplicationListener { private TWL twl; private SpriteBatch spriteBatch; private int clicks; private SimpleTextAreaModel clickText; @Override public void create() { spriteBatch = new SpriteBatch(); SimpleTextAreaModel mainText = new SimpleTextAreaModel(); mainText.setText("Click the button to count. See how high you can go!"); TextArea mainTextArea = new TextArea(mainText); clickText = new SimpleTextAreaModel(); clickText.setText("0"); TextArea clickCounter = new TextArea(clickText); Button clickButton = new Button(); clickButton.setText("click me"); clickButton.addCallback(new Runnable() { @Override public void run() { clicks++; } }); Layout layout = new Layout(); layout.horizontal().parallel(mainTextArea, clickCounter, clickButton); layout.vertical().sequence(mainTextArea, 5, clickCounter, 5, clickButton, 5); twl = new TWL(spriteBatch, "data/widgets.xml", FileType.Internal, layout); Gdx.input.setInputProcessor(twl); clicks = 0; } @Override public void resume() {} @Override public void render() { Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); clickText.setText(clicks + ""); twl.render(); try { Thread.sleep(16); // ~60FPS } catch (InterruptedException e) { } } @Override public void resize(int width, int height) {} @Override public void pause() {} @Override public void dispose() { spriteBatch.dispose(); twl.dispose(); } } |
Here’s what’s going on in this file.
26 27 28 | SimpleTextAreaModel mainText = new SimpleTextAreaModel(); mainText.setText("Click the button to count. See how high you can go!"); TextArea mainTextArea = new TextArea(mainText); |
I think this is pretty self-explanatory. Here, I’m creating an object to hold the text I want to display, and then I am loading it in to a TextArea Widget where it can be included in the layout. As the name implies SimpleTextAreaModel is pretty simple. TextArea supports word-wrapping and embedded newlines (\n). If you want to get fancy you can use the HTMLTextAreaModel (which allows DOM interaction).
37 38 39 40 41 42 | clickButton.addCallback(new Runnable() { @Override public void run() { clicks++; } }); |
This should also look pretty familiar, if you’ve done any Android programming. Where you might usually use setOnClickListener(new OnClickListener() { public void onClick() { } }); you’ll use addCallback, Runnable, and run instead. I’m not going out of my way to be thread-safe here (just incrementing a number) but it may be worth being careful if you’re doing something fancier.
44 45 46 47 | Layout layout = new Layout(); layout.horizontal().parallel(mainTextArea, clickCounter, clickButton); layout.vertical().sequence(mainTextArea, 5, clickCounter, 5, clickButton, 5); |
This part is a bit trickier. Layout is part of libgdx, acting as an interface in to TWL, specifically wrapping around TWL’s DialogLayout. In a DialogLayout, every child Widget must added to both the “horizontal” and “vertical” axes. These axis define the position of the object along the x- and y-axis. On line 45, the code is adding the three Widgets to the horizontal “parallel” group in TWL. Objects in the same parallel group share the same position, so left alone this line would mean that all three objects should be rendered in the same place. On line 46, the code is defining the order in which the objects will appear on the screen, vertically. The numbers are pixel-size gaps that will appear between the elements. The result of this block is three separate widgets, aligned along the left edge of the screen, separated by a small amount. See TWL’s DialogLayout javadoc for more details on this. If you forget to add a widget to one or the other axes you will see a pretty darn verbose IllegalStateException such as: “Widget de.matthiasmann.twl.TextArea@2c79809 with theme textarea is not part of the following groups: horizontal”.
widgets.xml
This file goes in your data directories. The layout of this file is well documented elsewhere, such as on the official site, but I’ll touch on it briefly here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | < ?xml version="1.0" encoding="UTF-8"?> < !DOCTYPE themes PUBLIC "-//www.matthiasmann.de//TWL-Theme//EN" "http://hg.l33tlabs.org/twl/raw-file/tip/src/de/matthiasmann/twl/theme/theme.dtd"> <themes> <fontdef name="normal" filename="droidserif.fnt" color="#FFFFFF"> <fontparam if="error" color="red" /> <fontparam if="warning" color="orange" /> <fontparam if="disabled" color="gray" /> <fontparam if="textSelection" color="blue" /> </fontdef> <fontdef name="button" filename="droidserif.fnt" color="#FF0000"> <fontparam if="error" color="red" /> <fontparam if="warning" color="orange" /> <fontparam if="disabled" color="gray" /> <fontparam if="textSelection" color="blue" /> <fontparam if="pressed|selected" offsetX="2" offsetY="2"/> </fontdef> <theme name="-defaults"> <param name="background"><image>none</image></param> <param name="overlay"><image>none</image></param> <param name="font"><font>normal</font></param> <param name="textAlignment"> <enum type="alignment">left</enum> </param> <param name="minWidth"><int>0</int></param> <param name="minHeight"><int>0</int></param> <param name="maxWidth"><int>0</int></param> <param name="maxHeight"><int>0</int></param> </theme> <theme name="tooltipwindow" ref="-defaults"> <param name="fadeInTime"><int>200</int></param> </theme> <theme name="textarea" ref="-defaults"> <param name="fonts"> <map> <param name="default"><font>normal</font></param> </map> </param> <param name="images"><map></map></param> </theme> <theme name="button" ref="-defaults"> <param name="border"><border>10,0</border></param> <param name="font"><font>normal</font></param> </theme> </themes> |
Let’s go over this file (as best as I can describe, for now). For starters, the order of elements used in this file is very important. You should put any fonts or images (not covered here) at the top of the file so you can refer to them later on in your code. If you mix up the order you will see useful exceptions telling you what went wrong.
4 5 6 7 8 9 | <fontdef name="normal" filename="droidserif.fnt" color="#FFFFFF"> <fontparam if="error" color="red" /> <fontparam if="warning" color="orange" /> <fontparam if="disabled" color="gray" /> <fontparam if="textSelection" color="blue" /> </fontdef> |
In order to use a font we need to define it and give it a name. This name will be used everywhere else in the file whenever you need to set a font (as in a TextArea or Button). These definitions should occur early in the file. See notes above on “Font files” for more information on fonts.
15 | <fontparam if="pressed|selected" offsetX="2" offsetY="2"/> |
This is a neat and simple way to give the user feedback. The font is simply offset by 2 pixels in both directions while the user is clicking.
18 19 20 21 22 23 24 25 26 27 28 29 | <theme name="-defaults"> <param name="background"><image>none</image></param> <param name="overlay"><image>none</image></param> <param name="font"><font>normal</font></param> <param name="textAlignment"> <enum type="alignment">left</enum> </param> <param name="minWidth"><int>0</int></param> <param name="minHeight"><int>0</int></param> <param name="maxWidth"><int>0</int></param> <param name="maxHeight"><int>0</int></param> </theme> |
Here the XML defines a theme that can be used for any following element. There’s nothing special about the dash in the name or the word “defaults”. It is used later on in “ref” attributes (see line 31). This allows you to set a common theme to be used throughout your application and then make small changes to it per-element. This is what I meant when I described it as being like CSS. You can think of this as the style for the “body” tag of your application.
35 36 37 38 39 40 41 42 | <theme name="textarea" ref="-defaults"> <param name="fonts"> <map> <param name="default"><font>normal</font></param> </map> </param> <param name="images"><map></map></param> </theme> |
This block defines the default theme to be used for all TextAreas. It creates a map of fonts to use (only defining the default font). Fancier TextAreas could use multiple fonts. This code isn’t using any images, so the image map is empty. If it wasn’t defined the code would generate a warning.
44 45 46 47 | <theme name="button" ref="-defaults"> <param name="border"><border>10,0,10,0</border></param> <param name="font"><font>button</font></param> </theme> |
This works much the same way as the textarea theme, but with buttons. The border is defined top, left, bottom, right.
The “tooltipwindow” theme is not required but without it you’ll get a warning to the console.
There’s a *ton* of things you can do in a theme file. I strongly suggest reading over the official TWL theme documentation if you are going to use it in your game.
The result
When you run the program you should see a line of text, a number, and a button with a red-font, shifted to the right just a bit (due to the border). Clicking or touching the button should cause the number to rise. My high score is 11, see what you can do!
Other things to try
Try using HTML (as HTMLTextAreaModel) in the TextArea constructor. This allows (potentially) more familiar control over how the text is laid out. Try nesting elements inside of ScrollPanes. Try image-based buttons that visually react to your clicking, and image maps that allow you to set click areas.
Due to Exceptions I had to change these in widgets.xml:
fontdef -> fontDef
fontparam -> fontParam
I have widget.xml file its for 800*480 screen size i want same xml to run for different screen resolutions.
Thank you very much! I will try it out…
Hi all.. how can i use my TWl theme – in libgdx Stage class ?
i mean i want add TWl widgets as Libgdx actors..
Thanks.. :)
displays this error…. :(
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): FATAL EXCEPTION: GLThread
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): com.badlogic.gdx.utils.GdxRuntimeException: Error loading theme: data/widgets.xml (Internal)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.twl.TWL.(TWL.java:92)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.twl.TWL.(TWL.java:68)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.fishgame.FishGame.create(FishGame.java:58)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.backends.android.AndroidGraphics.onSurfaceChanged(AndroidGraphics.java:292)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.backends.android.surfaceview.GLSurfaceViewCupcake$GLThread.guardedRun(GLSurfaceViewCupcake.java:708)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.backends.android.surfaceview.GLSurfaceViewCupcake$GLThread.run(GLSurfaceViewCupcake.java:646)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): Caused by: java.io.IOException: while parsing Theme XML: gdx-twl://local:80widgets.xml
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at de.matthiasmann.twl.theme.ThemeManager.parseThemeFile(ThemeManager.java:268)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at de.matthiasmann.twl.theme.ThemeManager.createThemeManager(ThemeManager.java:186)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at de.matthiasmann.twl.theme.ThemeManager.createThemeManager(ThemeManager.java:151)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.twl.TWL.(TWL.java:90)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): … 5 more
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): Caused by: com.badlogic.gdx.utils.GdxRuntimeException: Error loading font file: data/font.fnt
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.graphics.g2d.BitmapFont$BitmapFontData.(BitmapFont.java:217)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.graphics.g2d.BitmapFont.(BitmapFont.java:302)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.graphics.g2d.BitmapFont.(BitmapFont.java:294)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.twl.renderer.GdxCacheContext.loadBitmapFont(GdxCacheContext.java:56)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.twl.renderer.GdxRenderer.loadFont(GdxRenderer.java:136)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at de.matthiasmann.twl.theme.ThemeManager.parseFont(ThemeManager.java:382)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at de.matthiasmann.twl.theme.ThemeManager.parseThemeFile(ThemeManager.java:302)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at de.matthiasmann.twl.theme.ThemeManager.parseThemeFile(ThemeManager.java:263)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): … 8 more
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): Caused by: com.badlogic.gdx.utils.GdxRuntimeException: Invalid font file: data/font.fnt
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): at com.badlogic.gdx.graphics.g2d.BitmapFont$BitmapFontData.(BitmapFont.java:101)
08-08 12:48:50.342: ERROR/AndroidRuntime(9041): … 15 more
The error is caused because you forgot to include the file droidserif.fnt and the Java compiler is looking for the file font.fnt
Hello~I got this error, any help will be appreciated~
01-24 18:30:25.276: E/AndroidRuntime(1158): FATAL EXCEPTION: GLThread 108
01-24 18:30:25.276: E/AndroidRuntime(1158): java.lang.NoSuchMethodError: com.badlogic.gdx.graphics.g2d.BitmapFont.computeVisibleGlpyhs
01-24 18:30:25.276: E/AndroidRuntime(1158): at com.badlogic.gdx.twl.renderer.GdxFont.computeVisibleGlpyhs(GdxFont.java:146)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.TextArea.layoutTextPre(TextArea.java:976)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.TextArea.layoutTextElement(TextArea.java:819)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.TextArea.layoutElement(TextArea.java:550)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.TextArea.layoutElements(TextArea.java:542)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.TextArea.layout(TextArea.java:366)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.Widget.validateLayout(Widget.java:919)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.TextArea.getPreferredInnerHeight(TextArea.java:315)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.Widget.getPreferredHeight(Widget.java:797)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.DialogLayout$WidgetSpring.prepare(DialogLayout.java:691)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.DialogLayout.prepare(DialogLayout.java:411)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.DialogLayout.layout(DialogLayout.java:387)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.Widget.validateLayout(Widget.java:919)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.Widget.validateLayout(Widget.java:923)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.Widget.validateLayout(Widget.java:923)
01-24 18:30:25.276: E/AndroidRuntime(1158): at de.matthiasmann.twl.GUI.validateLayout(GUI.java:494)
01-24 18:30:25.276: E/AndroidRuntime(1158): at com.badlogic.gdx.twl.TWL.render(TWL.java:136)
01-24 18:30:25.276: E/AndroidRuntime(1158): at org.binhua.mytank.TestScreen.render(TestScreen.java:44)
01-24 18:30:25.276: E/AndroidRuntime(1158): at com.badlogic.gdx.Game.render(Game.java:46)
01-24 18:30:25.276: E/AndroidRuntime(1158): at com.badlogic.gdx.backends.android.AndroidGraphics.onDrawFrame(AndroidGraphics.java:449)
01-24 18:30:25.276: E/AndroidRuntime(1158): at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1516)
01-24 18:30:25.276: E/AndroidRuntime(1158): at android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1240)
I also got the computeVisibleGlpyhs in the same place. Do we need to recompile gdx-twl.jar ?