diff --git a/README.md b/README.md index 913ae800..6aaab68a 100644 --- a/README.md +++ b/README.md @@ -208,22 +208,27 @@ import { RiveView, RiveErrorType } from '@rive-app/react-native'; > **Note**: If no `onError` handler is provided, errors will be logged to the console by default. -## Animation Lifecycle +## Reacting to an Animation Finishing -The `RiveView` component provides an `onStop` callback prop, called when the animation/state machine stops playing — for example, when a non-looping animation reaches its end. This is useful for splash-screen-style animations where you want to navigate away once playback finishes: +To run app logic when an animation completes — for example navigating away once a splash-screen animation finishes — fire a data-binding trigger from your state machine and listen for it with `useRiveTrigger`: -```js - { - // The animation has finished playing - navigation.replace('Home'); - }} -/> +```tsx +import { + RiveView, + useRiveTrigger, + useViewModelInstance, +} from '@rive-app/react-native'; + +const { instance } = useViewModelInstance(riveFile, { async: true }); + +useRiveTrigger('finished', instance, { + onTrigger: () => navigation.replace('Home'), +}); + +; ``` -> **Note**: `onStop` is not called by `pause()` — only when playback naturally comes to rest (e.g. a one-shot animation or a state machine reaching a state with no further transitions). +In the Rive editor: add a **Trigger** property (e.g. `finished`) to your artboard's View Model, then on the transition out of your one-shot animation state enable **Exit Time** and set it to **100%** (the value matters — an exit time of `0ms` fires immediately), and add a **Set property value** action targeting the trigger. See [this community file](https://rive.app/community/files/28526-53929-fire-a-trigger-when-timeline-finishes) for a working setup, and the "Finished Trigger" demo in the example app for the app-side wiring. ## Feature Support @@ -251,7 +256,6 @@ The following table compares feature availability with the [previous Rive React | `useRive()` hook | ✅ | Convenient hook to access the Rive View ref after load | | `useRiveFile()` hook | ✅ | Convenient hook to load a Rive file | | `RiveView` error handling | ✅ | Error handler for failed view operations | -| `RiveView` `onStop` callback | ✅ | Callback fired when the animation/state machine stops playing | | `source` .riv file loading | ✅ | Conveniently load .riv files from JS source | | Accessibility semantics | ⚠️ | Editor-authored semantics → VoiceOver (iOS; Android in progress) | | Animation selection | ❌ | Animation playback not planned, use state machines | diff --git a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt index f1118170..d4f2ba4a 100644 --- a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -105,14 +105,6 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { } } override var onError: (error: RiveError) -> Unit = {} - - // Not wired on the legacy runtime: RiveFileController.Listener has no - // signal that reliably maps to "playback came to rest" here — a settling - // state machine reports notifyPause, and notifyStop instead fires - // spuriously from setArtboard()'s internal stopAnimations() on reconfigure. - // The legacy backend is internal-testing-only, so this stays a no-op - // rather than reporting an incorrect stop. - override var onStop: () -> Unit = {} //endregion //region View Methods diff --git a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt index d4f7ab0b..4d18af5d 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -67,9 +67,6 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { onError = { msg -> this@HybridRiveView.onError(RiveError(type = RiveErrorType.UNKNOWN, message = msg)) } - onStop = { - this@HybridRiveView.onStop() - } } private var needsReload = false private var dataBindingChanged = false @@ -122,7 +119,6 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { } } override var onError: (error: RiveError) -> Unit = {} - override var onStop: () -> Unit = {} override fun awaitViewReady(): Promise { return Promise.async { diff --git a/android/src/new/java/com/rive/RiveReactNativeView.kt b/android/src/new/java/com/rive/RiveReactNativeView.kt index 66a4b7b9..020b96a8 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -73,10 +73,6 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { var onError: ((String) -> Unit)? = null - // Fired when the state machine settles (reaches rest, e.g. a non-looping - // animation reaching its end); wired to the onStop prop. - var onStop: (() -> Unit)? = null - private var settledJob: Job? = null // rive-runtime's command server emits a settle signal on every advance @@ -321,9 +317,8 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { // compatibility shim. Revisit when 12.0 defines the long-term settling surface. @Suppress("DEPRECATION") worker.settledFlow.collect { settledHandle -> - if (settledHandle == handle && !settled) { + if (settledHandle == handle) { settled = true - onStop?.invoke() } } } diff --git a/docs/runtime-backends.md b/docs/runtime-backends.md index 7a45cf60..49b567b5 100644 --- a/docs/runtime-backends.md +++ b/docs/runtime-backends.md @@ -40,10 +40,6 @@ Behavioral differences on the new runtime: handles; a bad path surfaces via the `getValueAsync()` rejection or the `useRive*` hooks' `error` result instead of an `undefined` return. - `updateReferencedAssets` (runtime asset swapping) is not supported. -- The `onStop` prop is only wired up on the new runtime. The legacy backend - has no signal that reliably maps to "playback came to rest" (a settling - state machine reports `notifyPause`/`pausedWithModel`, not a stop), so - `onStop` is left as a no-op there rather than reporting an incorrect stop. ## Android render backend (new runtime only) diff --git a/example/__tests__/finished-trigger.harness.tsx b/example/__tests__/finished-trigger.harness.tsx new file mode 100644 index 00000000..eb3c57c3 --- /dev/null +++ b/example/__tests__/finished-trigger.harness.tsx @@ -0,0 +1,143 @@ +import { + describe, + it, + expect, + render, + waitFor, + cleanup, +} from 'react-native-harness'; +import { View } from 'react-native'; +import { + RiveView, + RiveFileFactory, + Fit, + useRiveTrigger, + type RiveFile, + type RiveViewRef, + type ViewModelInstance, +} from '@rive-app/react-native'; + +// One-shot animation that fires the 'finished' view-model trigger when it +// completes (state machine exit-time transition action) — the data-binding +// replacement for the removed onStop prop. +const FINISHED_TRIGGER = require('../assets/rive/finished_trigger.riv'); + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +function expectDefined(value: T): asserts value is NonNullable { + expect(value).toBeDefined(); +} + +type TestContext = { + ref: RiveViewRef | null; + triggerCount: number; + error: string | null; +}; + +function FinishedView({ + file, + instance, + context, +}: { + file: RiveFile; + instance: ViewModelInstance; + context: TestContext; +}) { + useRiveTrigger('finished', instance, { + onTrigger: () => { + context.triggerCount++; + }, + }); + + return ( + + { + context.ref = ref; + }, + }} + style={{ flex: 1 }} + file={file} + autoPlay={true} + fit={Fit.Contain} + dataBind={instance} + onError={(e) => { + context.error = e.message; + }} + /> + + ); +} + +// getPropertiesAsync is not implemented on the legacy backend, so the +// introspection test only runs on the new runtime. The behavioral test below +// runs on every backend. +const isExperimental = RiveFileFactory.getBackend() === 'experimental'; + +describe('finished-trigger.riv', () => { + (isExperimental ? it : it.skip)( + 'loads and exposes a view model with a finished trigger', + async () => { + const file = await RiveFileFactory.fromSource( + FINISHED_TRIGGER, + undefined + ); + expectDefined(file); + + const artboards = await file.getArtboardNamesAsync(); + const vmNames = await file.getViewModelNamesAsync(); + console.log(`artboards: ${JSON.stringify(artboards)}`); + console.log(`viewModels: ${JSON.stringify(vmNames)}`); + + for (const name of vmNames) { + const vm = await file.viewModelByNameAsync(name); + expectDefined(vm); + const props = await vm.getPropertiesAsync(); + console.log(`VM "${name}" properties: ${JSON.stringify(props)}`); + } + + const vm = await file.defaultArtboardViewModelAsync(); + expectDefined(vm); + const props = await vm.getPropertiesAsync(); + const finished = props.find((p) => p.name === 'finished'); + expectDefined(finished); + cleanup(); + } + ); + + it('fires the finished trigger exactly once when the animation completes', async () => { + const file = await RiveFileFactory.fromSource(FINISHED_TRIGGER, undefined); + const vm = await file.defaultArtboardViewModelAsync(); + expectDefined(vm); + const instance = await vm.createDefaultInstanceAsync(); + expectDefined(instance); + + const context: TestContext = { ref: null, triggerCount: 0, error: null }; + const mountedAt = Date.now(); + await render( + + ); + + await waitFor(() => expect(context.triggerCount).toBeGreaterThan(0), { + timeout: 10000, + }); + + // The one-shot timeline is ~1s; a fire much earlier means the state + // machine exited the animation state immediately (e.g. exit time not + // set to 100% in the editor) instead of at completion. + const firedAfterMs = Date.now() - mountedAt; + console.log(`finished trigger fired ${firedAfterMs} ms after mount`); + expect(firedAfterMs).toBeGreaterThan(500); + + const samples: number[] = []; + for (let i = 0; i < 3; i++) { + await delay(1000); + samples.push(context.triggerCount); + } + console.log(`finished trigger count progression: ${samples.join(', ')}`); + expect(context.error).toBeNull(); + expect(context.triggerCount).toBe(1); + cleanup(); + }); +}); diff --git a/example/__tests__/on-stop.harness.tsx b/example/__tests__/on-stop.harness.tsx deleted file mode 100644 index 5fbb0049..00000000 --- a/example/__tests__/on-stop.harness.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { - describe, - it, - expect, - render, - waitFor, - cleanup, -} from 'react-native-harness'; -import { View } from 'react-native'; -import { - RiveView, - RiveFileFactory, - Fit, - type RiveFile, - type RiveViewRef, -} from '@rive-app/react-native'; - -// onStop is only wired up on the new (experimental) runtime — see the -// "Animation Lifecycle" section of the README. -const isExperimental = RiveFileFactory.getBackend() === 'experimental'; - -// Interactive rating file: the state machine idles (settles) unless touched. -const RATING = require('../assets/rive/rating.riv'); -// Continuously animating ball: never settles while playing. -const BOUNCING_BALL = require('../assets/rive/bouncing_ball.riv'); - -const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -type TestContext = { - ref: RiveViewRef | null; - stopCount: number; - error: string | null; -}; - -function OnStopView({ - file, - context, -}: { - file: RiveFile; - context: TestContext; -}) { - return ( - - { - context.ref = ref; - }, - }} - style={{ flex: 1 }} - file={file} - autoPlay={true} - fit={Fit.Contain} - onStop={() => { - context.stopCount++; - }} - onError={(e) => { - context.error = e.message; - }} - /> - - ); -} - -describe('RiveView onStop', () => { - (isExperimental ? it : it.skip)( - 'fires exactly once when playback comes to rest', - async () => { - const file = await RiveFileFactory.fromSource(RATING, undefined); - const context: TestContext = { ref: null, stopCount: 0, error: null }; - - await render(); - await waitFor(() => expect(context.stopCount).toBeGreaterThan(0), { - timeout: 10000, - }); - - // The render loop keeps running after the state machine settles; onStop - // must not fire again while the content stays at rest. - const samples: number[] = []; - for (let i = 0; i < 4; i++) { - await delay(1000); - samples.push(context.stopCount); - } - console.log(`onStop count progression: ${samples.join(', ')}`); - expect(context.error).toBeNull(); - expect(context.stopCount).toBe(1); - cleanup(); - } - ); - - (isExperimental ? it : it.skip)('does not fire for pause()', async () => { - const file = await RiveFileFactory.fromSource(BOUNCING_BALL, undefined); - const context: TestContext = { ref: null, stopCount: 0, error: null }; - - await render(); - await waitFor(() => expect(context.ref).not.toBeNull(), { timeout: 5000 }); - - await context.ref!.pause(); - await delay(1000); - expect(context.error).toBeNull(); - expect(context.stopCount).toBe(0); - cleanup(); - }); -}); diff --git a/example/assets/rive/finished_trigger.riv b/example/assets/rive/finished_trigger.riv new file mode 100644 index 00000000..101e6b2e Binary files /dev/null and b/example/assets/rive/finished_trigger.riv differ diff --git a/example/assets/rive/finished_trigger.riv.d.ts b/example/assets/rive/finished_trigger.riv.d.ts new file mode 100644 index 00000000..4efa6b05 --- /dev/null +++ b/example/assets/rive/finished_trigger.riv.d.ts @@ -0,0 +1,17 @@ +// Generated by rive-gen-types — do not edit manually. @generated +/* eslint-disable */ +// Source: finished_trigger.riv +import type { RiveAsset } from '@rive-app/react-native'; +declare const asset: RiveAsset<{ + artboards: 'Artboard'; + defaultArtboard: 'Artboard'; + stateMachines: { + Artboard: 'State Machine 1'; + }; + viewModels: { + ViewModel1: { + finished: 'trigger'; + }; + }; +}>; +export default asset; diff --git a/example/src/demos/FinishedTriggerDemo.tsx b/example/src/demos/FinishedTriggerDemo.tsx new file mode 100644 index 00000000..0e688274 --- /dev/null +++ b/example/src/demos/FinishedTriggerDemo.tsx @@ -0,0 +1,138 @@ +import { useRef, useState } from 'react'; +import { View, Text, Pressable, StyleSheet } from 'react-native'; +import { + RiveView, + useRive, + useRiveFile, + useRiveTrigger, + useViewModelInstance, + Fit, +} from '@rive-app/react-native'; +import type { Metadata } from '../shared/metadata'; + +// One-shot animation whose state machine fires the 'finished' view-model +// trigger via an exit-time transition action — the data-binding way to react +// to playback completing (e.g. navigate away from a splash screen). +export default function FinishedTriggerDemo() { + const { riveFile } = useRiveFile( + require('../../assets/rive/finished_trigger.riv') + ); + const { riveViewRef, setHybridRef } = useRive(); + const { instance } = useViewModelInstance(riveFile, { async: true }); + + const [runId, setRunId] = useState(1); + const [fireLog, setFireLog] = useState([]); + const mountedAt = useRef(Date.now()); + + useRiveTrigger('finished', instance, { + onTrigger: () => { + const elapsed = Date.now() - mountedAt.current; + setFireLog((log) => [ + `#${log.length + 1} fired ${elapsed} ms after (re)mount`, + ...log, + ]); + }, + }); + + const replay = () => { + mountedAt.current = Date.now(); + setRunId((id) => id + 1); + }; + + return ( + + + {riveFile && instance && ( + + )} + + + + {fireLog.length === 0 + ? 'Playing… waiting for finished trigger' + : `finished fired ${fireLog.length}×`} + + + + + Replay (remount) + + riveViewRef?.pause()}> + Pause + + riveViewRef?.play()}> + Play + + + + + {fireLog.map((entry) => ( + + {entry} + + ))} + + + ); +} + +FinishedTriggerDemo.metadata = { + name: 'Finished Trigger', + description: + 'View-model trigger fired when a one-shot animation completes (onStop replacement)', + order: 50, +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + }, + riveBox: { + height: 260, + borderRadius: 8, + overflow: 'hidden', + backgroundColor: '#f2f2f7', + }, + rive: { + flex: 1, + }, + status: { + marginTop: 12, + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + }, + buttons: { + flexDirection: 'row', + justifyContent: 'center', + gap: 8, + marginTop: 12, + }, + button: { + backgroundColor: '#007aff', + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 8, + }, + buttonText: { + color: 'white', + fontWeight: '600', + }, + log: { + marginTop: 16, + }, + logEntry: { + fontFamily: 'Menlo', + fontSize: 13, + paddingVertical: 2, + }, +}); diff --git a/example/src/reproducers/DeinitOffMain.tsx b/example/src/reproducers/DeinitOffMain.tsx index 12b3a908..9af8a867 100644 --- a/example/src/reproducers/DeinitOffMain.tsx +++ b/example/src/reproducers/DeinitOffMain.tsx @@ -42,7 +42,6 @@ function RiveContent({ raw }: { raw: boolean }) { style={styles.rive} autoPlay={true} onError={{ f: (e) => console.log('raw onError', e.message) }} - onStop={{ f: () => console.log('raw onStop') }} /> ); } diff --git a/ios/legacy/HybridRiveView.swift b/ios/legacy/HybridRiveView.swift index 87551008..de8f80d5 100644 --- a/ios/legacy/HybridRiveView.swift +++ b/ios/legacy/HybridRiveView.swift @@ -113,12 +113,6 @@ class HybridRiveView: HybridRiveViewSpec { // runtime (the experimental backend). var semantics: Semantics? var onError: (RiveError) -> Void = { _ in } - // Not wired on the legacy runtime: RivePlayerDelegate has no signal that - // reliably maps to "playback came to rest" here (stoppedWithModel only - // fires from an explicit stop(), which this wrapper never calls). The - // legacy backend is internal-testing-only, so this stays a no-op rather - // than reporting an incorrect stop. - var onStop: () -> Void = {} func awaitViewReady() throws -> Promise { return Promise.async { [self] in diff --git a/ios/new/HybridRiveView.swift b/ios/new/HybridRiveView.swift index c87091d5..394319b4 100644 --- a/ios/new/HybridRiveView.swift +++ b/ios/new/HybridRiveView.swift @@ -101,7 +101,6 @@ class HybridRiveView: HybridRiveViewSpec { var frameRate: Variant_Double_FrameRateRange? var semantics: Semantics? var onError: (RiveError) -> Void = { _ in } - var onStop: () -> Void = {} func awaitViewReady() throws -> Promise { return Promise.async { [self] in @@ -224,9 +223,6 @@ class HybridRiveView: HybridRiveViewSpec { riveView.onLoadError = { [weak self] message in self?.onError(RiveError(message: message, type: .unknown)) } - riveView.onSettled = { [weak self] in - self?.onStop() - } riveView.configure( config, dataBindingChanged: dataBindingChanged, reload: needsReload, initialUpdate: initialUpdate) diff --git a/ios/new/RiveReactNativeView.swift b/ios/new/RiveReactNativeView.swift index bd2c3baf..1f10b9c6 100644 --- a/ios/new/RiveReactNativeView.swift +++ b/ios/new/RiveReactNativeView.swift @@ -28,8 +28,6 @@ class RiveReactNativeView: UIView { private var viewReadyContinuations: [CheckedContinuation] = [] private var isViewReady = false private var configTask: Task? - private var settledTask: Task? - private var stopNotifyTask: Task? private var isPaused = false private var semantics: RiveRuntime.Semantics = RiveUIView.Constants.Defaults.semantics { didSet { riveUIView?.semantics = semantics } @@ -42,10 +40,6 @@ class RiveReactNativeView: UIView { /// Configure failures are reported here (wired to the onError prop). var onLoadError: ((String) -> Void)? - /// Fired whenever the state machine settles (reaches rest, e.g. a - /// non-looping animation reaching its end); wired to the onStop prop. - var onSettled: (() -> Void)? - func awaitViewReady() async -> Bool { if isViewReady { return true @@ -218,35 +212,7 @@ class RiveReactNativeView: UIView { // MARK: - Internal - private func observeSettled(of rive: RiveRuntime.Rive) { - settledTask?.cancel() - // A stop scheduled by the previous Rive instance must not fire into the - // new one after a reconfigure. - stopNotifyTask?.cancel() - stopNotifyTask = nil - settledTask = Task { [weak self] in - for await _ in rive.stateMachine.settledStream() { - guard !Task.isCancelled else { return } - self?.scheduleStopNotification() - } - } - } - - // The SDK can emit settle back-to-back right after configure (an initial - // rest, immediately perturbed by setup — e.g. data-bind — and re-settling), - // which would otherwise report two stops for a single, user-visible "came - // to rest". Coalesce a tight burst into one onStop call. - private func scheduleStopNotification() { - stopNotifyTask?.cancel() - stopNotifyTask = Task { [weak self] in - try? await Task.sleep(nanoseconds: 150_000_000) - guard !Task.isCancelled else { return } - self?.onSettled?() - } - } - private func setupRiveUIView(with rive: RiveRuntime.Rive) { - observeSettled(of: rive) if let existing = riveUIView { // Reuse the existing view — avoids tearing down the MTKView on every // reconfigure, which previously caused orphaned draw calls ("state machine @@ -275,10 +241,6 @@ class RiveReactNativeView: UIView { dispatchPrecondition(condition: .onQueue(.main)) configTask?.cancel() configTask = nil - settledTask?.cancel() - settledTask = nil - stopNotifyTask?.cancel() - stopNotifyTask = nil riveUIView?.removeFromSuperview() riveUIView = nil riveInstance = nil @@ -299,19 +261,13 @@ class RiveReactNativeView: UIView { // main thread (JS/GC thread); cleanup() must run on main. Capture the // resources, not self. let task = configTask - let settled = settledTask - let stopNotify = stopNotifyTask let uiView = riveUIView if Thread.isMainThread { task?.cancel() - settled?.cancel() - stopNotify?.cancel() uiView?.removeFromSuperview() } else { DispatchQueue.main.async { task?.cancel() - settled?.cancel() - stopNotify?.cancel() uiView?.removeFromSuperview() } } diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp index ee2ed1ce..b6827bc7 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp @@ -61,7 +61,6 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include "JRiveError.hpp" #include "RiveErrorType.hpp" #include "JRiveErrorType.hpp" -#include "JFunc_void.hpp" #include #include #include @@ -210,23 +209,6 @@ namespace margelo::nitro::rive { static const auto method = _javaPart->javaClassStatic()->getMethod /* onError */)>("setOnError_cxx"); method(_javaPart, JFunc_void_RiveError_cxx::fromCpp(onError)); } - std::function JHybridRiveViewSpec::getOnStop() { - static const auto method = _javaPart->javaClassStatic()->getMethod()>("getOnStop_cxx"); - auto __result = method(_javaPart); - return [&]() -> std::function { - if (__result->isInstanceOf(JFunc_void_cxx::javaClassStatic())) [[likely]] { - auto downcast = jni::static_ref_cast(__result); - return downcast->cthis()->getFunction(); - } else { - auto __resultRef = jni::make_global(__result); - return JNICallable(std::move(__resultRef)); - } - }(); - } - void JHybridRiveViewSpec::setOnStop(const std::function& onStop) { - static const auto method = _javaPart->javaClassStatic()->getMethod /* onStop */)>("setOnStop_cxx"); - method(_javaPart, JFunc_void_cxx::fromCpp(onStop)); - } // Methods std::shared_ptr> JHybridRiveViewSpec::awaitViewReady() { diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp index 91ea8d5c..fc8641bc 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp @@ -72,8 +72,6 @@ namespace margelo::nitro::rive { void setDataBind(const std::optional, DataBindMode, DataBindByName>>& dataBind) override; std::function getOnError() override; void setOnError(const std::function& onError) override; - std::function getOnStop() override; - void setOnStop(const std::function& onStop) override; public: // Methods diff --git a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp index ecfe8860..e003051d 100644 --- a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp +++ b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp @@ -81,10 +81,6 @@ void JHybridRiveViewStateUpdater::updateViewProps(jni::alias_ref /* hybridView->setOnError(props->onError.value); props->onError.isDirty = false; } - if (props->onStop.isDirty) { - hybridView->setOnStop(props->onStop.value); - props->onStop.isDirty = false; - } // Update hybridRef if it changed if (props->hybridRef.isDirty) { diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt index a3e782de..be540182 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt @@ -100,20 +100,6 @@ abstract class HybridRiveViewSpec: HybridView() { set(value) { onError = value } - - abstract var onStop: () -> Unit - - private var onStop_cxx: Func_void - @Keep - @DoNotStrip - get() { - return Func_void_java(onStop) - } - @Keep - @DoNotStrip - set(value) { - onStop = value - } // Methods @DoNotStrip diff --git a/nitrogen/generated/android/riveOnLoad.cpp b/nitrogen/generated/android/riveOnLoad.cpp index f8166a32..e98e5b0c 100644 --- a/nitrogen/generated/android/riveOnLoad.cpp +++ b/nitrogen/generated/android/riveOnLoad.cpp @@ -27,13 +27,13 @@ #include "JHybridRiveRuntimeSpec.hpp" #include "JHybridRiveViewSpec.hpp" #include "JFunc_void_RiveError.hpp" -#include "JFunc_void.hpp" #include "JFunc_void_UnifiedRiveEvent.hpp" #include "views/JHybridRiveViewStateUpdater.hpp" #include "JHybridViewModelSpec.hpp" #include "JHybridViewModelInstanceSpec.hpp" #include "JHybridViewModelPropertySpec.hpp" #include "JHybridViewModelNumberPropertySpec.hpp" +#include "JFunc_void.hpp" #include "JFunc_void_double.hpp" #include "JHybridViewModelStringPropertySpec.hpp" #include "JFunc_void_std__string.hpp" @@ -129,13 +129,13 @@ void registerAllNatives() { margelo::nitro::rive::JHybridRiveRuntimeSpec::CxxPart::registerNatives(); margelo::nitro::rive::JHybridRiveViewSpec::CxxPart::registerNatives(); margelo::nitro::rive::JFunc_void_RiveError_cxx::registerNatives(); - margelo::nitro::rive::JFunc_void_cxx::registerNatives(); margelo::nitro::rive::JFunc_void_UnifiedRiveEvent_cxx::registerNatives(); margelo::nitro::rive::views::JHybridRiveViewStateUpdater::registerNatives(); margelo::nitro::rive::JHybridViewModelSpec::CxxPart::registerNatives(); margelo::nitro::rive::JHybridViewModelInstanceSpec::CxxPart::registerNatives(); margelo::nitro::rive::JHybridViewModelPropertySpec::CxxPart::registerNatives(); margelo::nitro::rive::JHybridViewModelNumberPropertySpec::CxxPart::registerNatives(); + margelo::nitro::rive::JFunc_void_cxx::registerNatives(); margelo::nitro::rive::JFunc_void_double_cxx::registerNatives(); margelo::nitro::rive::JHybridViewModelStringPropertySpec::CxxPart::registerNatives(); margelo::nitro::rive::JFunc_void_std__string_cxx::registerNatives(); diff --git a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp index 167b1ee5..b3c0dfe7 100644 --- a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp @@ -180,13 +180,6 @@ namespace margelo::nitro::rive { inline void setOnError(const std::function& onError) noexcept override { _swiftPart.setOnError(onError); } - inline std::function getOnStop() noexcept override { - auto __result = _swiftPart.getOnStop(); - return __result; - } - inline void setOnStop(const std::function& onStop) noexcept override { - _swiftPart.setOnStop(onStop); - } public: // Methods diff --git a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm index 03fdc83e..f1669805 100644 --- a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm +++ b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm @@ -127,11 +127,6 @@ - (void) updateProps:(const std::shared_ptr&)props swiftPart.setOnError(newViewProps.onError.value); newViewProps.onError.isDirty = false; } - // onStop: function - if (newViewProps.onStop.isDirty) { - swiftPart.setOnStop(newViewProps.onStop.value); - newViewProps.onStop.isDirty = false; - } swiftPart.afterUpdate(); diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift index cdb1a81f..d2e38de6 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift @@ -21,7 +21,6 @@ public protocol HybridRiveViewSpec_protocol: HybridObject, HybridView { var semantics: Semantics? { get set } var dataBind: Variant__any_HybridViewModelInstanceSpec__DataBindMode_DataBindByName? { get set } var onError: (_ error: RiveError) -> Void { get set } - var onStop: () -> Void { get set } // Methods func awaitViewReady() throws -> Promise diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift index bba3dd02..bb9ead71 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift @@ -402,25 +402,6 @@ open class HybridRiveViewSpec_cxx { }() } } - - public final var onStop: bridge.Func_void { - @inline(__always) - get { - return { () -> bridge.Func_void in - let __closureWrapper = Func_void(self.__implementation.onStop) - return bridge.create_Func_void(__closureWrapper.toUnsafe()) - }() - } - @inline(__always) - set { - self.__implementation.onStop = { () -> () -> Void in - let __wrappedFunction = bridge.wrap_Func_void(newValue) - return { () -> Void in - __wrappedFunction.call() - } - }() - } - } // Methods @inline(__always) diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp index 25276fca..dadb86eb 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp @@ -36,8 +36,6 @@ namespace margelo::nitro::rive { prototype.registerHybridSetter("dataBind", &HybridRiveViewSpec::setDataBind); prototype.registerHybridGetter("onError", &HybridRiveViewSpec::getOnError); prototype.registerHybridSetter("onError", &HybridRiveViewSpec::setOnError); - prototype.registerHybridGetter("onStop", &HybridRiveViewSpec::getOnStop); - prototype.registerHybridSetter("onStop", &HybridRiveViewSpec::setOnStop); prototype.registerHybridMethod("awaitViewReady", &HybridRiveViewSpec::awaitViewReady); prototype.registerHybridMethod("bindViewModelInstance", &HybridRiveViewSpec::bindViewModelInstance); prototype.registerHybridMethod("getViewModelInstance", &HybridRiveViewSpec::getViewModelInstance); diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp index 7fb6ab7b..b6bce968 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp @@ -100,8 +100,6 @@ namespace margelo::nitro::rive { virtual void setDataBind(const std::optional, DataBindMode, DataBindByName>>& dataBind) = 0; virtual std::function getOnError() = 0; virtual void setOnError(const std::function& onError) = 0; - virtual std::function getOnStop() = 0; - virtual void setOnStop(const std::function& onStop) = 0; public: // Methods diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp index 82757d46..570176df 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp @@ -145,16 +145,6 @@ namespace margelo::nitro::rive::views { throw std::runtime_error(std::string("RiveView.onError: ") + exc.what()); } }()), - onStop([&]() -> CachedProp> { - try { - const react::RawValue* rawValue = rawProps.at("onStop", nullptr, nullptr); - if (rawValue == nullptr) return sourceProps.onStop; - const auto& [runtime, value] = (std::pair)*rawValue; - return CachedProp>::fromRawValue(*runtime, value.asObject(*runtime).getProperty(*runtime, PropNameIDCache::get(*runtime, "f")), sourceProps.onStop); - } catch (const std::exception& exc) { - throw std::runtime_error(std::string("RiveView.onStop: ") + exc.what()); - } - }()), hybridRef([&]() -> CachedProp& /* ref */)>>> { try { const react::RawValue* rawValue = rawProps.at("hybridRef", nullptr, nullptr); @@ -179,7 +169,6 @@ namespace margelo::nitro::rive::views { case hashString("semantics"): return true; case hashString("dataBind"): return true; case hashString("onError"): return true; - case hashString("onStop"): return true; case hashString("hybridRef"): return true; default: return false; } diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp index 27d708de..564ffcc9 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp @@ -63,7 +63,6 @@ namespace margelo::nitro::rive::views { CachedProp> semantics; CachedProp, DataBindMode, DataBindByName>>> dataBind; CachedProp> onError; - CachedProp> onStop; CachedProp& /* ref */)>>> hybridRef; private: diff --git a/nitrogen/generated/shared/json/RiveViewConfig.json b/nitrogen/generated/shared/json/RiveViewConfig.json index bed6f54b..1283b0d9 100644 --- a/nitrogen/generated/shared/json/RiveViewConfig.json +++ b/nitrogen/generated/shared/json/RiveViewConfig.json @@ -15,7 +15,6 @@ "semantics": true, "dataBind": true, "onError": true, - "onStop": true, "hybridRef": true } } diff --git a/src/core/RiveView.tsx b/src/core/RiveView.tsx index 15e07fda..137e1218 100644 --- a/src/core/RiveView.tsx +++ b/src/core/RiveView.tsx @@ -21,16 +21,9 @@ export interface RiveViewProps< A extends SchemaOf['artboards'] = SchemaOf['defaultArtboard'], > extends Omit< NitroRiveViewProps, - 'onError' | 'onStop' | 'file' | 'artboardName' | 'stateMachineName' + 'onError' | 'file' | 'artboardName' | 'stateMachineName' > { onError?: (error: RiveError) => void; - /** - * Called when the animation/state machine stops playing, e.g. when a - * non-looping animation reaches its end. Not called for pause() — only - * when playback naturally comes to rest. Useful for splash-screen-style - * animations where you want to navigate away once playback finishes. - */ - onStop?: () => void; file: TypedRiveFile; /** Name of the artboard to display. When using a generated schema, only valid artboard names are accepted. */ artboardName?: A; @@ -44,8 +37,6 @@ export interface RiveViewProps< const defaultOnError = (error: RiveError) => console.error(`[${RiveErrorType[error.type]}] ${error.message}`); -const defaultOnStop = () => {}; - /** * RiveView is a React Native component that renders Rive graphics. * It provides a seamless way to display and control Rive graphics in your app. @@ -72,7 +63,6 @@ const defaultOnStop = () => {}; * @property {number | FrameRateRange} [frameRate] - Preferred frame rate for the render loop (new runtimes only) * @property {Object} [style] - React Native style object for container customization * @property {(error: RiveError) => void} [onError] - Callback function that is called when an error occurs - * @property {() => void} [onStop] - Callback function that is called when the animation/state machine stops playing (e.g. reaches the end of a non-looping animation) * * The component also exposes methods for controlling playback: * - play(): Starts playing the Rive graphic @@ -82,9 +72,8 @@ export function RiveView< T extends RiveFileSchema | RiveAsset = RiveFileSchema, A extends SchemaOf['artboards'] = SchemaOf['defaultArtboard'], >(props: RiveViewProps) { - const { onError, onStop, hybridRef: userHybridRef, ...rest } = props; + const { onError, hybridRef: userHybridRef, ...rest } = props; const wrappedOnError = onError ?? defaultOnError; - const wrappedOnStop = onStop ?? defaultOnStop; const viewRef = useRef(null); useEffect(() => { @@ -107,7 +96,6 @@ export function RiveView< ); diff --git a/src/specs/RiveView.nitro.ts b/src/specs/RiveView.nitro.ts index 25d7b3dc..f26af654 100644 --- a/src/specs/RiveView.nitro.ts +++ b/src/specs/RiveView.nitro.ts @@ -77,12 +77,6 @@ export interface RiveViewProps extends HybridViewProps { dataBind?: ViewModelInstance | DataBindMode | DataBindByName; /** Callback function that is called when an error occurs */ onError: (error: RiveError) => void; - /** - * Callback function that is called when the animation/state machine stops - * playing, e.g. when a non-looping animation reaches its end. Not called - * for pause() — only when playback naturally comes to rest. - */ - onStop: () => void; } /**