Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for animated gif images. #153

Closed
sureshg opened this issue Nov 30, 2020 · 29 comments
Closed

Support for animated gif images. #153

sureshg opened this issue Nov 30, 2020 · 29 comments
Assignees
Labels
desktop discussion Need further discussion to understand if it actually needed enhancement New feature or request resources

Comments

@sureshg
Copy link

sureshg commented Nov 30, 2020

Trying to display the following animated gif image displays only the first frame because it supports only the Bitmap.

Image(
     bitmap = imageFromResource("lottie-sample.gif"),
     modifier = Modifier.align(Alignment.BottomCenter).preferredSize(100.dp,100.dp)
)

lottie-sample

@olonho
Copy link
Contributor

olonho commented Nov 30, 2020

Not sure if animated GIFs work even on Android. Maybe there's third-party library doing the same?

@jimgoog
Copy link
Collaborator

jimgoog commented Nov 30, 2020

it's worth noting that gifs are hugely bandwidth/latency inefficient (https://rigor.com/blog/optimizing-animated-gifs-with-html5-video/) which becomes a much bigger consideration for mobile devices.

For an animation like this one in particular, it might be worth generating/drawing the bubble explosion directly rather than having a pre-generated resource file (something like https://github.com/adibfara/composeclock). That would allow you to programmatically control things like animation velocity, number of bubbles, etc. It would also allow you to have an animation that could run continuously/forever without jumping, one that visually doesn't ever repeat, etc. Just food for thought.

@Dominaezzz
Copy link
Contributor

Dominaezzz commented Nov 30, 2020

This seems interesting, I'll try and make a sample of this.

@Dominaezzz
Copy link
Contributor

This is a sample of naive GIF rendering with compose.

https://gist.github.com/Dominaezzz/cd51f8821162a149ee2a5fb69a702e7f

(Can PR a sample if maintainers are interested)

@sureshg
Copy link
Author

sureshg commented Dec 1, 2020

@Dominaezzz awesome.. Compiling it on JDK 15 gives the following error even though JDK has the java.xml module

Unresolved reference: NodeList

Should I explicitly add the XML API dependencies for import org.w3c.dom.* ?

@Dominaezzz
Copy link
Contributor

Ha, I guess so, I ran it with JDK 11 and the IntelliJ compose project setup and it just worked.

@sureshg
Copy link
Author

sureshg commented Dec 3, 2020

It's really weird that only fully qualified NodeList name is working with the compose plugin. Here is the Gradle compilation logs

2020-12-03T00:20:05.814-0800 [DEBUG] [org.gradle.api.Task] v: Configuring the compilation environment
2020-12-03T00:20:05.814-0800 [DEBUG] [org.gradle.api.Task] v: Loading modules: [java.se, jdk.accessibility, jdk.attach, jdk.compiler, jdk.dynalink, jdk.httpserver, jdk.incubator.foreign, jdk.jartool, jdk.javadoc, jdk.jconsole, jdk.jdi, jdk.jfr, jdk.jshell, jdk.jsobject, jdk.management, jdk.management.jfr, jdk.net, jdk.nio.mapmode, jdk.sctp, jdk.security.auth, jdk.security.jgss, jdk.unsupported, jdk.unsupported.desktop, jdk.xml.dom, java.base, java.compiler, java.datatransfer, java.desktop, java.xml, java.instrument, java.logging, java.management, java.management.rmi, java.rmi, java.naming, java.net.http, java.prefs, java.scripting, java.security.jgss, java.security.sasl, java.sql, java.transaction.xa, java.sql.rowset, java.xml.crypto, jdk.internal.jvmstat, jdk.management.agent, jdk.jdwp.agent, jdk.internal.ed, jdk.internal.le, jdk.internal.opt]
2020-12-03T00:20:05.814-0800 [ERROR] [org.gradle.api.Task] e: /Users/sgopal1/code/compose-desktop-sample/src/main/kotlin/dev/suresh/gif/AnimatedGif.kt: (221, 21): Unresolved reference: NodeList
2020-12-03T00:20:05.814-0800 [ERROR] [org.gradle.api.Task] e: /Users/sgopal1/code/compose-desktop-sample/src/main/kotlin/dev/suresh/gif/AnimatedGif.kt: (233, 21): Unresolved reference: NodeList
2020-12-03T00:20:05.815-0800 [DEBUG] [sun.rmi.transport.tcp] Execution worker for ':': reuse connection

From the logs, it's clear that JDK has java.xml module in the compilation environment but somehow Kotlin compile step is failing. The issue goes away when we have the fully qualified class name in the code (private fun org.w3c.dom.NodeList.asSequence())

JDK         : 16-loom+9-316
Gradle      : 6.8-rc-1
Kotlin      : 1.4.20
Compose     : 0.3.0-build133

@olonho olonho added enhancement New feature or request discussion Need further discussion to understand if it actually needed labels Dec 8, 2020
@olonho olonho self-assigned this Dec 8, 2020
@igordmn
Copy link
Collaborator

igordmn commented Apr 12, 2021

Example how to show gif using org.jetbrains.skija.Codec:
(see more optimized version in the next comment)

import androidx.compose.desktop.Window
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.unit.dp
import org.jetbrains.skija.Bitmap
import org.jetbrains.skija.Canvas
import org.jetbrains.skija.Codec
import org.jetbrains.skija.Data
import java.net.URL

fun main() = Window {
    val codec = remember {
        val bytes = URL(
            "https://raw.githubusercontent.com/JetBrains/skija/ccf303ebcf926e5ef000fc42d1a6b5b7f1e0b2b5/examples/scenes/images/codecs/animated.gif"
        ).readBytes()
        Codec.makeFromData(Data.makeFromBytes(bytes))
    }

    GifAnimation(codec, Modifier.size(100.dp))
}

@Composable
fun GifAnimation(codec: Codec, modifier: Modifier) {
    val animation = remember(codec) { GifAnimation(codec) }

    LaunchedEffect(animation) {
        while (true) {
            withFrameNanos {
                animation.update(it)
            }
        }
    }

    Canvas(modifier) {
        drawIntoCanvas {
            animation.draw(it.nativeCanvas)
        }
    }
}

private class GifAnimation(private val codec: Codec) {
    private val bitmap = Bitmap().apply {
        allocPixels(codec.imageInfo)
    }
    private val durations = codec.framesInfo.map { it.duration * 1_000_000 }
    private val totalDuration = durations.sum()

    private var startTime = -1L
    private var frame by mutableStateOf(0)

    fun update(nanoTime: Long) {
        if (startTime == -1L) {
            startTime = nanoTime
        }

        frame = frameOf(time = (nanoTime - startTime) % totalDuration)
    }

    // WARNING: it is not optimal
    private fun frameOf(time: Long): Int {
        var t = 0
        for (frame in durations.indices) {
            t += durations[frame]
            if (t >= time) return frame
        }
        error("Unexpected")
    }

    fun draw(canvas: Canvas) {
        codec.readPixels(bitmap, frame)
        canvas.drawBitmap(bitmap, 0f, 0f)
    }
}

@olonho
Copy link
Contributor

olonho commented Apr 19, 2021

A bit optimized version:

import androidx.compose.desktop.Window
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.unit.dp
import org.jetbrains.skija.Bitmap
import org.jetbrains.skija.Canvas
import org.jetbrains.skija.Codec
import org.jetbrains.skija.Data
import java.net.URL
fun main() = Window {
    val codec = remember {
        val bytes = URL(
            "https://raw.githubusercontent.com/JetBrains/skija/ccf303ebcf926e5ef000fc42d1a6b5b7f1e0b2b5/examples/scenes/images/codecs/animated.gif"
        ).readBytes()
        Codec.makeFromData(Data.makeFromBytes(bytes))
    }
    GifAnimation(codec, Modifier.size(100.dp))
}
@Composable
fun GifAnimation(codec: Codec, modifier: Modifier) {
    val animation = remember(codec) { GifAnimation(codec) }
    LaunchedEffect(animation) {
        while (true) {
            withFrameNanos {
                animation.update(it)
            }
        }
    }
    Canvas(modifier) {
        drawIntoCanvas {
            animation.draw(it.nativeCanvas)
        }
    }
}
private class GifAnimation(private val codec: Codec) {
    private val bitmap = Bitmap().apply {
        allocPixels(codec.imageInfo)
    }
    private val durations = codec.framesInfo.map { it.duration * 1_000_000 }
    private val totalDuration = durations.sum()
    private var startTime = -1L
    private var lastFrame = 0
    private var lastDuration = 0L
    private var frame by mutableStateOf(0)
    fun update(nanoTime: Long) {
        if (startTime == -1L) {
            startTime = nanoTime
        }
        frame = frameOf(time = (nanoTime - startTime) % totalDuration)
    }
    private fun frameOf(time: Long): Int {
        var t = lastDuration
        for (frame in lastFrame until durations.size) {
            if (t >= time) {
                lastFrame = frame
                lastDuration = t
                return frame
            }
            t += durations[frame]
        }
        lastFrame = 0
        lastDuration = 0L
        return 0
    }
    fun draw(canvas: Canvas) {
        codec.readPixels(bitmap, frame)
        canvas.drawBitmap(bitmap, 0f, 0f)
    }
}

@samuelprince77
Copy link

@olonho This approach seems to fail on most lottie generated gifs. Most either play a single frame or the first few frames before stopping, not really sure why. An example gif Brain gif. The approach posted by Dominaezzz seems to work well though.

@sureshg
Copy link
Author

sureshg commented May 18, 2021

@samuelprince77 the issue is because of Int overflow happening in this line

private val durations = codec.framesInfo.map { it.duration * 1_000_000 }
private val totalDuration = durations.sum() // int overflow here

The return type of duration is List<Int>, so that the totalDuration will become int. Changing it to 1_000_000L will fix the issue.

@sureshg
Copy link
Author

sureshg commented May 18, 2021

By the way, canvas.drawBitmap(bitmap, 0f, 0f) has removed in the latest release (0.4.0-build198). The modified draw() function is,

 fun draw(canvas: Canvas) {
    codec.readPixels(bitMap, currFrame)
    canvas.drawImage(Image.makeFromBitmap(bitMap), 0f, 0f)
 }

@whitescent
Copy link
Contributor

whitescent commented May 23, 2021

It would be great if the Coil and Glide in accompanist could be used in Compose-Jb. Anyone to test it?

@KotlinGeekDev
Copy link

Hello @nthily . I think that Accompanist is Android-only. I don't there is desktop(or multiplatform) support yet.

@Dominaezzz
Copy link
Contributor

Slightly shorter.

import androidx.compose.animation.core.*
import androidx.compose.desktop.Window
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.dp
import org.jetbrains.skija.*
import java.net.URL

fun main() = Window {
    val codec = remember {
        val bytes = URL(
            "https://raw.githubusercontent.com/JetBrains/skija/ccf303ebcf926e5ef000fc42d1a6b5b7f1e0b2b5/examples/scenes/images/codecs/animated.gif"
        ).readBytes()
        Codec.makeFromData(Data.makeFromBytes(bytes))
    }
    GifAnimation(codec, Modifier.size(100.dp))
}

@Composable
fun GifAnimation(codec: Codec, modifier: Modifier) {
    val transition = rememberInfiniteTransition()
    val frameIndex by transition.animateValue(
        initialValue = 0,
        targetValue = codec.frameCount - 1,
        Int.VectorConverter,
        animationSpec = infiniteRepeatable(
            animation = keyframes {
                durationMillis = 0
                for ((index, frame) in codec.framesInfo.withIndex()) {
                    index at durationMillis
                    durationMillis += frame.duration
                }
            }
        )
    )

    val bitmap = remember { Bitmap().apply { allocPixels(codec.imageInfo) } }
    Canvas(modifier) {
        codec.readPixels(bitmap, frameIndex)
        drawImage(bitmap.asImageBitmap())
    }
}

@Dominaezzz
Copy link
Contributor

Can this issue be closed now?

@igordmn
Copy link
Collaborator

igordmn commented Jun 20, 2021

We'll probably publish this as part of https://github.com/JetBrains/compose-jb/tree/master/components

@Dominaezzz
Copy link
Contributor

Oooh, I'll make a PR then. (I might ping you on slack for help)

@igordmn
Copy link
Collaborator

igordmn commented Jun 20, 2021

Oooh, I'll make a PR then.

Awesome! Thanks :)

