Skip to content
This repository has been archived by the owner on May 27, 2021. It is now read-only.

Actor injection

MJ edited this page Oct 8, 2016 · 4 revisions

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, adds null 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 use String as keys. Will map actors by their IDs. If an actor with the selected ID is missing, will map null 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.