javafxgluon-mobilejavafxports

Animation "ghosting" on xxhdpi Android javafxports application


I've written a small javafx app that animates a square moving from the top-left corner to bottom-right. It then reverses the animation and runs it continuously. On my pixel 4 (xxhdpi) the square leaves behind a trail of edges on the return trip. This does not happen on on my Nexus 7 2013 (xhdpi) or on my desktop.

Tried both the gluon plugin and also the gluon-vm plugin.

Seems related to screen pixel density . . . how do you prevent the ghosting artifacts on dense screens? Image and code below.

Pixel 4 screenshot:

Nexus 2013 Screenshot:

And the app:

public class StockJavaFx extends Application {
    @Override
    public void start(Stage primaryStage) {
        Dimension2D dimension = Services.get(DisplayService.class)
                .map(DisplayService::getDefaultDimensions)
                .orElseThrow(() -> new NullPointerException("DisplayService"));

        Rectangle rectangle = new Rectangle(75, 75);

        Pane container = new Pane();
        container.getChildren().add(new Rectangle(dimension.getWidth(), dimension.getHeight(), Color.DARKSLATEGRAY));
        container.getChildren().add(rectangle);

        Scene scene = new Scene(container);

        primaryStage.setScene(scene);

        TranslateTransition tt = new TranslateTransition(Duration.millis(750), rectangle);
        tt.setFromX(0);
        tt.setToX(dimension.getWidth() - 75);
        tt.setFromY(0);
        tt.setToY(dimension.getHeight() - 75);
        tt.setCycleCount(Animation.INDEFINITE);
        tt.setAutoReverse(true);

        FillTransition ft = new FillTransition(Duration.millis(750), rectangle);
        ft.setFromValue(Color.ORANGERED);
        ft.setToValue(Color.CADETBLUE);
        ft.setCycleCount(Animation.INDEFINITE);
        ft.setAutoReverse(true);

        tt.play();
        ft.play();

        primaryStage.show();
    }
}