@igordmn
Copy link
Collaborator

igordmn commented Aug 13, 2021

#802 is closed, as there is no activity. We still plan to make it as a separate component in https://github.com/JetBrains/compose-jb/tree/master/components, when we find time to do it.

@YeungKC
Copy link

YeungKC commented Aug 13, 2021

#802 is closed, as there is no activity. We still plan to make it as a separate component in https://github.com/JetBrains/compose-jb/tree/master/components, when we find time to do it.

Why gif support is a separate component? Shouldn't the image itself support Gif.

@igordmn
Copy link
Collaborator

igordmn commented Aug 13, 2021

Why gif support is a separate component? Shouldn't the image itself support Gif.

Maybe, but it requires a lot more effort to make it Compose default. We need to make it to fit the core Compose architecture, discuss political questions, etc.

components is for the things which doesn't belong to the core Compose, or doesn't yet belong to the core Compose, without proper refactoring.

I don't have an answer yet, why Image itself shouldn't support Gif's, maybe during the development we decide to do it.

One of the things that stops me now - it is how animatedVectorResource was designed in Compose for Android. Here we retrieve each frame separately, Image itself don't do it. Maybe it is by-design that Image doesn't support animations, as it adds a functionality which doesn't belong here. The reason maybe the same, why Image won't support videos by default, or any other arbitrary source of frames.

@bassstorm
Copy link

Quite some time passed since Aug 2021... @igordmn may I ask for an update, please?

@igordmn
Copy link
Collaborator

igordmn commented Feb 20, 2022

There is no update.

Feel free to make a PR, adding a component to ”components” folder.

@igordmn
Copy link
Collaborator

igordmn commented Apr 29, 2022

@JetpackDuba implemented this component here. Thanks!

The component will be available in the next 1.2.0-build:

@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.animatedImage)

@igordmn igordmn closed this as completed Apr 29, 2022
@goldenduo
Copy link

Compile Error: Unresolved reference: Unresolved reference: skija

@BehnamMaboodi
Copy link

What about ios?

@phillwiggins
Copy link

Was this originally intended to be just for desktop? It would be great to have support across Android & iOS as well.

@okushnikov
Copy link

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
desktop discussion Need further discussion to understand if it actually needed enhancement New feature or request resources
Projects
None yet
Development

No branches or pull requests