Skip to content

Commit

Permalink
Android Out of Band Assets
Browse files Browse the repository at this point in the history
Adding OOB Asset Loaders for our RiveAnimationView:
- Add a Loader parameter to the import flow, a FileAssetLoader object in Kotlin & JNI
- Supports creating custom `FileAssetLoader`s, `CDNAssetLoader` and `FallbackAssetLoader` like in our other runtimes
- Adds a `RiveAnimationView.Builder` that uses the [Builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) for easier instantiation and configuration of `RiveAnimationView`s programmatically
- Adds a bunch of examples and tests for these new scenarios and functionalities
- Bumps all dependencies to comply with the latest SDK requirements

Diffs=
8c99c8dcf Android Out of Band Assets (#6019)

Co-authored-by: Umberto Sonnino <[email protected]>
  • Loading branch information
umberto-sonnino and umberto-sonnino committed Oct 16, 2023
1 parent 6d92474 commit 8776210
Show file tree
Hide file tree
Showing 69 changed files with 1,486 additions and 126 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6a74a01f27fbdabe03827a72614e24f8eff018a0
8c99c8dcf67c5bf80bba1ca953d3e3dfa591e6d7
2 changes: 1 addition & 1 deletion .rive_renderer
Original file line number Diff line number Diff line change
@@ -1 +1 @@
56232dbd3f8cfd22d9304467427561e2cafd035c
6a47e8989777488f8bc159c4312a1c92e1308733
17 changes: 10 additions & 7 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
compileSdk 33
compileSdk 34

defaultConfig {
applicationId "app.rive.runtime.example"
Expand Down Expand Up @@ -67,24 +67,27 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
debugImplementation project(path: ':kotlin')
releaseImplementation project(path: ':kotlin')
previewImplementation 'app.rive:rive-android:+'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'com.google.android.material:material:1.10.0'
implementation 'androidx.activity:activity-compose:1.8.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.compose.material3:material3:1.2.0-alpha01'
implementation 'androidx.compose.material3:material3:1.2.0-alpha09'
implementation 'androidx.startup:startup-runtime:1.1.1'
implementation 'com.android.volley:volley:1.2.1'
implementation "org.jetbrains.kotlin:kotlin-reflect:1.8.22"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.0"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.3"
androidTestImplementation project(path: ':kotlin')
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
Expand Down
263 changes: 263 additions & 0 deletions app/src/androidTest/java/app/rive/runtime/example/RiveBuilderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
package app.rive.runtime.example

import TestUtils.Companion.waitUntil
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import app.rive.runtime.kotlin.RiveAnimationView
import app.rive.runtime.kotlin.controllers.RiveFileController
import app.rive.runtime.kotlin.core.Alignment
import app.rive.runtime.kotlin.core.CDNAssetLoader
import app.rive.runtime.kotlin.core.FallbackAssetLoader
import app.rive.runtime.kotlin.core.File
import app.rive.runtime.kotlin.core.FileAsset
import app.rive.runtime.kotlin.core.FileAssetLoader
import app.rive.runtime.kotlin.core.Fit
import app.rive.runtime.kotlin.core.Loop
import app.rive.runtime.kotlin.core.RendererType
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.time.Duration.Companion.milliseconds


@RunWith(AndroidJUnit4::class)
class RiveBuilderTest {
@Test
fun withIdResource() {
val activityScenario = ActivityScenario.launch(EmptyActivity::class.java)
lateinit var riveView: RiveAnimationView
lateinit var controller: RiveFileController
activityScenario.onActivity {
riveView = RiveAnimationView.Builder(it)
.setResource(R.raw.off_road_car_blog)
.build()
it.container.addView(riveView)
controller = riveView.controller
assertTrue(controller.isActive)
assertEquals("New Artboard", controller.activeArtboard?.name)
assertEquals(
listOf("idle"),
controller.playingAnimations.toList().map { anim -> anim.name })

}
activityScenario.close()
// Background thread deallocates asynchronously.
waitUntil(1500.milliseconds) { controller.refCount == 0 }
assertFalse(controller.isActive)
assertNull(controller.file)
assertNull(controller.activeArtboard)
// Asset loader was deallocated.
assert(riveView.rendererAttributes.assetLoader?.hasCppObject == false)
}

@Test
fun withFileResource() {
val activityScenario = ActivityScenario.launch(EmptyActivity::class.java)
lateinit var riveView: RiveAnimationView
lateinit var controller: RiveFileController
activityScenario.onActivity { activity ->
val file = activity
.resources
.openRawResource(R.raw.basketball)
.use { res -> File(res.readBytes()) }

riveView = RiveAnimationView.Builder(activity)
.setResource(file)
.build()
activity.container.addView(riveView)
controller = riveView.controller
assertTrue(controller.isActive)
assertEquals("New Artboard", controller.activeArtboard?.name)
assertEquals(
listOf("idle"),
controller.playingAnimations.toList().map { anim -> anim.name })
}
activityScenario.close()
// Background thread deallocates asynchronously.
waitUntil(1500.milliseconds) { controller.refCount == 0 }
assertFalse(controller.isActive)
assertNull(controller.file)
assertNull(controller.activeArtboard)
// Asset loader was deallocated.
assert(riveView.rendererAttributes.assetLoader?.hasCppObject == false)
}

@Test
fun withBytesResource() {
val activityScenario = ActivityScenario.launch(EmptyActivity::class.java)
lateinit var riveView: RiveAnimationView
lateinit var controller: RiveFileController
activityScenario.onActivity { activity ->
val file = activity
.resources
.openRawResource(R.raw.basketball)
.use { res -> res.readBytes() }
riveView = RiveAnimationView.Builder(activity)
.setResource(file)
.build()
activity.container.addView(riveView)
controller = riveView.controller
assertTrue(controller.isActive)
assertEquals("New Artboard", controller.activeArtboard?.name)
assertEquals(
listOf("idle"),
controller.playingAnimations.toList().map { anim -> anim.name })
}
activityScenario.close()
// Background thread deallocates asynchronously.
waitUntil(1500.milliseconds) { controller.refCount == 0 }
assertFalse(controller.isActive)
assertNull(controller.file)
assertNull(controller.activeArtboard)
// Asset loader was deallocated.
assert(riveView.rendererAttributes.assetLoader?.hasCppObject == false)
}

@Test
fun manyParameters() {
val activityScenario = ActivityScenario.launch(EmptyActivity::class.java)
lateinit var riveView: RiveAnimationView
lateinit var controller: RiveFileController
activityScenario.onActivity { activity ->
riveView = RiveAnimationView.Builder(activity)
.setAlignment(Alignment.BOTTOM_CENTER)
.setFit(Fit.FIT_HEIGHT)
.setLoop(Loop.PINGPONG)
.setAutoplay(false)
.setTraceAnimations(true)
.setArtboardName("artboard2")
.setAnimationName("artboard2animation1")
.setResource(R.raw.multipleartboards)
.build()
activity.container.addView(riveView)
controller = riveView.controller
val renderer = riveView.artboardRenderer
assertTrue(controller.isActive)
assertFalse(controller.autoplay)
assertEquals(Alignment.BOTTOM_CENTER, renderer?.alignment)
assertEquals(Fit.FIT_HEIGHT, renderer?.fit)
assertEquals(Loop.PINGPONG, renderer?.loop)
assertTrue(riveView.artboardRenderer!!.trace)
assertEquals("artboard2", controller.activeArtboard?.name)
assertEquals(
emptyList<String>(), // autoplay = false
controller.playingAnimations.toList().map { anim -> anim.name })
}
activityScenario.close()
// Background thread deallocates asynchronously.
waitUntil(1500.milliseconds) { controller.refCount == 0 }
assertFalse(controller.isActive)
assertNull(controller.file)
assertNull(controller.activeArtboard)
// Asset loader was deallocated.
assert(riveView.rendererAttributes.assetLoader?.hasCppObject == false)
}

@Test
fun assetLoader() {
val activityScenario = ActivityScenario.launch(EmptyActivity::class.java)
lateinit var riveView: RiveAnimationView
lateinit var controller: RiveFileController
val assetStore = mutableListOf<FileAsset>()
val customLoader = object : FileAssetLoader() {
override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean {
return assetStore.add(asset)
}
}

activityScenario.onActivity { activity ->
riveView = RiveAnimationView.Builder(activity)
.setResource(R.raw.walle)
.setAssetLoader(customLoader)
.build()
activity.container.addView(riveView)
controller = riveView.controller
val actualLoader = riveView.rendererAttributes.assetLoader
assert(actualLoader is FallbackAssetLoader)
val fallbackLoader = actualLoader as FallbackAssetLoader
assertEquals(2, fallbackLoader.loaders.size)
assertEquals(customLoader as FileAssetLoader, fallbackLoader.loaders.first())
assert(fallbackLoader.loaders.last() is CDNAssetLoader)
assertEquals(2, assetStore.size)
}
activityScenario.close()
// Background thread deallocates asynchronously.
waitUntil(1500.milliseconds) { controller.refCount == 0 }
assertFalse(controller.isActive)
assertNull(controller.file)
assertNull(controller.activeArtboard)
assertFalse(customLoader.hasCppObject)
}

@Test
fun noCDNLoader() {
val activityScenario = ActivityScenario.launch(EmptyActivity::class.java)
lateinit var riveView: RiveAnimationView
lateinit var controller: RiveFileController
activityScenario.onActivity { activity ->
riveView = RiveAnimationView.Builder(activity)
.setResource(R.raw.walle)
.setShouldLoadCDNAssets(false)
.build()
activity.container.addView(riveView)
controller = riveView.controller

val actualLoader = riveView.rendererAttributes.assetLoader
assert(actualLoader is FallbackAssetLoader)
val fallbackLoader = actualLoader as FallbackAssetLoader
assertTrue(fallbackLoader.loaders.isEmpty())
}
activityScenario.close()
// Background thread deallocates asynchronously.
waitUntil(1500.milliseconds) { controller.refCount == 0 }
assertFalse(controller.isActive)
assertNull(controller.file)
assertNull(controller.activeArtboard)
}

@Test
fun withRendererType() {
val activityScenario = ActivityScenario.launch(EmptyActivity::class.java)
lateinit var riveView: RiveAnimationView
lateinit var controller: RiveFileController
activityScenario.onActivity { activity ->
riveView = RiveAnimationView.Builder(activity)
.setResource(R.raw.basketball)
.setRendererType(RendererType.Rive)
.build()
activity.container.addView(riveView)
controller = riveView.controller
assertNotNull(riveView.artboardRenderer)
assertEquals(RendererType.Rive, riveView.artboardRenderer?.type)
}
activityScenario.close()
// Background thread deallocates asynchronously.
waitUntil(1500.milliseconds) { controller.refCount == 0 }
assertFalse(controller.isActive)
assertNull(controller.file)
assertNull(controller.activeArtboard)
}

@Test
fun withStateMachineName() {
val activityScenario = ActivityScenario.launch(EmptyActivity::class.java)
lateinit var riveView: RiveAnimationView
lateinit var controller: RiveFileController
activityScenario.onActivity { activity ->
riveView = RiveAnimationView.Builder(activity)
.setResource(R.raw.what_a_state)
.setStateMachineName("State Machine 2")
.build()
activity.container.addView(riveView)
controller = riveView.controller
assertEquals(1, controller.playingStateMachines.size)
assertEquals("State Machine 2", controller.playingStateMachines.first().name)
}
activityScenario.close()
// Background thread deallocates asynchronously.
waitUntil(1500.milliseconds) { controller.refCount == 0 }
assertFalse(controller.isActive)
assertNull(controller.file)
assertNull(controller.activeArtboard)
}
}
5 changes: 4 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@
android:theme="@style/AppTheme" />
<activity android:name=".FrameActivity" />
<activity android:name=".DynamicTextActivity" />
<activity android:name=".AssetLoaderActivity" />
<activity android:name=".StressTestActivity" />
<activity android:name=".SingleActivity" /> <!-- For testing -->
<!-- For testing 👇 -->
<activity android:name=".SingleActivity" />
<activity android:name=".EmptyActivity" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,26 @@ import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.widget.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.RadioButton
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatButton
import androidx.appcompat.widget.AppCompatCheckBox
import androidx.appcompat.widget.AppCompatEditText
import app.rive.runtime.kotlin.RiveAnimationView
import app.rive.runtime.kotlin.controllers.RiveFileController
import app.rive.runtime.kotlin.core.*
import app.rive.runtime.kotlin.core.Artboard
import app.rive.runtime.kotlin.core.Direction
import app.rive.runtime.kotlin.core.LinearAnimationInstance
import app.rive.runtime.kotlin.core.Loop
import app.rive.runtime.kotlin.core.PlayableInstance
import app.rive.runtime.kotlin.core.SMIBoolean
import app.rive.runtime.kotlin.core.SMINumber
import app.rive.runtime.kotlin.core.StateMachineInstance

class AndroidPlayerActivity : AppCompatActivity() {
var loop: Loop = Loop.AUTO
Expand Down Expand Up @@ -74,10 +86,13 @@ class AndroidPlayerActivity : AppCompatActivity() {
when (view.getId()) {
R.id.loop_auto ->
loop = Loop.AUTO

R.id.loop_loop ->
loop = Loop.LOOP

R.id.loop_oneshot ->
loop = Loop.ONESHOT

R.id.loop_pingpong ->
loop = Loop.PINGPONG
}
Expand All @@ -90,8 +105,10 @@ class AndroidPlayerActivity : AppCompatActivity() {
when (view.getId()) {
R.id.direction_auto ->
direction = Direction.AUTO

R.id.direction_backwards ->
direction = Direction.BACKWARDS

R.id.direction_forwards ->
direction = Direction.FORWARDS
}
Expand Down
Loading

0 comments on commit 8776210

Please sign in to comment.