by Alexander Kouznetsov and José Pereda
"Write once, run anywhere"—vibrant efforts by the OpenJFX community make this vision real in the tough area of developing for mobile devices. Now you can use OpenJFX to develop cross-platform applications that can run on Android, iOS, and desktop devices. This article describes the transition of a desktop OpenJFX application into a cross-platform application using Gluon. Caveats, tips, and tricks are demonstrated.
Part 1. Getting into Mobile
Welcome to Gluon
OpenJFX by itself is not ready to run on mobile devices, nor does it provide tools to configure, debug, and deploy apps. However, the Gluon team has made great progress in this area by providing frameworks and tools for OpenJFX to be used for mobile development.
As an example, in this article, we will demonstrate how to convert an existing OpenJFX game application into a mobile app that runs on Android, iOS, and desktop devices. The app will also be enhanced to use some of the Gluon-provided features, such as cloud and native-platform support.
Project Creation
Gluon has support for all major IDEs, as well as Gradle templates. In this article, NetBeans will be used as the primary development tool. In order to prepare NetBeans for use, a Gluon plugin needs to be installed by using the Plugin Manager available from the Tools menu.
Note: It is important to have a Gradle support plugin installed, because the Gluon plugin requires it, even though NetBeans doesn't list that in its dependencies. In case of other IDEs, the plugin will manage to install the Gradle support plugin if necessary.
After the necessary plugins are installed, create an application project using the New Project wizard. The following are the key points:
- Select the Gluon category, and choose one of the Gluon Mobile projects: Single View, Multi View or Multi View with FXML.
- Select the required platforms where the project will be deployed: Android, iOS, Desktop, or Embedded.
Tip: Selecting Desktop is very useful for project development, because it is the easiest way to develop, test, and debug parts of the project that are common to all the platforms.
The wizard will create the structure of the project, which includes source trees for Source Packages and, depending on the selected platforms, Android/Java Packages, Ios/Java Packages, Desktop/Java Packages, and/or Embedded/Java Packages. There are corresponding Resources trees as well.
In most cases, Source Packages [Java] is the only source tree where development will happen, because all the platform specifics are hidden from the developer.
Next, drop the existing source code for your application into this shared Source Packages [Java] source tree.
Dealing with the Source Level
The source level set for a Gluon application is JDK 8. Because mobile platforms currently support JDK 7, this means that JDK 8 source code needs to be backported into JDK 7. This mainly includes removing new APIs, such as the Stream API, and some additions to existing APIs, such as the Comparator.reverseOrder() method. Another thing is that effectively final variables in JDK 7 have to be explicitly final. On the other side, lambda expressions can be used given that the Retrolambda plugin converts internally any lambda expression found in the source code (not in third-party dependencies) into anonymous inner classes.
Tip: It might be tricky to completely backport JDK 8 source code into JDK 7, especially with respect to preserving lambdas, because IDEs might not support this particular source-level configuration. A general piece of advice would be to set the source level and target platform to JDK 7 and fix all the issues shown by the IDE, while keeping lambdas untouched. If the target platform is set to JDK 8, it is likely that some of the newer API usages might be overlooked, which will lead to unexpected crashes when the code is running on actual devices.
Note: JDK 8 APIs that need to be removed include the following:
- Comparator.reverseOrder()
- Collection.forEach
- Effectively final variables
- The Stream API
Getting Through the Corporate Firewall
Because the basic Gluon project uses a combination of Gradle and Maven, it is important to set proxies for each of them in case the build is executed from behind a firewall.
Gradle proxy settings are set in the <GRADLE_HOME>/gradle.properties file, as follows:
systemProp.http.proxyHost=proxy.example.com
systemProp.http.proxyPort=80
systemProp.http.nonProxyHosts=*.example.com|localhost
Tip: GRADLE_HOME by default is USER_HOME/.gradle, but it may be reconfigured to the GRADLE_USER_HOME environment variable.
Maven proxy settings are set in <USER_HOME>/.m2/settings.xml, as follows:
<settings>
\<proxies>
\<proxy>
\<active>true\</active>
\<protocol>http\</protocol>
\<host>proxy.example.com\</host>
\<port>80\</port>
\<nonProxyHosts>\*.example.com|localhost\</nonProxyHosts>
\</proxy>
...
Tip: Proxy configuration issues are really annoying, because proxy settings need to be configured in different places and any mistake will cause a long pause in the build, which will result in a build failure. Unfortunately, error messages do not always properly indicate a network connectivity issue, so a long pause is the best indicator that something is wrong due to networking and proxies.
Running the App
Running the app is an important step to verify that the project is configured properly, all the necessary libraries and tools are installed, and the code has the proper source level and is working as expected on the target device.
Tip: Reviewing application logs is the best way to troubleshoot app failures. The next sections will provide tips on what tools to use.
Tip: It is often easier to make a "Hello, World" application work first before dealing with the issues of a real application.
Desktop
Running the app on the desktop is pretty straightforward. Execute the Run task of the gradle.build file. All IDEs are configured by default to run the app in desktop mode.
This is an essential step to ensure that the application is still working after its source code was transplanted into a different project. This step is quite likely to catch missing dependencies and other misconfigurations at this stage.
Android
Note: Running the app on Android requires the Android SDK to be installed.
Note: Basic Gluon Application won't run in the Emulator. Real devices need to be used.
Connect the Android phone using a USB cable. Make sure the phone is connected as a media device and developer options are activated.
The androidInstall task in the project's build.gradle file will compile and install the application's Android application package (APK) file to the currently connected device.
Tip: In NetBeans, right-click the application's project node in the Projects view and select one of the tasks in Tasks menu.
Tip: It is possible to configure NetBeans to execute the androidInstall task as a Run Project action. This can be set in the Project Properties, Built-In Tasks screen, as shown below:
After the app is installed on the device, it can be run on the device from the list of all the apps. Currently, there is no Gradle task that will run the app on the device, but it can be easily added to the build.gradle file:
task androidRun(type: Exec) {
def adb = "${ANDROID\_HOME}/platform-tools/adb"
def dotIndex = mainClassName.lastIndexOf('.')
if (dotIndex != -1) {
def packageName = mainClassName\[0..\<dotIndex\]
commandLine "$adb", 'shell', 'am', 'start', '-n', "$packageName/javafxports.android.FXActivity"
}
}
Tip: It is important to use appropriate Android SDK tools to receive logging information from the device. From the command line, under <ANDROID_SDK>/platform-tools, running adb logcat will dump to the console all logging messages. Android Device Monitor is one of the GUI-based tools available. It can be found in <ANDROID_SDK>/tools/monitor. Another option is to use Android Studio, which has an Android Monitor view with similar functionality.
iPhone
To deploy onto an iPhone, it is necessary to have a Mac-based system with XCode installed. A virtual image (such as one provided by Oracle VM VirtualBox) might also work.
The following are the tasks to use:
- launchIOSDevice
- launchIPadSimulator
- launchIPhoneSimulator
Unlike with Android, a Gluon app can be run in the simulator. For example, after the project is copied to the Mac OS system, it can be run in simulator using the gradlew launchIPhoneSimulator or gradlew launchIPadSimulator command. Be aware that the performance won't be as good as on a real device.
Note: It is important that the system is not locked when simulator task is invoked.
To run on a physical device, the device must be connected to the system with the USB cable and the following command should be executed: gradlew launchIOSDevice.
Note: The following error message might appear:
Execution failed for task ':launchIOSDevice'.
> No signing identity found matching '/(?i)iPhone Developer|iOS Development/'
In that case, some additional steps are needed. The following article describes the additional steps in more detail: http://docs.robovm.com/getting-started/provisioning.html
Note: The following error message might appear on the device:
Execution failed for task ':launchIOSDevice'.
> org.apache.commons.exec.ExecuteException: Command 'codesign -f -s <sha1 fingerprint> --entitlements <project path>/build/javafxports/tmp/ios/Entitlements.plist <project path>/build/javafxports/tmp/ios/<project name>.app' failed (Exit value: 1)
If it does, it is most likely because the codesign command return the following error: User interaction is not allowed. This happens when the command is invoked from an SSH terminal, not from the terminal opened directly in the logged-in session of the Mac OS system.
Note: It is important that the iOS device is not locked when the app is executed on it. Otherwise, the following exception will be shown:
AppLauncher failed with an exception:
java.lang.RuntimeException: Launch failed: Locked
In this case, the application will be installed, and can be started on the device after unlocking the device, but logging messages won't be shown on the console.
Note: The following messages are quite expected:
[WARN] java.lang.Class: Class.forName() failed to load 'org.glassfish.json.JsonProviderImpl'. Use the -forcelinkclasses command line option or add <forceLinkClasses><pattern>org.glassfish.json.JsonProviderImpl</pattern></forceLinkClasses> to your robovm.xml file to link it in.
javax.json.JsonException: Provider org.glassfish.json.JsonProviderImpl not found
Caused by: java.lang.ClassNotFoundException: org.glassfish.json.JsonProviderImpl
When something like this happens, it does not necessarily mean there is an issue. For example, there are no classes com.sun.javafx.font.t2k.T2KFactory, com.sun.javafx.tk.quantum.*, or com.oracle.jrockit.jfr.FlightRecorder when running on iOS. However, classes that must be linked in, even if not referenced directly or indirectly from the main class, need to be added into build.gradle script like this:
jfxmobile {
ios {
forceLinkClasses = \[
'org.glassfish.json.\*\*'
\]
}
}
Summary
At the end of Part 1, you should have a working basic Gluon application project that has been successfully executed on all the target platforms and the desktop.
Part 2. Adjusting the Application for the Reality of Mobile Devices
Although the application is already working, most likely it was not designed for mobile devices. So it might have been designed for a larger screen, mouse input, multiple windows, a different lifecycle, and so on.
Screen Size
When the application is running on the target device, as opposed to running on the desktop, it needs to always occupy the full screen of the device.
The following switch statement is helpful to set up the application, depending on what platform the app is running on:
switch (PlatformFactory.getPlatform().getName()) {
case PlatformFactory.ANDROID:
// Setup for Android
break;
case PlatformFactory.IOS:
// Setup for IOS
break;
case PlatformFactory.DESKTOP:
// Setup for the desktop
break;
}
PlatformFactory here is the com.gluonhq.charm.down.common.PlatformFactory class.
On the desktop, screen size can be determined using the regular OpenJFX API:
Screen screen = Screen.getPrimary();
double width = screen.getBounds().getWidth();
double height = screen.getBounds().getHeight();
Or, if you switch to the MobileApplication base class, as will be shown in Part 3 of this article, you can use its methods MobileApplication.getScreenWidth() and MobileApplication.getScreenHeight().
Multiple Windows
The best advice is not to use multiple windows/stages. Although it is possible to instantiate new ones, the user won't have an opportunity to move them around or resize them. All the stages will be borderless. As was mentioned earlier, the application should always occupy the whole screen of the mobile device.
Instead, the same root stage should be reused and its content should be swapped when necessary.
A commercial offering of Gluon provides a base MobileApplication class with a concept of views. It will be shown in Part 3 in more detail.
Touch Input Versus Mouse Input
In many cases, touch input for the application will work out of the box. A standard mapping of touch events to mouse events is implemented in the OpenJFX platform. However, there are cases when tweaking is needed, especially when the application depends on a combination of mouse input events.
For example, the onMouseMoved() event will never happen on the mobile device, because there is no mouse cursor. So most tooltips will not work.
Another example is that scroll events will happen much more often because any dragging will also result in a scroll event. With a mouse, a scroll event is produced only when a mouse wheel is rotated.
The application could also take advantage of multiple touch point events. Some, such as resize and rotate, are provided by OpenJFX. But in general, it is necessary to implement processing of additional touch points because most of the default processing is related to the first event or to the special gestures. For example, it might be beneficial to allow dragging of two (or more) objects simultaneously with individual touch points.
Application Lifecycle
Mobile applications have a different lifecycle compared to desktop applications. Most importantly, a mobile application could be killed at any moment and it is expected that once the user reruns it, its state is restored.
Here is example code that could register a listener for lifecycle events.
PlatformFactory.getPlatform().setOnLifecycleEvent((LifecycleEvent param) -> {
switch (param) {
case RESUME:
Platform.runLater(() -> {
paused.set(false);
});
break;
case PAUSE:
Platform.runLater(() -> {
paused.set(true);
saveState();
});
break;
case START:
// Do something on start if needed
break;
case STOP:
// Not on iOS
break;
}
return null;
});
There are four events: PAUSE and RESUME and START and STOP. The application gets a PAUSE event each time it goes offscreen, for example, when a user switches to a different app or answers a call. RESUME indicates switching back. On Android, STOP is invoked when the system is getting ready to kill the app, which might be the last time the app's state can be saved.
Unfortunately, these events happen on the main thread, not on the OpenJFX dispatching thread. So you have to wrap in Platform.runLater() any code that is updating or reading the state of the OpenJFX model.
In the example above, BooleanPropertypaused is used to control animations in the app. For simple applications, the property can be bound directly to each animation pausedProperty(), as follows:
animation.pausedProperty().bind(paused);
When an app is running on Android 4.4.4 (KitKat), whenever the user is switching from the app to the Android home page, a PAUSE event is issued first, and then in about a second a STOP event is issued. If the user switches back to the app before the app was killed, a START event is issued immediately followed by RESUME event, as shown in this debug log:
01-04 17:59:32.830 13813-13813/com.kamtris I/System.out: LifecycleEvent = PAUSE, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 17:59:33.490 13813-13813/com.kamtris I/System.out: LifecycleEvent = STOP, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 17:59:51.068 13813-13813/com.kamtris I/System.out: LifecycleEvent = START, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 17:59:51.068 13813-13813/com.kamtris I/System.out: LifecycleEvent = RESUME, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 17:59:57.894 13813-13813/com.kamtris I/System.out: LifecycleEvent = PAUSE, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 17:59:58.485 13813-13813/com.kamtris I/System.out: LifecycleEvent = STOP, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 18:00:25.401 13813-13813/com.kamtris I/System.out: LifecycleEvent = START, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 18:00:25.401 13813-13813/com.kamtris I/System.out: LifecycleEvent = RESUME, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 18:00:31.637 13813-13813/com.kamtris I/System.out: LifecycleEvent = PAUSE, Thread = Thread[main,5,main], isFxApplicationThread = false
01-04 18:00:32.238 13813-13813/com.kamtris I/System.out: LifecycleEvent = STOP, Thread = Thread[main,5,main], isFxApplicationThread = false
It is also important to note that when the application is launched, START and RESUME events won't be received, because the LifecycleEvent listener is installed afterwards. The first event that is received (if any) is PAUSE, as shown in the log above. So anything related to initial setup should happen during the application's regular setup phase (init() method).
Note: These lifecycle events have different behavior on iOS versus. Android. For example, on Android, a STOP event always followed a PAUSE whereas on iOS devices, STOP is not used. Moreover, when the app is killed on iOS, it is killed without first sending a STOP event. So it is important to save any important data on the PAUSE event.
Saving Application State
Gluon provides several services that can be used to persist an application's state. And all of them work across different platforms.
Here they are:
- SettingService: Simple storage of String key-value pairs.
- PlatformFactory.getPlatform().getPrivateStorage(): A File object representing the directory on the file system where application files could be stored.
- StorageService: Sophisticated storage that can be local or in the cloud and that can also be shared across different devices. It supports several datatypes that are mapped to String keys.
SettingService
SettingService is good for storing simple states of an application that can be represented in a combination of Strings, such as a limited number of Strings, numbers, and Boolean values.
Here is a simple example of how to use SettingService:
private static final String EXECUTION\_COUNT = "executionCount";
private int executionCount = 0;
private void saveState() {
SettingService settingService = PlatformFactory.getPlatform().getSettingService();
settingService.store(EXECUTION\_COUNT, String.valueOf(executionCount));
}
private void loadState() {
SettingService settingService = PlatformFactory.getPlatform().getSettingService();
String executionCountString = settingService.retrieve(EXECUTION\_COUNT);
if (executionCountString != null) {
this.executionCount = Integer.valueOf(executionCountString);
}
}
If it is necessary to keep track of the number of executions of the application, the code above could store the current count. Of course, it is also necessary to increment the count after the app is loaded.
SettingService also allows you to remove the value from the storage by using the remove(String key) method.
Tip: These settings are stored:
- %USER_DIR%/.gluon/settings.properties in plain text on a desktop device
- A private secured data folder on Android devices
- A private application data folder on iOS devices
Private Storage
Private storage gives access to a fragment of the file system. It is the best place to store any kind of application data such as various files, binary or serialized objects. It is the best place to store any kind of application data including binary files and serialized objects.
Here is an example of the code using the private storage:
private static final String STATE\_FILE\_NAME = "state.dat";
private State loadState() {
ObjectInputStream objectInputStream = null;
State savedState = null;
try {
File privateStorage = PlatformFactory.getPlatform().getPrivateStorage();
File stateFile = new File(privateStorage, STATE\_FILE\_NAME);
if (stateFile.exists()) {
objectInputStream = new ObjectInputStream(new BufferedInputStream(new FileInputStream(stateFile)));
savedState = (State) objectInputStream.readObject();
}
} catch (IOException | ClassNotFoundException ex) {
ex.printStackTrace();
} finally {
if (objectInputStream != null) {
try {
objectInputStream.close();
} catch (IOException ex2) {
ex2.printStackTrace();
}
}
}
return savedState;
}
private void saveState(State state) {
ObjectOutputStream objectOutputStream = null;
try {
File privateStorage = PlatformFactory.getPlatform().getPrivateStorage();
File stateFile = new File(privateStorage, STATE\_FILE\_NAME);
objectOutputStream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(stateFile)));
objectOutputStream.writeObject(state);
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (objectOutputStream != null) {
try {
objectOutputStream.close();
} catch (IOException ex2) {
ex2.printStackTrace();
}
}
}
}
The code example above assumes that State object is serializable and it is persisted to the private storage in the saveState() method and loaded from the private storage in the loadState() method.
Note: In Gluon Mobile (Charm version 2.1.1), these files are created in the following locations:
- In %USER_DIR%/.gluon/ folder on desktop devices
- In a private secured data folder on Android devices
- In /var/mobile/Containers/Data/Application/<application UID>/Library/Caches/gluon on iPhones.
StorageService
StorageService is mostly used when the information needs to be stored in the cloud or shared between devices. It can also be used as a local storage facility.
In Gluon Mobile (Charm version 2.1.1), it is suitable for storing strings, lists, maps, and custom classes made of simple types or properties. The most beneficial part is that lists and properties are observable so that updates from the cloud (other devices) are immediately reflected on the property causing corresponding bindings to be updated and change listeners to be triggered.
Tip: A useful article on StorageService is available on the Gluon website: http://docs.gluonhq.com/charm/latest/#_storage_and_synchronization
Let's consider the following example: a hall of fame for a game. The hall of fame is a list that contains top scores for the players of the game. So essentially it needs to store a list of hall of fame entries. The hall of fame entries themselves can be implemented as a custom class with several fields, as follows:
public static class HallOfFameEntry {
String name;
int score;
}
Note: These fields cannot be final, because they cannot be reflectively initialized by Gluon.
Note: If the values of the entries can change remotely, they should be stored as properties like this:
public static class HallOfFameEntry {
StringProprety name;
IntegerProperty score;
}
Note: Only simple types and OpenJFX properties can be used as fields for the custom classes to be stored in a StorageService. If a class has any field of another type, the object won't be stored.
Note: Initialized property fields can be final.
There is no need to create the list itself because it will be created by Gluon upon retrieval. Here is the code to retrieve the list:
final String HALL\_OF\_FAME = "hallOfFame";
final String applicationKey = "...";
final String applicationSecret = "...";
GluonClient gluonClient = GluonClientBuilder.create()
.credentials(new GluonCredentials(applicationKey, applicationSecret))
.build();
StorageService storageService = gluonClient.getStorageService();
CharmObservableList\<HallOfFameEntry> listOfFame = storageService.retrieveList(HALL\_OF\_FAME, HallOfFameEntry.class,
StorageWhere.GLUONCLOUD,
SyncFlag.LIST\_READ\_THROUGH, SyncFlag.LIST\_WRITE\_THROUGH);
listOfFame.stateProperty().addListener((ObservableValue\<? extends CharmObservable.State> observable, CharmObservable.State oldValue, CharmObservable.State newValue) -> {
switch (newValue) {
case INITIALIZED:
// The value is retrieved
break;
}
});
Tip: The list is being retrieved asynchronously and it contains no data from the cloud until it gets into the INITIALIZED state.
Let's walk through the code above. In order to get access to the Gluon cloud, the application needs to be registered there (http://portal.gluonhq.com/). ApplicationKey and ApplicationSecret will be provided at the end of the registration. All the entries in the cloud are stored using String names. The one here is defined in line 1.
Note: Credentials are not needed if local storage is used instead of a cloud. In that case, line 6 is not needed and lines 12 and 13 should have only StorageWhere.DEVICE and no SyncFlag arguments.
After getting an instance of GluonClient and StorageService, the application can request necessary data from the cloud using retrieve_XXX_() methods. The name suggests that it is not a plain getter but rather an asynchronous call to the cloud. The returned value, which is a CharmObservable, has several additional fields and methods including the state property. Initially it holds no real value until the state changes to the INITIALIZED state. Then the value matches the cloud.
Retrieve() methods also taken an arbitrary number of SyncFlag arguments. These arguments define how the synchronization should happen. The LIST_READ_THROUGH flag requests that whenever a list has changed in the cloud, the application's copy of it should be immediately updated. The LIST_WRITE_THROUGH flag requests that any changes done locally to the list be immediately sent to cloud.
Note: At the moment, how conflicts are solved is not defined. Because two devices with relation to the cloud can behave similarly to two threads with relation common memory, most multithreading memory-use conflicts are applicable to shared cloud data. For example, a device might want to increment a counter stored on the cloud. Another device might do the same. Because the operation is not atomic, the counter might be incremented only once.
There are also flags OBJECT_READ_THROUGH and OBJECT_WRITE_THROUGH, which are applicable for other types of data such as strings, objects, or maps. They specify how changes in these datatypes are propagated to the cloud and back. They also have an additional meaning: when used with lists, they indicate that changes to the list items should be propagated to the cloud and back immediately.
Controls and Their Visual Appearance
All JavaFX controls can be used on mobile device platforms directly. There is no need to change anything. Everything will work out of the box.
However, they look exactly as they do on a desktop device, and this might be too much of a contrast compared to the native application's look and feel. Thus Gluon provides its own library of controls with "material" style. The library is called Glisten and it is a part of Gluon Mobile. This library is covered in Part 3 below.
Part 3. Using Glisten to Make Applications Look Native
Glisten is a commercial offering from Gluon that requires a license. It works without a license, but it shows a "nag screen" pop-up each time the application starts. Please refer to the Gluon website: http://gluonhq.com/.
To use this library, a MobileApplication class should be used instead of the OpenJFX Application class for the main class of the application:
public class HelloWorld extends MobileApplication {
...
}
When changing an existing application, additional steps need to be done:
-
The original start(Stage) method needs to be split into init() and postInit().
-
init() is responsible only for adding view factories, where a view factory is a producer that creates and returns a View of the application. View in Glisten is equivalent to the Scene class in OpenJFX. It takes a root of the scene graph hierarchy as an argument.
-
postInit() is the place to perform any other setup besides the scene creation that used to be in the start method.
-
It is recommended for the application UI to be redesigned to use views and layers. Whenever a desktop application would open an additional window, it might just switch to another view or display an overlay layer. The MobileApplication class gives control for views and layers with methods such as showLayer(), hideLayer(), switchView(), and switchToPreviousView().
The diff below summarizes the changes:
@Override
- public void start(Stage primaryStage) {
-
public void init() throws Exception {
-
super.init();
-
addViewFactory(HOME\_VIEW, () -> {
-
View view = createHomeView();
-
return view;
-
});
-
}
-
private View createHomeView() {
...
- Scene scene = new Scene(root);
-
View view = new View(root);
-
return view;
-
}
-
/**
-
* Called once we have a scene, allowing first run settings
-
* @param scene
-
*/
-
@Override
-
public void postInit(Scene scene){
-
Swatch.INDIGO.assignTo(scene);
Views
View is one of the building blocks of a mobile application in Glisten just as Scene is the building block for a desktop application. Its size always matches the size of the device screen so there is only one view visible at a time. The application may have others and switch between them.
Its layout is specified with MobileLayoutPane, which is an extension of the BorderPane that supports only top, bottom, and center nodes.
In addition to those, a view may have layers installed. Layers are the overlays that may add any additional UI elements.
By default, the application starts by showing its home view. By providing additional view factories and using MobileApplication.switchView() and switchToPreviousView(), more views can be used in the application.
Layers
Views occupy the whole screen of the device, while layers allow you to display some additional information on top of a view. A layer could display a message box or a score panel, to name a couple of examples. A layer has two modes of operation controlled by the autoHide property. If that property is set, the layer is hidden once a user taps outside the layer.
In terms of layout, Layer extends StackPane and, by default, it occupies the whole screen. However, it is transparent, so by limiting its child node size and setting an alignment, it can be made to appear at a specific location.
Below is an example of the layer that is used to display an overlay of the next piece in the Tetris game.
class PreviewLayer extends Layer implements Preview {
private final PreviewNode previewNode = new PreviewNode();
public PreviewLayer() {
super();
getChildren().add(previewNode);
setAutoHide(false);
setAlignment(Pos.TOP\_RIGHT);
}
}
class PreviewNode extends StackPane {
public PreviewNode() {
getChildren().add(content);
setPrefSize(elementSize \* 4, elementSize \* 4);
setMaxSize(USE\_PREF\_SIZE, USE\_PREF\_SIZE);
}
}
Material Design Look and Feel
A material design look and feel is enabled by default on Glisten. Many controls will get a material design UI look just by adding them to views and layouts.
However, not all the UI controls are styled. For example, TableView doesn't have a material design yet as of Charm 2.1.1. In order to fix this, you may provide additional CSS stylesheets that provide the missing styling.
The material design look and feel is configurable by assigning different values of Swatch and Theme enums to the scene, where Swatch sets the color scheme and Theme chooses between dark and light style. Swatch includes 19 color options in Charm 2.1.1.
Material design also provides a number of vector icons to be used, for example, with FloatingActionButton. These icons are enumerated with the MaterialDesignIcon enum and include more than 900 icons.
Glisten Controls
Glisten provides a number of built-in controls. Significant changes might need to be made to an application to start using them. However, some of them will give the application a mobile look. These include AppBar, NavigationDrawer, SnackbarPopupView, and others. It is beyond the scope of this article to describe each of them, but they're definitely worth considering. All of these controls can be found under the com.gluonhq.charm.glisten package.
Distribution
Gluon apps can be easily distributed through the major stores (Google Play for Android apps and the Apple Store for iOS apps). On Android, signing the APK is required with the androidRelease task. Check the documentation here: http://docs.gluonhq.com/charm/latest/#_android_2.
See Also
About the Authors
Alexander Kouznetsov, a principal software engineer at Oracle, has been member of the JavaFX demos and samples development and quality teams, and has also contributed to the JavaFX platform code. He was the major developer of JavaOne keynote demos featuring JavaFX, including a 3-D animated chessboard, a chess robot, an automotive demo, and earlier demos.
José Pereda, a software engineer at Gluon, has been developing commercial and open source JavaFX projects for the last four years, and JavaFX mobile apps for the last year, including the app 2048FX.
Join the Conversation
Join the Java community conversation on Facebook, Twitter, and the Oracle Java Blog!