From 8776210a4e4920f9ee1945e66c8a78d0f0b4f43c Mon Sep 17 00:00:00 2001 From: umberto-sonnino Date: Mon, 16 Oct 2023 15:38:25 +0000 Subject: [PATCH] Android Out of Band Assets 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 --- .rive_head | 2 +- .rive_renderer | 2 +- app/build.gradle | 17 +- .../rive/runtime/example/RiveBuilderTest.kt | 263 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 5 +- .../runtime/example/AndroidPlayerActivity.kt | 21 +- .../runtime/example/AssetLoaderActivity.kt | 101 +++++++ .../runtime/example/AssetLoaderFragment.kt | 197 +++++++++++++ .../rive/runtime/example/EventsActivity.kt | 31 ++- .../app/rive/runtime/example/HttpActivity.kt | 2 +- .../example/InteractiveSamplesActivity.kt | 2 +- .../rive/runtime/example/LowLevelActivity.kt | 2 +- .../app/rive/runtime/example/MainActivity.kt | 2 +- .../rive/runtime/example/RecyclerActivity.kt | 1 + .../app/rive/runtime/example/RiveFragment.kt | 2 +- .../rive/runtime/example/SingleActivity.kt | 17 -- .../runtime/example/StressTestActivity.kt | 32 ++- .../app/rive/runtime/example/TestActivity.kt | 39 +++ .../rive/runtime/example/ViewPagerActivity.kt | 4 - .../main/res/layout/activity_asset_loader.xml | 25 ++ .../main/res/layout/fragment_asset_button.xml | 11 + .../main/res/layout/fragment_asset_loader.xml | 21 ++ app/src/main/res/layout/main.xml | 9 + app/src/main/res/raw/cdn_image.riv | Bin 0 -> 325 bytes app/src/main/res/raw/firacode.ttf | Bin 0 -> 259388 bytes app/src/main/res/raw/fontz.riv | Bin 0 -> 1776743 bytes app/src/main/res/raw/fontz_oob.riv | Bin 0 -> 20878 bytes app/src/main/res/raw/montserrat.ttf | Bin 0 -> 394140 bytes app/src/main/res/raw/multiple_animations.riv | Bin 0 -> 81 bytes app/src/main/res/raw/multipleartboards.riv | Bin 0 -> 289 bytes app/src/main/res/raw/roboto.ttf | Bin 0 -> 1755856 bytes app/src/main/res/raw/walle.riv | Bin 0 -> 264 bytes app/src/main/res/raw/walle_img_eve.png | Bin 0 -> 246825 bytes app/src/main/res/raw/walle_img_walle.png | Bin 0 -> 218873 bytes app/src/main/res/values/colors.xml | 1 + build.gradle | 4 +- compatibilitytest/build.gradle | 6 +- kotlin/build.gradle | 7 +- .../runtime/kotlin/core/RiveFileLoadTest.kt | 53 ++++ .../rive/runtime/kotlin/core/RiveViewTest.kt | 16 ++ .../app/rive/runtime/kotlin/core/TestUtils.kt | 3 +- kotlin/src/androidTest/res/raw/cdn_image.riv | Bin 0 -> 325 bytes kotlin/src/androidTest/res/raw/eve.png | Bin 0 -> 246825 bytes kotlin/src/androidTest/res/raw/walle.riv | Bin 0 -> 264 bytes kotlin/src/main/cpp/clean_all.sh | 3 +- .../src/main/cpp/include/helpers/general.hpp | 7 +- .../include/models/jni_file_asset_loader.hpp | 38 +++ .../main/cpp/include/models/jni_renderer.hpp | 1 + .../main/cpp/src/bindings/bindings_file.cpp | 11 +- .../cpp/src/bindings/bindings_file_asset.cpp | 73 +++++ .../bindings/bindings_file_asset_loader.cpp | 42 +++ .../src/main/cpp/src/helpers/egl_worker.cpp | 9 + kotlin/src/main/cpp/src/helpers/general.cpp | 35 ++- kotlin/src/main/cpp/src/jni_refs.cpp | 10 +- .../cpp/src/models/jni_file_asset_loader.cpp | 75 +++++ .../rive/runtime/kotlin/RiveAnimationView.kt | 172 +++++++++++- .../kotlin/controllers/RiveFileController.kt | 16 +- .../app/rive/runtime/kotlin/core/Artboard.kt | 1 - .../java/app/rive/runtime/kotlin/core/File.kt | 24 +- .../app/rive/runtime/kotlin/core/FileAsset.kt | 17 ++ .../runtime/kotlin/core/FileAssetLoader.kt | 144 ++++++++++ .../rive/runtime/kotlin/core/NativeObject.kt | 1 + .../java/app/rive/runtime/kotlin/core/Rive.kt | 1 - .../app/rive/runtime/kotlin/core/RiveEvent.kt | 2 +- .../runtime/kotlin/core/RiveOpenURLEvent.kt | 2 +- .../kotlin/core/StateMachineInstance.kt | 10 +- .../rive/runtime/kotlin/renderers/Renderer.kt | 14 +- kotlin/src/main/res/values/attrs.xml | 4 + submodules/rive-cpp | 2 +- 69 files changed, 1486 insertions(+), 126 deletions(-) create mode 100644 app/src/androidTest/java/app/rive/runtime/example/RiveBuilderTest.kt create mode 100644 app/src/main/java/app/rive/runtime/example/AssetLoaderActivity.kt create mode 100644 app/src/main/java/app/rive/runtime/example/AssetLoaderFragment.kt delete mode 100644 app/src/main/java/app/rive/runtime/example/SingleActivity.kt create mode 100644 app/src/main/java/app/rive/runtime/example/TestActivity.kt create mode 100644 app/src/main/res/layout/activity_asset_loader.xml create mode 100644 app/src/main/res/layout/fragment_asset_button.xml create mode 100644 app/src/main/res/layout/fragment_asset_loader.xml create mode 100644 app/src/main/res/raw/cdn_image.riv create mode 100644 app/src/main/res/raw/firacode.ttf create mode 100644 app/src/main/res/raw/fontz.riv create mode 100644 app/src/main/res/raw/fontz_oob.riv create mode 100644 app/src/main/res/raw/montserrat.ttf create mode 100644 app/src/main/res/raw/multiple_animations.riv create mode 100644 app/src/main/res/raw/multipleartboards.riv create mode 100644 app/src/main/res/raw/roboto.ttf create mode 100644 app/src/main/res/raw/walle.riv create mode 100644 app/src/main/res/raw/walle_img_eve.png create mode 100644 app/src/main/res/raw/walle_img_walle.png create mode 100644 kotlin/src/androidTest/res/raw/cdn_image.riv create mode 100644 kotlin/src/androidTest/res/raw/eve.png create mode 100644 kotlin/src/androidTest/res/raw/walle.riv create mode 100644 kotlin/src/main/cpp/include/models/jni_file_asset_loader.hpp create mode 100644 kotlin/src/main/cpp/src/bindings/bindings_file_asset.cpp create mode 100644 kotlin/src/main/cpp/src/bindings/bindings_file_asset_loader.cpp create mode 100644 kotlin/src/main/cpp/src/models/jni_file_asset_loader.cpp create mode 100644 kotlin/src/main/java/app/rive/runtime/kotlin/core/FileAsset.kt create mode 100644 kotlin/src/main/java/app/rive/runtime/kotlin/core/FileAssetLoader.kt diff --git a/.rive_head b/.rive_head index 3bd9e52f..ed15abfb 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -6a74a01f27fbdabe03827a72614e24f8eff018a0 +8c99c8dcf67c5bf80bba1ca953d3e3dfa591e6d7 diff --git a/.rive_renderer b/.rive_renderer index 4368c092..90da70b1 100644 --- a/.rive_renderer +++ b/.rive_renderer @@ -1 +1 @@ -56232dbd3f8cfd22d9304467427561e2cafd035c +6a47e8989777488f8bc159c4312a1c92e1308733 diff --git a/app/build.gradle b/app/build.gradle index 5e09f5ab..c3b9cdc8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "app.rive.runtime.example" @@ -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" diff --git a/app/src/androidTest/java/app/rive/runtime/example/RiveBuilderTest.kt b/app/src/androidTest/java/app/rive/runtime/example/RiveBuilderTest.kt new file mode 100644 index 00000000..160ca7f1 --- /dev/null +++ b/app/src/androidTest/java/app/rive/runtime/example/RiveBuilderTest.kt @@ -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(), // 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() + 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) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0a6a2240..b5dbf637 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,8 +61,11 @@ android:theme="@style/AppTheme" /> + - + + + \ No newline at end of file diff --git a/app/src/main/java/app/rive/runtime/example/AndroidPlayerActivity.kt b/app/src/main/java/app/rive/runtime/example/AndroidPlayerActivity.kt index 64f951b5..b9b99192 100644 --- a/app/src/main/java/app/rive/runtime/example/AndroidPlayerActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/AndroidPlayerActivity.kt @@ -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 @@ -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 } @@ -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 } diff --git a/app/src/main/java/app/rive/runtime/example/AssetLoaderActivity.kt b/app/src/main/java/app/rive/runtime/example/AssetLoaderActivity.kt new file mode 100644 index 00000000..dd6dcaab --- /dev/null +++ b/app/src/main/java/app/rive/runtime/example/AssetLoaderActivity.kt @@ -0,0 +1,101 @@ +package app.rive.runtime.example + +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import app.rive.runtime.example.databinding.ActivityAssetLoaderBinding +import app.rive.runtime.kotlin.core.ContextAssetLoader +import app.rive.runtime.kotlin.core.FileAsset +import app.rive.runtime.kotlin.core.FileAssetLoader +import app.rive.runtime.kotlin.core.BytesRequest +import app.rive.runtime.kotlin.core.ExperimentalAssetLoader +import app.rive.runtime.kotlin.core.Rive +import com.android.volley.toolbox.Volley +import com.google.android.material.tabs.TabLayoutMediator +import kotlin.random.Random + +class AssetLoaderActivity : AppCompatActivity() { + private lateinit var binding: ActivityAssetLoaderBinding + + @ExperimentalAssetLoader + override fun onCreate(savedInstanceState: Bundle?) { + // Setup + Rive.init(this) + super.onCreate(savedInstanceState) + + binding = ActivityAssetLoaderBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.viewPager.adapter = object : FragmentStateAdapter(this) { + override fun getItemCount(): Int = 3 + + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> AssetLoaderFragment() + 1 -> AssetButtonFragment() + else -> FontAssetFragment() + } + } + } + + TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> + tab.text = when (position) { + 0 -> "Asset Loader" + 1 -> "Change Images" + else -> "Fonts" + } + }.attach() + } +} + +/** + * [FileAssetLoader] tailored for the two assets in the walle.riv file. + * + * The main purpose of this class is to demonstrate how create a custom [ContextAssetLoader] via XML. + */ +@ExperimentalAssetLoader +class WalleAssetLoader(context: Context) : ContextAssetLoader(context) { + override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean { + val identifier = + if (asset.uniqueFilename.contains("eve")) R.raw.walle_img_eve + else R.raw.walle_img_walle + + context.resources.openRawResource(identifier).use { + asset.decode(it.readBytes()) + } + return true + } +} + +/** + * Loads a random image from picsum.photos. + */ +@ExperimentalAssetLoader +class RandomNetworkLoader(context: Context) : FileAssetLoader() { + private val loremImage = "https://picsum.photos" + private val queue = Volley.newRequestQueue(context) + private val minSize = 150 + private val maxSize = 500 + private val maxId = 1084 + + override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean { + val randomWidth = Random.nextInt(minSize, maxSize) + val randomHeight = Random.nextInt(minSize, maxSize) + val imgId = Random.nextInt(maxId) + + val url = "$loremImage/id/$imgId/$randomWidth/$randomHeight" + val request = BytesRequest( + url, + { bytes -> asset.decode(bytes) }, + { + Log.e("Request", "onAssetLoaded: failed to load image from $url") + it.printStackTrace() + } + ) + queue.add(request) + return true + } +} diff --git a/app/src/main/java/app/rive/runtime/example/AssetLoaderFragment.kt b/app/src/main/java/app/rive/runtime/example/AssetLoaderFragment.kt new file mode 100644 index 00000000..e9cecd6a --- /dev/null +++ b/app/src/main/java/app/rive/runtime/example/AssetLoaderFragment.kt @@ -0,0 +1,197 @@ +package app.rive.runtime.example + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.FrameLayout +import androidx.fragment.app.Fragment +import app.rive.runtime.example.databinding.FragmentAssetButtonBinding +import app.rive.runtime.example.databinding.FragmentAssetLoaderBinding +import app.rive.runtime.kotlin.RiveAnimationView +import app.rive.runtime.kotlin.core.BytesRequest +import app.rive.runtime.kotlin.core.ExperimentalAssetLoader +import app.rive.runtime.kotlin.core.FileAsset +import app.rive.runtime.kotlin.core.FileAssetLoader +import app.rive.runtime.kotlin.core.Loop +import com.android.volley.RequestQueue +import com.android.volley.toolbox.Volley +import kotlin.random.Random + +private fun makeContainer(context: Context): FrameLayout { + return FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ).apply { + // 16dp equivalent + val dpPadding = (16 * resources.displayMetrics.density).toInt() + topMargin = dpPadding + } + } +} + +class AssetLoaderFragment : Fragment() { + private var _binding: FragmentAssetLoaderBinding? = null + private val binding get() = _binding!! + private lateinit var networkLoader: RandomNetworkLoader + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAssetLoaderBinding.inflate(inflater, container, false) + val view = binding.root + val ctx = view.context + networkLoader = RandomNetworkLoader(ctx) + + + makeContainer(ctx).let { + val riveView = RiveAnimationView.Builder(ctx) + .setAssetLoader(networkLoader) + .setResource(R.raw.walle) + .build() + it.addView(riveView) + binding.fragmentLoaderContainer.addView(it) + } + + + makeContainer(ctx).let { + val cdnRiveView = RiveAnimationView.Builder(ctx) + .setResource(R.raw.cdn_image) + .build() + it.addView(cdnRiveView) + binding.fragmentLoaderContainer.addView(it) + } + + return view + } +} + +@ExperimentalAssetLoader +class AssetButtonFragment : Fragment() { + private var _binding: FragmentAssetButtonBinding? = null + private val binding get() = _binding!! + + private val loremImage = "https://picsum.photos" + private lateinit var queue: RequestQueue + private lateinit var assetStore: AssetStore + private val minSize = 150 + private val maxSize = 500 + private val maxId = 1084 + private var lastAssetChanged = 0 + + private fun makeButton(context: Context): Button { + return Button(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + text = "Change Image" + setOnClickListener { + val randomAsset = + assetStore.assetList[lastAssetChanged++ % assetStore.assetList.size] + val randomWidth = Random.nextInt(minSize, maxSize) + val randomHeight = Random.nextInt(minSize, maxSize) + val imgId = Random.nextInt(maxId) + val url = "$loremImage/id/$imgId/$randomWidth/$randomHeight" + val request = BytesRequest( + url, + { bytes -> randomAsset.decode(bytes) }, + { + Log.e("Request", "onAssetLoaded: failed to load $url.") + it.printStackTrace() + } + ) + queue.add(request) + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = FragmentAssetButtonBinding.inflate(inflater, container, false) + val view = binding.root + val ctx = view.context + queue = Volley.newRequestQueue(ctx) + + assetStore = AssetStore() + val riveView = RiveAnimationView.Builder(ctx) + .setAssetLoader(assetStore) + .setResource(R.raw.walle) + .build() + + makeContainer(ctx).let { + it.addView(riveView) + binding.fragmentButtonContainer.addView(it) + } + + makeButton(ctx).let { + binding.fragmentButtonContainer.addView(it) + } + + + return view + } + + @ExperimentalAssetLoader + private class AssetStore : FileAssetLoader() { + val assetList = mutableListOf() + override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean { + return assetList.add(asset) + } + } +} + +@ExperimentalAssetLoader +class FontAssetFragment : Fragment() { + private var _binding: FragmentAssetButtonBinding? = null + private val binding get() = _binding!! + + private lateinit var fontDecoder: RandomFontDecoder + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = FragmentAssetButtonBinding.inflate(inflater, container, false) + val view = binding.root + val ctx = view.context + + fontDecoder = RandomFontDecoder(ctx) + val riveView = RiveAnimationView.Builder(ctx) + .setAssetLoader(fontDecoder) + .setAnimationName("Bounce") + .setLoop(Loop.PINGPONG) + .setResource(R.raw.fontz_oob) + .build() + + makeContainer(ctx).let { + it.addView(riveView) + binding.fragmentButtonContainer.addView(it) + } + + return view + } + + @ExperimentalAssetLoader + private class RandomFontDecoder(private val context: Context) : FileAssetLoader() { + lateinit var fontAsset: FileAsset + override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean { + // Store a reference to the asset. + fontAsset = asset + + // Load the original font + context.resources.openRawResource(R.raw.roboto).use { + return asset.decode(it.readBytes()) + } + } + } +} diff --git a/app/src/main/java/app/rive/runtime/example/EventsActivity.kt b/app/src/main/java/app/rive/runtime/example/EventsActivity.kt index 1aa41b8e..14b7d6d5 100644 --- a/app/src/main/java/app/rive/runtime/example/EventsActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/EventsActivity.kt @@ -8,10 +8,9 @@ import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import app.rive.runtime.kotlin.RiveAnimationView import app.rive.runtime.kotlin.controllers.RiveFileController +import app.rive.runtime.kotlin.core.RiveEvent import app.rive.runtime.kotlin.core.RiveGeneralEvent import app.rive.runtime.kotlin.core.RiveOpenURLEvent -import app.rive.runtime.kotlin.core.RiveEvent -import java.lang.Exception class EventsActivity : AppCompatActivity() { @@ -37,15 +36,15 @@ class EventsActivity : AppCompatActivity() { } override fun onDestroy() { - starRatingAnimation.removeEventListener(starRatingListener); - urlButtonAnimation.removeEventListener(urlButtonListener); - logButtonAnimation.removeEventListener(logButtonListener); + starRatingAnimation.removeEventListener(starRatingListener) + urlButtonAnimation.removeEventListener(urlButtonListener) + logButtonAnimation.removeEventListener(logButtonListener) super.onDestroy() } - private lateinit var starRatingListener: RiveFileController.RiveEventListener; - private lateinit var urlButtonListener: RiveFileController.RiveEventListener; - private lateinit var logButtonListener: RiveFileController.RiveEventListener; + private lateinit var starRatingListener: RiveFileController.RiveEventListener + private lateinit var urlButtonListener: RiveFileController.RiveEventListener + private lateinit var logButtonListener: RiveFileController.RiveEventListener private fun setStarRating() { val starRatingTextView = findViewById(R.id.star_rating) @@ -54,7 +53,10 @@ class EventsActivity : AppCompatActivity() { override fun notifyEvent(event: RiveEvent) { when (event) { is RiveGeneralEvent -> { - Log.i("RiveEvent", "General event received, name: ${event.name}, delaySeconds: ${event.delay} properties: ${event.properties}") + Log.i( + "RiveEvent", + "General event received, name: ${event.name}, delaySeconds: ${event.delay} properties: ${event.properties}" + ) runOnUiThread { // This event contains a number value with the name "rating" // to indicate the star rating selected @@ -67,7 +69,7 @@ class EventsActivity : AppCompatActivity() { } } } - starRatingAnimation.addEventListener(starRatingListener); + starRatingAnimation.addEventListener(starRatingListener) } private fun setUrlButton() { @@ -78,7 +80,7 @@ class EventsActivity : AppCompatActivity() { Log.i("RiveEvent", "Open URL Rive event: ${event.url}") runOnUiThread { try { - val uri = Uri.parse(event.url); + val uri = Uri.parse(event.url) val browserIntent = Intent(Intent.ACTION_VIEW, uri) startActivity(browserIntent) @@ -90,7 +92,7 @@ class EventsActivity : AppCompatActivity() { } } } - urlButtonAnimation.addEventListener(urlButtonListener); + urlButtonAnimation.addEventListener(urlButtonListener) } private fun setLogButton() { @@ -100,6 +102,7 @@ class EventsActivity : AppCompatActivity() { is RiveOpenURLEvent -> { Log.i("RiveEvent", "Open URL Rive event: ${event.url}") } + is RiveGeneralEvent -> { Log.i("RiveEvent", "General Rive event") } @@ -109,9 +112,9 @@ class EventsActivity : AppCompatActivity() { Log.i("RiveEvent", "type: ${event.type}") Log.i("RiveEvent", "properties: ${event.properties}") // `data` contains all information in the event - Log.i("RiveEvent", "data: ${event.data}"); + Log.i("RiveEvent", "data: ${event.data}") } } - logButtonAnimation.addEventListener(logButtonListener); + logButtonAnimation.addEventListener(logButtonListener) } } diff --git a/app/src/main/java/app/rive/runtime/example/HttpActivity.kt b/app/src/main/java/app/rive/runtime/example/HttpActivity.kt index 21f3e770..3cc6e903 100644 --- a/app/src/main/java/app/rive/runtime/example/HttpActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/HttpActivity.kt @@ -31,7 +31,7 @@ class HttpActivity : AppCompatActivity() { setContentView(R.layout.activity_http) // Hides the app/action bar - supportActionBar?.hide(); + supportActionBar?.hide() // Load the Rive data asynchronously httpViewModel.byteLiveData.observe( diff --git a/app/src/main/java/app/rive/runtime/example/InteractiveSamplesActivity.kt b/app/src/main/java/app/rive/runtime/example/InteractiveSamplesActivity.kt index a984f737..d32a12f2 100644 --- a/app/src/main/java/app/rive/runtime/example/InteractiveSamplesActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/InteractiveSamplesActivity.kt @@ -5,7 +5,7 @@ import android.os.Handler import android.os.Looper import androidx.appcompat.app.AppCompatActivity import app.rive.runtime.kotlin.RiveAnimationView -import java.util.* +import java.util.Calendar class InteractiveSamplesActivity : AppCompatActivity() { private var keepGoing = true diff --git a/app/src/main/java/app/rive/runtime/example/LowLevelActivity.kt b/app/src/main/java/app/rive/runtime/example/LowLevelActivity.kt index f111b2fb..bc5acb80 100644 --- a/app/src/main/java/app/rive/runtime/example/LowLevelActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/LowLevelActivity.kt @@ -22,7 +22,7 @@ class LowLevelActivity : AppCompatActivity() { setContentView(R.layout.activity_low_level) // Hides the app/action bar - supportActionBar?.hide(); + supportActionBar?.hide() // Attach the Rive view to the activity's root layout val layout = findViewById(R.id.low_level_view_root) diff --git a/app/src/main/java/app/rive/runtime/example/MainActivity.kt b/app/src/main/java/app/rive/runtime/example/MainActivity.kt index 64f5d300..2ee0e1b8 100644 --- a/app/src/main/java/app/rive/runtime/example/MainActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/MainActivity.kt @@ -24,7 +24,6 @@ class MainActivity : AppCompatActivity() { Pair(R.id.go_blend, BlendActivity::class.java), Pair(R.id.go_metrics, MetricsActivity::class.java), Pair(R.id.go_assets, AssetsActivity::class.java), - Pair(R.id.go_recycler, RecyclerActivity::class.java), Pair(R.id.go_viewpager, ViewPagerActivity::class.java), Pair(R.id.go_meshes, MeshesActivity::class.java), @@ -32,6 +31,7 @@ class MainActivity : AppCompatActivity() { Pair(R.id.go_compose, ComposeActivity::class.java), Pair(R.id.go_frame, FrameActivity::class.java), Pair(R.id.go_dynamic_text, DynamicTextActivity::class.java), + Pair(R.id.go_assets_loader, AssetLoaderActivity::class.java), Pair(R.id.go_stress_test, StressTestActivity::class.java), ) diff --git a/app/src/main/java/app/rive/runtime/example/RecyclerActivity.kt b/app/src/main/java/app/rive/runtime/example/RecyclerActivity.kt index 7bfd41d5..ccc2a719 100644 --- a/app/src/main/java/app/rive/runtime/example/RecyclerActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/RecyclerActivity.kt @@ -22,6 +22,7 @@ class RecyclerActivity : AppCompatActivity() { companion object { const val holderCount = 200 const val numCols = 3 + // Either use the shared file (true) or initialize in the adapter (false) const val useSharedFile = true } diff --git a/app/src/main/java/app/rive/runtime/example/RiveFragment.kt b/app/src/main/java/app/rive/runtime/example/RiveFragment.kt index 4d7b86d4..36be9b1c 100644 --- a/app/src/main/java/app/rive/runtime/example/RiveFragment.kt +++ b/app/src/main/java/app/rive/runtime/example/RiveFragment.kt @@ -10,7 +10,7 @@ import app.rive.runtime.kotlin.core.Fit import kotlin.properties.Delegates // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -public const val RIVE_FRAGMENT_ARG_RES_ID = "resourceId" +const val RIVE_FRAGMENT_ARG_RES_ID = "resourceId" /** * A [RiveFragment] encapsulates a [RiveAnimationView]. diff --git a/app/src/main/java/app/rive/runtime/example/SingleActivity.kt b/app/src/main/java/app/rive/runtime/example/SingleActivity.kt deleted file mode 100644 index e0545fe2..00000000 --- a/app/src/main/java/app/rive/runtime/example/SingleActivity.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.rive.runtime.example - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class SingleActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val isRiveRenderer = intent.getStringExtra("renderer") == "Rive" - setContentView( - if (isRiveRenderer) - R.layout.single_rive_renderer - else - R.layout.single - ) - } -} diff --git a/app/src/main/java/app/rive/runtime/example/StressTestActivity.kt b/app/src/main/java/app/rive/runtime/example/StressTestActivity.kt index a4034c23..4635e8c6 100644 --- a/app/src/main/java/app/rive/runtime/example/StressTestActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/StressTestActivity.kt @@ -3,17 +3,21 @@ package app.rive.runtime.example import android.content.Context import android.graphics.RectF import android.os.Bundle +import android.view.MotionEvent import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import app.rive.runtime.kotlin.RiveTextureView -import app.rive.runtime.kotlin.core.* -import app.rive.runtime.kotlin.renderers.Renderer -import java.util.* -import android.view.MotionEvent import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleObserver -import kotlin.math.* +import app.rive.runtime.kotlin.RiveTextureView +import app.rive.runtime.kotlin.core.Alignment +import app.rive.runtime.kotlin.core.Artboard +import app.rive.runtime.kotlin.core.File +import app.rive.runtime.kotlin.core.Fit +import app.rive.runtime.kotlin.core.LinearAnimationInstance +import app.rive.runtime.kotlin.renderers.Renderer +import java.util.Locale +import kotlin.math.min class StressTestActivity : AppCompatActivity() { @@ -33,9 +37,9 @@ class StressTestActivity : AppCompatActivity() { } class StressTestView(context: Context) : RiveTextureView(context) { - private var instanceCount : Int = 1 - private var totalElapsed : Float = 0f - private var totalFrames : Int = 0 + private var instanceCount: Int = 1 + private var totalElapsed: Float = 0f + private var totalFrames: Int = 0 override fun createObserver(): LifecycleObserver { return object : DefaultLifecycleObserver { @@ -60,7 +64,7 @@ class StressTestView(context: Context) : RiveTextureView(context) { val rows = (instanceCount + 6) / 7 val cols = min(instanceCount, 7) translate(0f, (rows - 1) * -.5f * 200f) - for (j in 1 .. rows) { + for (j in 1..rows) { save() translate((cols - 1) * -.5f * 125f, 0f) for (i in 1..cols) { @@ -95,7 +99,7 @@ class StressTestView(context: Context) : RiveTextureView(context) { if (totalElapsed > 1f) { val fps = totalFrames / totalElapsed val fpsView = - ((getParent() as ViewGroup).getParent() as ViewGroup).getChildAt(1) as TextView + ((parent as ViewGroup).parent as ViewGroup).getChildAt(1) as TextView fpsView.text = java.lang.String.format( Locale.US, @@ -132,7 +136,7 @@ class StressTestView(context: Context) : RiveTextureView(context) { override fun onTouchEvent(event: MotionEvent): Boolean { - val action: Int = event.getActionMasked() + val action: Int = event.actionMasked return when (action) { MotionEvent.ACTION_DOWN -> { @@ -142,10 +146,12 @@ class StressTestView(context: Context) : RiveTextureView(context) { instanceCount += 7 totalElapsed = 0f totalFrames = 0 - val fpsView = ((getParent() as ViewGroup).getParent() as ViewGroup).getChildAt(1) as TextView + val fpsView = + ((parent as ViewGroup).parent as ViewGroup).getChildAt(1) as TextView fpsView.text = java.lang.String.format("%d instances", instanceCount) true } + else -> super.onTouchEvent(event) } } diff --git a/app/src/main/java/app/rive/runtime/example/TestActivity.kt b/app/src/main/java/app/rive/runtime/example/TestActivity.kt new file mode 100644 index 00000000..f133a7d8 --- /dev/null +++ b/app/src/main/java/app/rive/runtime/example/TestActivity.kt @@ -0,0 +1,39 @@ +package app.rive.runtime.example + +import android.os.Bundle +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity + + +class SingleActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val isRiveRenderer = intent.getStringExtra("renderer") == "Rive" + setContentView( + if (isRiveRenderer) + R.layout.single_rive_renderer + else + R.layout.single + ) + } +} + +class EmptyActivity : AppCompatActivity() { + + lateinit var container: FrameLayout + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Create a new FrameLayout object and add it. + FrameLayout(this).let { + container = it + it.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ) + setContentView(it) + } + + + } +} \ No newline at end of file diff --git a/app/src/main/java/app/rive/runtime/example/ViewPagerActivity.kt b/app/src/main/java/app/rive/runtime/example/ViewPagerActivity.kt index fda045bc..0da930bb 100644 --- a/app/src/main/java/app/rive/runtime/example/ViewPagerActivity.kt +++ b/app/src/main/java/app/rive/runtime/example/ViewPagerActivity.kt @@ -73,10 +73,6 @@ class ViewPagerActivity : AppCompatActivity() { // Keep ControllerStates around. var resourceCache = arrayOfNulls(itemCount) - override fun onViewRecycled(holder: RiveTestViewHolder) { - super.onViewRecycled(holder) - } - override fun onViewAttachedToWindow(holder: RiveTestViewHolder) { super.onViewAttachedToWindow(holder) val riveView = holder.binding.riveTestView diff --git a/app/src/main/res/layout/activity_asset_loader.xml b/app/src/main/res/layout/activity_asset_loader.xml new file mode 100644 index 00000000..b619226a --- /dev/null +++ b/app/src/main/res/layout/activity_asset_loader.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_asset_button.xml b/app/src/main/res/layout/fragment_asset_button.xml new file mode 100644 index 00000000..f7c9c7a9 --- /dev/null +++ b/app/src/main/res/layout/fragment_asset_button.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_asset_loader.xml b/app/src/main/res/layout/fragment_asset_loader.xml new file mode 100644 index 00000000..09cfcf45 --- /dev/null +++ b/app/src/main/res/layout/fragment_asset_loader.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml index bcdc8d56..2585942f 100644 --- a/app/src/main/res/layout/main.xml +++ b/app/src/main/res/layout/main.xml @@ -218,6 +218,15 @@ android:text="Dynamic Text" android:textColor="@color/textColorPrimary" /> +