Closed sureshg closed 2 years ago
Not sure if animated GIFs work even on Android. Maybe there's third-party library doing the same?
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.
This seems interesting, I'll try and make a sample of this.
This is a sample of naive GIF rendering with compose.
https://gist.github.com/Dominaezzz/cd51f8821162a149ee2a5fb69a702e7f
(Can PR a sample if maintainers are interested)
@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.*
?
Ha, I guess so, I ran it with JDK 11 and the IntelliJ compose project setup and it just worked.
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
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)
}
}
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)
}
}
@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.
@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.
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)
}
It would be great if the Coil
and Glide
in accompanist could be used in Compose-Jb
. Anyone to test it?
Hello @Nthily . I think that Accompanist is Android-only. I don't there is desktop(or multiplatform) support yet.
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())
}
}
Can this issue be closed now?
We'll probably publish this as part of https://github.com/JetBrains/compose-jb/tree/master/components
Oooh, I'll make a PR then. (I might ping you on slack for help)
Oooh, I'll make a PR then.
Awesome! Thanks :)
https://github.com/JetBrains/compose-jb/pull/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.
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.
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.
Quite some time passed since Aug 2021... @igordmn may I ask for an update, please?
There is no update.
Feel free to make a PR, adding a component to ”components” folder.
@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)
Compile Error: Unresolved reference: Unresolved reference: skija
What about ios?
Was this originally intended to be just for desktop? It would be great to have support across Android & iOS as well.
Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.
Trying to display the following animated gif image displays only the first frame because it supports only the Bitmap.