-
Notifications
You must be signed in to change notification settings - Fork 25
Actor injection
Array<Actor>
returned by LmlParser#parseTemplate(FileHandle)
is OK, if all you just want to quickly create some actors. LmlParser#fillStage(FileHandle, Stage)
is fine, if you just want to create some actors and put them on a Stage
.
But most views require direct access to specific actors. Accessing ID-mapped actors with LmlParser#getActorsMappedByIds()
after parsing is an option, but it gets tedious, uses magic ID strings (or a ton of constants) and needs constant casting from Actor
, so it might not be a maintainable option in the long run. However, there's a much cleaner alternative.
There are a few LmlParser#createView
methods that consume view class or view object as an argument. The view can - but does not have to - implement LmlView
interface. If it does, LmlView#getStage()
method will be called after parsing and root actors will be appended to the view's Stage
.
LmlView
can also be an ActionContainer
. There is even an abstract base prepared for this kind of situation: AbstractLmlView
(you just have to pass a Stage
to the constuctor, choose a unique view ID and that's it). If a view passed to the LmlParser
implements ActionContainer
interface, it will be added as an action container with ID returned by LmlView#getViewId() before parsing. The action container will be removed after parsing, so it is not available for other views.
The biggest advantage of using a #createView
method is the fact that passing the ID-mapped actors to Java is handled for you. For example, you consider this template:
<table id=root>
<button id=main>@main</button>
<button id=credits>@credits</button>
<button id=exit>@exit</button>
</table>
Let's say you need a reference to your root table - to add actions changing the look of the whole structure - and a collection with your buttons for some fancy effects when you change screens. If you let the LML parser create your view, it will look for annotated fields and inject the actors that you need:
@LmlActor("root")
private Table table;
@LmlActor({ "main", "credits", "exit" })
private Array<Button> buttons;
These fields will be filled after parsing of the LML template. As you probably already noticed, multiple actors injection is supported with LibGDX collections. If you pass 1 actor ID to the annotation, the field's type has to match a superclass/interface/actual type of the actor (to avoid ClassCastException
during injection); if multiple actor IDs are passed, you can use either of this collections:
-
Array
: adds actors in the same order as their IDs were specified in the annotations. If an actor with the selected ID is unavailable, addsnull
to the array. Duplicates are not removed. -
ObjectSet
: order is not preserved. If actor with the selected ID is missing,null
is ignored - only present actors will be added. Duplicates are removed. -
ObjectMap
: has to useString
as keys. Will map actors by their IDs. If an actor with the selected ID is missing, will mapnull
value to the ID.
There is no point in initiating collection fields yourself - they will be created for you.
@LmlActor
annotation is also accepts LML arrays, so the second field could look like this:
@LmlActor("main;credits;exit")
private ObjectSet<Button> buttons;
This is especially useful when injecting actors created with for-each loop thanks to using the same arrays.
There is also @OnChange
annotation that you can use on your fields. They will find the actor with the ID specified in the annotation and add a ChangeListener
that will update field value according to actor's state each time it changes. This feature is supported for: buttons (boolean
; checked status), progress bars (float
; current value), text fields (String
; current text) and lists/select boxes (String/ArraySelection
; current selected value or current array selection).
Custom change processors can be easily added to LmlData
with #addOnChangeProcessor(OnChangeProcessor)
. See AbstractOnChangeProcessor
for a decent OnChangeProcessor
base.
An example of your typical view with some mock-up functionalities:
public class MyView extends AbstractLmlView {
@LmlActor("button")
private Button someButton;
@LmlActor({ "title0", "title1", "title2" })
private Array<Label> titleLabels;
@OnChange("loadingBar")
private float loadingProgress;
public MyView() {
super(StageProvider.newStage());
}
@Override
public String getViewId() {
return "myView";
}
@LmlAction("randomTitle")
public String getRandomTitleLabelStyle() {
return ImmutableArray.of("red", "green", "blue").random();
}
@Override
public void render() {
MyAssetManager.update();
super.render();
}
public float getLoadingProgress() {
return loadingProgress;
}
}
(...)
// Manual creation:
MyView myView = new MyView();
Array<Actor> rootActors = lmlParser.createView(myView, Gdx.files.internal("someTemplate.lml"));
// Do something with rootActors if you need them; this method is mainly
// meant to support views not implementing LmlView
// Using reflection:
MyView myView = lmlParser.createView(MyView.class, Gdx.files.internal("someTemplate.lml"));
// General usage of AbstractLmlView:
myView.resize(width, height); // viewport update
myView.render(); // stage: act() + draw()
myView.dispose() // stage: dispose()
Methods and fields are also extracted from and filled in superclasses, so you can use @LmlActor
, @OnChange
and @LmlAction
annotations in your own abstract views - they should work. If they don't, blame me (and let me know).
LmlApplicationListener
allows you to manage multiple AbstractLmlView
instances. What LibGDX Game
is to Screen
, LmlApplicationListener
is the same thing to AbstractLmlView
. Your first LML application explains the usage of LmlApplicationListener
in detail.
MJ 2016