Solution

  • The old Gluon jfxmobile plugin is more or less EOL, and it's being replaced by the new Gluon Client plugin. More details can be found here and here. Detailed documentation can be found here.

    This is how you can try creating an Android app that will solve the "ghosting" issue, with some extra "small" benefits, like using Java and JavaFX 11+, GraalVM, and getting a much more performant app. Note that the client plugin for Android is still under heavy development and it's not ready for production yet.

    Before you get started, please check that you follow the prerequisites here:

    You can modify one of the existing samples, like HelloGluon.

    You can modify the pom to use the latest versions like:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.gluonhq.hello</groupId>
        <artifactId>hellogluon</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>jar</packaging>
    
        <name>hellogluon</name>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.release>11</maven.compiler.release>
            <javafx.version>14.0.1</javafx.version>
            <attach.version>4.0.7</attach.version>
            <client.plugin.version>0.1.22</client.plugin.version>
            <mainClassName>com.gluonhq.hello.HelloGluon</mainClassName>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>${javafx.version}</version>
            </dependency>
            <dependency>
                <groupId>com.gluonhq</groupId>
                <artifactId>charm-glisten</artifactId>
                <version>6.0.4</version>
            </dependency>
            <dependency>
                <groupId>com.gluonhq.attach</groupId>
                <artifactId>display</artifactId>
                <version>${attach.version}</version>
            </dependency>
            <dependency>
                <groupId>com.gluonhq.attach</groupId>
                <artifactId>lifecycle</artifactId>
                <version>${attach.version}</version>
            </dependency>
            <dependency>
                <groupId>com.gluonhq.attach</groupId>
                <artifactId>statusbar</artifactId>
                <version>${attach.version}</version>
            </dependency>
            <dependency>
                <groupId>com.gluonhq.attach</groupId>
                <artifactId>storage</artifactId>
                <version>${attach.version}</version>
            </dependency>
            <dependency>
                <groupId>com.gluonhq.attach</groupId>
                <artifactId>util</artifactId>
                <version>${attach.version}</version>
            </dependency>
        </dependencies>
    
        <repositories>
            <repository>
                <id>Gluon</id>
                <url>https://nexus.gluonhq.com/nexus/content/repositories/releases</url>
            </repository>
        </repositories>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <release>${maven.compiler.release}</release>
                    </configuration>
                </plugin>
    
                <plugin>
                    <groupId>org.openjfx</groupId>
                    <artifactId>javafx-maven-plugin</artifactId>
                    <version>0.0.4</version>
                    <configuration>
                        <mainClass>${mainClassName}</mainClass>
                    </configuration>
                </plugin>
    
                <plugin>
                    <groupId>com.gluonhq</groupId>
                    <artifactId>client-maven-plugin</artifactId>
                    <version>${client.plugin.version}</version>
                    <configuration>
                        <target>${client.target}</target>
                        <attachList>
                            <list>display</list>
                            <list>lifecycle</list>
                            <list>statusbar</list>
                            <list>storage</list>
                        </attachList>
                        <mainClass>${mainClassName}</mainClass>
                    </configuration>
                </plugin>
            </plugins>
    
        <profiles>
            <profile>
                <id>desktop</id>
                <activation>
                    <activeByDefault>true</activeByDefault>
                </activation>
                <properties>
                    <client.target>host</client.target>
                </properties>
                <dependencies>
                    <dependency>
                        <groupId>com.gluonhq.attach</groupId>
                        <artifactId>display</artifactId>
                        <version>${attach.version}</version>
                        <classifier>desktop</classifier>
                        <scope>runtime</scope>
                    </dependency>
                    <dependency>
                        <groupId>com.gluonhq.attach</groupId>
                        <artifactId>lifecycle</artifactId>
                        <version>${attach.version}</version>
                        <classifier>desktop</classifier>
                        <scope>runtime</scope>
                    </dependency>
                    <dependency>
                        <groupId>com.gluonhq.attach</groupId>
                        <artifactId>storage</artifactId>
                        <version>${attach.version}</version>
                        <classifier>desktop</classifier>
                        <scope>runtime</scope>
                    </dependency>
                </dependencies>
            </profile>
            <profile>
                <id>ios</id>
                <properties>
                    <client.target>ios</client.target>
                </properties>
            </profile>
            <profile>
                <id>android</id>
                <properties>
                    <client.target>android</client.target>
                </properties>
            </profile>
        </profiles>
        </build>
    </project>
    

    Now you can modify the main view to add your transition:

    public class HelloGluon extends MobileApplication {
    
        @Override
        public void init() {
            addViewFactory(HOME_VIEW, () -> {
                Rectangle rectangle = new Rectangle(75, 75, Color.DARKSLATEGRAY);
                Pane container = new Pane(rectangle);
                container.setStyle("-fx-background-color: darkslategray");
    
                return new View(container) {
                    @Override
                    protected void updateAppBar(AppBar appBar) {
                        appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
                        appBar.setTitleText("Gluon Mobile");
                        appBar.getActionItems().add(MaterialDesignIcon.PLAY_ARROW.button(e -> {
                            TranslateTransition tt = new TranslateTransition(Duration.millis(750), rectangle);
                            tt.setFromX(0);
                            tt.setToX(getWidth() - 75);
                            tt.setFromY(0);
                            tt.setToY(getHeight() - 75);
                            tt.setCycleCount(Animation.INDEFINITE);
                            tt.setAutoReverse(true);
    
                            FillTransition ft = new FillTransition(Duration.millis(750), rectangle);
                            ft.setFromValue(Color.ORANGERED);
                            ft.setToValue(Color.CADETBLUE);
                            ft.setCycleCount(Animation.INDEFINITE);
                            ft.setAutoReverse(true);
                            tt.play();
                            ft.play();
                        }));
                    }
                };
            });
        }
    
        @Override
        public void postInit(Scene scene) {
            Swatch.TEAL.assignTo(scene);
            scene.getStylesheets().add(HelloGluon.class.getResource("styles.css").toExternalForm());
    
            if (Platform.isDesktop()) {
                Dimension2D dimension2D = DisplayService.create()
                        .map(DisplayService::getDefaultDimensions)
                        .orElse(new Dimension2D(640, 480));
                scene.getWindow().setWidth(dimension2D.getWidth());
                scene.getWindow().setHeight(dimension2D.getHeight());
            }
        }
    
        public static void main(String[] args) {
            launch();
        }
    
    }
    

    You can now run with your regular JDK on your machine:

    mvn clean javafx:run
    

    and verify that works fine.

    If that is the case, you can now create a native image with GraalVM, also on your machine:

    mvn clean client:build
    

    This is a lengthy process, that requires usually 16 GB RAM, and a few minutes.

    Once finished successfully, you can run it:

    mvn client:run
    

    It should work as expected:

    Finally, you can try to build an Android native image:

    mvn -Pandroid client:build
    

    When finished, create the apk:

    mvn -Pandroid client:package
    

    It will create an apk under target/client/aarch64-android/gvm/apk/bin/hellogluon.apk.

    Plug a device, to install and run:

    mvn -Pandroid client:install client:run
    

    Note: by default, icon assets and AndroidManifest are generated at target/client/aarch64-android/gensrc/android. If you want to modify either of them, you have to copy the content of this folder to src/android.