Rive + Jetpack Compose (Android)

339 Views Asked by At

For those new to Rive, you probably know that Rive doesn't have support for Composables yet. Well, they don't have an official way of doing it as of today, but I found a way to do it.

Here is what I came up with, hope it helps:

/**
 * RiveComposable it's a custom composable made to show RiveAnimationView objects (xml)
 * in Compose. As of today, Dec 6th, 2023, Rive hasn't finish creating a composable that
 * would work with their XML objects; hence this composable.
 *
 * Rive is still working on fixing issues with the Preview functionality of Jetpack Compose.
 * Until that is fixed, I added [LocalInspectionMode] to replace the real animations with
 * an image of Rive's logo.
 * 
 * By LeviD Dec 2023
 */
@Composable
fun RiveAnimation(
    modifier: Modifier = Modifier,
    @RawRes resId: Int,
    autoplay: Boolean = true,
    artboardName: String? = null,
    animationName: String? = null,
    stateMachineName: String? = null,
    fit: Fit = Fit.CONTAIN,
    alignment: Alignment = Alignment.CENTER,
    loop: Loop = Loop.AUTO,
    contentDescription: String? ,
    updateFunction: (RiveAnimationView) -> Unit = { _ -> }
) {
    if (LocalInspectionMode.current) { // For Developing only, see documentation above for details.
        Image(
            modifier = modifier.size(100.dp)),
            painter = painterResource(id = R.drawable.rive_logo), // Use your own image here (.png) if you want.
            contentDescription = contentDescription
        )
    } else {
        val semantics = if (contentDescription != null) {
            Modifier.semantics {
                this.contentDescription = contentDescription
                this.role = Role.Image
            }
        } else {
            Modifier
        }

        AndroidView(
            modifier = modifier
                .then(semantics)
                .clipToBounds(),
            factory = { context ->
                RiveAnimationView(context).apply {
                    setRiveResource(
                        resId,
                        artboardName,
                        animationName,
                        stateMachineName,
                        autoplay,
                        fit,
                        alignment,
                        loop
                    )
                }
            },
            update = { updateFunction.invoke(it) }
        )
    }
}

How to use it? Here is an example of how to use it with a listener.

RiveAnimation(
    resId = R.raw.animation_file, // Your rive animation as it is in the raw folder
    autoplay = true,
    animationName = "Timeline 1",
    contentDescription = "Some content Description"
) {
    val listener = object : RiveFileController.Listener {
        override fun notifyLoop(animation: PlayableInstance) {
            //Nothing to listen to
        }

        override fun notifyPause(animation: PlayableInstance) {
            //Nothing to listen to
        }

        override fun notifyPlay(animation: PlayableInstance) {
            //Nothing to listen to
        }

        override fun notifyStateChanged(
            stateMachineName: String,
            stateName: String
        ) {
            //Nothing to listen to
        }

        override fun notifyStop(animation: PlayableInstance) {
            enableButton = true // enableButton is a mutable variable in my composable
        }
    }
    it.registerListener(listener)
}

This piece of code will listen until the animation (Timeline 1) is over and then it will set the value of enableButton to true which in my code, it will enable my button "next".

Let me know if you have any questions, hope it helps.

I have tried many ways to implement Rive in Compose without breaking the Preview functionality and this is what works best for me and we use it in the company.

1

There are 1 best solutions below

0
On

Here is an updated version of the previous code. This should handle listeners in a way that it get's them disposed as well. So no memory leaks. ;) If no callback is passed to the function, no listener is created.

Hope it helps.

 @Suppress("LongMethod")
    @Composable
    fun RiveAnimation(
        modifier: Modifier = Modifier,
        @RawRes resId: Int,
        autoplay: Boolean = true,
        artboardName: String? = null,
        animationName: String? = null,
        stateMachineName: String? = null,
        fit: Fit = Fit.CONTAIN,
        alignment: Alignment = Alignment.CENTER,
        loop: Loop = Loop.AUTO,
        contentDescription: String?,
        notifyLoop: ((PlayableInstance) -> Unit)? = null,
        notifyPause: ((PlayableInstance) -> Unit)? = null,
        notifyPlay: ((PlayableInstance) -> Unit)? = null,
        notifyStateChanged: ((String, String) -> Unit)? = null,
        notifyStop: ((PlayableInstance) -> Unit)? = null,
        update: (RiveAnimationView) -> Unit = { _ -> }
    ) {
    
        var riveAnimationView: RiveAnimationView? = null
        var listener: RiveFileController.Listener? = null
        val lifecycleOwner = LocalLifecycleOwner.current
    
        if (LocalInspectionMode.current) { // For Developing only, 
            Image(
                modifier = modifier.size(100.dp),
                painter = painterResource(id = R.drawable.rive_logo), //any image
                contentDescription = contentDescription
            )
        } else {
            val semantics = if (contentDescription != null) {
                Modifier.semantics {
                    this.contentDescription = contentDescription
                    this.role = Role.Image
                }
            } else {
                Modifier
            }
            listener = object : RiveFileController.Listener {
                override fun notifyLoop(animation: PlayableInstance) {
                    notifyLoop?.invoke(animation)
                }
    
                override fun notifyPause(animation: PlayableInstance) {
                    notifyPause?.invoke(animation)
                }
    
                override fun notifyPlay(animation: PlayableInstance) {
                    notifyPlay?.invoke(animation)
                }
    
                override fun notifyStateChanged(
                    stateMachineName: String,
                    stateName: String
                ) {
                    notifyStateChanged?.invoke(stateMachineName, stateName)
                }
    
                override fun notifyStop(animation: PlayableInstance) {
                    notifyStop?.invoke(animation)
                }
            }.takeIf {
                (notifyLoop != null) || (notifyPause != null) ||
                        (notifyPlay != null) || (notifyStateChanged != null) ||
                        (notifyStop != null)
            }
    
            AndroidView(
                modifier = modifier
                    .then(semantics)
                    .clipToBounds(),
                factory = { context ->
                    riveAnimationView = RiveAnimationView(context).apply {
                        setRiveResource(
                            resId,
                            artboardName,
                            animationName,
                            stateMachineName,
                            autoplay,
                            fit,
                            alignment,
                            loop
                        )
                    }
                    listener?.let {
                        riveAnimationView?.registerListener(it)
                    }
                    riveAnimationView!!
                },
                update = {
                    update.invoke(it)
                }
            )
    
            DisposableEffect(lifecycleOwner) {
                onDispose {
                    listener?.let {
                        riveAnimationView?.unregisterListener(it)
                    }
                }
            }
        }
    }
    
    @Composable
    @Preview(showSystemUi = true)
    fun RiveComposablePreview() {
        RiveAnimation(
            resId = R.raw.testrive,
            autoplay = true,
            animationName = "Bouncing",
            contentDescription = "Just a Rive Animation"
        )
    }