compose-multiplatform icon indicating copy to clipboard operation
compose-multiplatform copied to clipboard

Document how to show OS native splash screen

Open mahozad opened this issue 1 year ago • 10 comments

Final result

https://github.com/JetBrains/compose-multiplatform/assets/29678011/e5120290-ec79-43ab-a684-4d3ff244d52e

Problem

Compose (Java) applications usually take a little time to start. I did not find any guide about how to show a native splash screen (not an "artificial" app splash screen) before the app is ready, so wanted to mention it here. Feel free to close the issue.

Solution

It seems Java supports showing operating system native splash screen while the app is being started: https://docs.oracle.com/javase/tutorial/uiswing/misc/splashscreen.html I only tested it on Windows.

So, in the build script I added this:

compose.desktop {
    application {
        nativeDistributions {
            appResourcesRootDir = (rootDir.toPath() / "assets").toFile()
            // ...
        }
        // The image path is relative to the app (Java) working directory
        // which is the app installation root directory (where the .exe file resides)
        jvmArgs += "-splash:app/resources/splash-screen.gif"
        // ...
    }
}

It uses the splash-screen.gif image placed in <PROJECT_DIR>/assets/common/ directory. See Adding files to packaged applications for more information.

The splash screen works when the app is launched with its installed exe or one of run*Distributable tasks. To also set the splash for application fat/uber jar (created with one of package*UberJar* tasks), copy the splash image into classpath (like src/main/resources directory) and update all the Jar tasks in the build file like below:

tasks.withType<org.gradle.jvm.tasks.Jar> {
    manifest {
        attributes["SplashScreen-Image"] = "image.gif"
    }
}

Sidenote

Probably related issue: https://github.com/JetBrains/compose-multiplatform/issues/200

It seems that Windows does not respect the GIF speed and no-loop settings and plays the animation at a low frame rate. See https://stackoverflow.com/q/25382400

mahozad avatar Jun 05 '23 07:06 mahozad

In my project for a Windows Desktop app I got the splash screen working by adding the following, based on the above:

project-root
│
├── common
│   ├── src
│   │   ├── commonMain
│   └── build.gradle.kts
│
├── desktop
│   ├── assets
│   │   ├── common
│   │   │   ├── loading.gif  // added gif here
│   ├── src
│   │   ├── jvmMain
│   │   │   ├── kotlin
│   │   └── ...
│   └── build.gradle.kts
├── build.gradle.kts
│ ...

In my desktop/build.gradle.kts I added:

compose.desktop {
   application {
      mainClass = "MainKt"
      nativeDistributions {
         appResourcesRootDir.set(project.layout.projectDirectory.dir("assets")
         jvmArgs += "-splash:app/resources/loading.gif"
         // etc.
      }
   }
}

I agree that it would be helpful to get this added to the documentation!

dev-jesser avatar Nov 26 '23 19:11 dev-jesser

To make it work whatever is the working directory when invoking the app I had to use:

jvmArgs += "-splash:${'$'}APPDIR/resources/splash.png"

eskatos avatar Jan 21 '24 11:01 eskatos

@eskatos

jvmArgs += "-splash:${'$'}APPDIR/resources/splash.png"

this doesn't work for me. where (or when) is $APPDIR set?

zoff99 avatar Jan 29 '24 18:01 zoff99

is there any solution to show the splash screen on all platforms when started from an installed application (msi, exe, dmg, deb, rpm) ? none of the above seem to work

zoff99 avatar Feb 02 '24 13:02 zoff99

I think using a simple (undecorated) frame works. JVM itself doesn't take long. Even on my Virtual Windows (which takes 1-2 seconds to load the calculator app), the splash screen appears in less than a second.

Put the splash screen and icon in the resources directory common file as per the docs on adding files.

package com.ustadmobile.port.desktop

import java.awt.Dimension
import java.awt.Frame
import java.awt.Graphics
import java.io.File
import javax.imageio.ImageIO

/**
 * Display a splash screen. We can't use the SplashScreen API because conveyor uses its own loader.
 */
class SplashScreen: Frame() {

    private val splashDir = File(System.getProperty("compose.application.resources.dir"), "splash")

    private val iconDir = File(System.getProperty("compose.application.resources.dir"), "icon")

    private val splashImage = ImageIO.read(File(splashDir, "splash.png"))

    private val iconImage = ImageIO.read(File(iconDir, "icon-512.png"))

    init {
        preferredSize = Dimension(splashImage.width, splashImage.height)
        isUndecorated = true
        pack()
        setLocationRelativeTo(null)
        setIconImage(iconImage)
        isVisible = true
    }

    override fun paint(g: Graphics) {
        super.paint(g)
        g.drawImage(splashImage, 0, 0, this)
    }

    fun close() {
        isVisible = false
    }

}

Then in your application

fun main() {
   var splashScreen: SplashScreen? = SplashScreen()
   
   application {
       ...
       //When ready (e.g. inside Window composable)
       LaunchedEffect(Unit)  {
          splashScreen?.close()
          splashScreen = null
       }
   }

mikedawson avatar Feb 02 '24 22:02 mikedawson

@zoff99 APPDIR environment variable is set by the jpackage launcher. The splash screen will only show up when running the application distributable, not when running using the run task or from a uber-jar.

To be more complete here's my Gradle setup:

compose.desktop {
    application {
        nativeDistributions {
            appResourcesRootDir = layout.projectDirectory.dir("src/desktopMain/assets")
            jvmArgs += "-splash:${'$'}APPDIR/resources/splash.png"
        }
    }
}

And my splash screen file is stored at src/desktopMain/assets/common/splash.png.

When building the distributable the splash image ends up in build/compose/binaries/main/app/<your-app-name>/lib/app/resources/splash.png

eskatos avatar Feb 05 '24 11:02 eskatos

hm maybe it's because my resourceroot dir ist set differently, and my directory structure is different

https://github.com/Zoxcore/trifa_material/blob/9054bdba7232bda66c8a9ad43c1ab6a1a9ce1fdd/build.gradle.kts#L118

but even then it does not work for me. i tried many combinations, also this:

https://github.com/Zoxcore/trifa_material/blob/master/build.gradle.kts#L156

what does "running the application distributable" mean exactly? does it mean install .deb and run? if so, thats not working. can i see (or log) at runtime what value is set for $APPDIR?

zoff99 avatar Feb 05 '24 12:02 zoff99

https://bugs.openjdk.org/browse/JDK-8231910 They are only expanded when used in --java-options, not when used in --arguments how do i add java options to the build.gradle.kts file?

zoff99 avatar Feb 05 '24 18:02 zoff99

@zof99 There is documentation available on appResourcesRootDir. To debug your case I'd suggest trying the following:

Included resources can be accessed via compose.application.resources.dir system property:

import java.io.File

val resourcesDir = File(System.getProperty("compose.application.resources.dir"))

fun main() {
    println(resourcesDir.resolve("resource.txt").readText())
}

You could probably figure out which directory is being set and what it's contents are using the above.

I didn't figure out how to get my splash gif to show up when just using compose desktop run in development mode, but with my above layout it does show up when I have installed the app with in an exe or msi build and start it up.

dev-jesser avatar Feb 05 '24 18:02 dev-jesser

its a horror. why is something like this not properly working out of the box in 2024?

zoff99 avatar Feb 05 '24 19:02 zoff99