From ec62e664819b8d48a42a24f8c1bf8a96c7e1d9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 30 Jul 2026 16:15:02 +0200 Subject: [PATCH 1/2] feat: offscreenBehavior and renderEnabled props to cut offscreen CPU cost Android keeps rendering Rive views at full rate while they are scrolled out of the viewport or covered. offscreenBehavior ('none' | 'skip-draws' | 'pause', default 'none') handles visibility the view can detect itself: 'skip-draws' keeps advancing the state machine (events and data binding stay live) and only skips draws, 'pause' stops advance and draw. renderEnabled (default true) is the manual counterpart for occlusion the view cannot detect, e.g. a React Native Modal covering it: false skips draws while the state machine keeps advancing. Android (new backend) gates the Choreographer loop on isShown + getGlobalVisibleRect; the skip fast-path keeps the timebase fresh so resuming advances by one frame. iOS (new backend) combines the user pause state with a low-frequency visibility poll into RiveUIView .isPaused; since the upstream runtime couples advancing and drawing, 'skip-draws' and renderEnabled=false degrade to 'none' on iOS ('pause' is fully supported). Legacy backends accept and ignore both props. Emulator (Pixel 6, API 34): offscreen-scrolled 31% of a core with 'none', 5.7% with 'skip-draws', 4.5% with 'pause'; modal-covered 44.7% vs 5.2% with renderEnabled=false; paused/unmounted reference ~4%. Onscreen cost is unchanged. The 'Offscreen behavior' example page drives all scenarios. --- .../com/margelo/nitro/rive/HybridRiveView.kt | 5 + .../com/margelo/nitro/rive/HybridRiveView.kt | 12 + .../new/java/com/rive/RiveReactNativeView.kt | 46 ++- example/src/reproducers/OffscreenBehavior.tsx | 270 ++++++++++++++++++ ios/legacy/HybridRiveView.swift | 4 + ios/new/HybridRiveView.swift | 5 + ios/new/RiveReactNativeView.swift | 78 ++++- .../android/c++/JHybridRiveViewSpec.cpp | 22 ++ .../android/c++/JHybridRiveViewSpec.hpp | 4 + .../android/c++/JOffscreenBehavior.hpp | 61 ++++ .../c++/views/JHybridRiveViewStateUpdater.cpp | 8 + .../margelo/nitro/rive/HybridRiveViewSpec.kt | 12 + .../margelo/nitro/rive/OffscreenBehavior.kt | 24 ++ .../generated/ios/RNRive-Swift-Cxx-Bridge.hpp | 18 ++ .../ios/RNRive-Swift-Cxx-Umbrella.hpp | 3 + .../ios/c++/HybridRiveViewSpecSwift.hpp | 17 ++ .../ios/c++/views/HybridRiveViewComponent.mm | 10 + .../ios/swift/HybridRiveViewSpec.swift | 2 + .../ios/swift/HybridRiveViewSpec_cxx.swift | 41 +++ .../ios/swift/OffscreenBehavior.swift | 44 +++ .../shared/c++/HybridRiveViewSpec.cpp | 4 + .../shared/c++/HybridRiveViewSpec.hpp | 7 + .../shared/c++/OffscreenBehavior.hpp | 80 ++++++ .../c++/views/HybridRiveViewComponent.cpp | 24 ++ .../c++/views/HybridRiveViewComponent.hpp | 3 + .../generated/shared/json/RiveViewConfig.json | 2 + src/core/RiveView.tsx | 2 + src/index.tsx | 2 +- src/specs/RiveView.nitro.ts | 39 +++ 29 files changed, 836 insertions(+), 13 deletions(-) create mode 100644 example/src/reproducers/OffscreenBehavior.tsx create mode 100644 nitrogen/generated/android/c++/JOffscreenBehavior.hpp create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/rive/OffscreenBehavior.kt create mode 100644 nitrogen/generated/ios/swift/OffscreenBehavior.swift create mode 100644 nitrogen/generated/shared/c++/OffscreenBehavior.hpp 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..982957d0 100644 --- a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -94,6 +94,11 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { // experimental backends. override var frameRate: Variant_Double_FrameRateRange? = null + // Accepted for API parity; offscreen handling and draw skipping are only + // implemented on the new runtimes. + override var offscreenBehavior: OffscreenBehavior? = null + override var renderEnabled: Boolean? = null + // Accepted for API parity; semantics are only available in the new Rive // runtime, and Android support is pending upstream (iOS-only for now). override var semantics: Semantics? = null 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..e51740c4 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -111,6 +111,18 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { ) } + override var offscreenBehavior: OffscreenBehavior? = null + set(value) { + field = value + view.offscreenBehavior = value ?: OffscreenBehavior.NONE + } + + override var renderEnabled: Boolean? = null + set(value) { + field = value + view.renderEnabled = value ?: true + } + // Accepted for API parity; semantics support is pending in the upstream // rive-android runtime (iOS-only for now). override var semantics: Semantics? = null diff --git a/android/src/new/java/com/rive/RiveReactNativeView.kt b/android/src/new/java/com/rive/RiveReactNativeView.kt index dcfdf423..87b5844b 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -1,6 +1,7 @@ package com.rive import android.annotation.SuppressLint +import android.graphics.Rect import android.graphics.SurfaceTexture import android.os.Build import android.util.Log @@ -21,6 +22,7 @@ import app.rive.core.RiveSurface import app.rive.core.StateMachineHandle import app.rive.core.SurfaceTextureSurface import com.facebook.react.uimanager.ThemedReactContext +import com.margelo.nitro.rive.OffscreenBehavior import com.margelo.nitro.rive.RiveErrorLogger import com.margelo.nitro.rive.RiveLog import kotlinx.coroutines.CompletableDeferred @@ -71,6 +73,22 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { updateFrameRateHint() } + // What to do while the view is outside the visible viewport (scrolled out, + // hidden, or windowless): keep running, skip draws only, or pause fully. + // Never automatic — data-binding consumers may rely on the state machine + // advancing while offscreen. Overlays in the same window (e.g. RN Modal) + // don't count as covering the view. + var offscreenBehavior: OffscreenBehavior = OffscreenBehavior.NONE + + // Manual counterpart to offscreenBehavior for occlusion the view can't + // detect (RN Modal, bottom sheets): false = skip draws, keep advancing. + var renderEnabled: Boolean = true + + private val visibleRectBuffer = Rect() + + private fun isInVisibleViewport(): Boolean = + isShown && getGlobalVisibleRect(visibleRectBuffer) + var onError: ((String) -> Unit)? = null // Fired when the state machine settles (reaches rest, e.g. a non-looping @@ -172,7 +190,10 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { override fun doFrame(frameTimeNanos: Long) { if (!renderLoopRunning || disposed) return - if (paused && !needsRedraw) { + val offscreen = offscreenBehavior != OffscreenBehavior.NONE && !isInVisibleViewport() + val pausedOffscreen = offscreen && offscreenBehavior == OffscreenBehavior.PAUSE + + if ((paused || pausedOffscreen) && !needsRedraw) { // Keep the timebase fresh so resuming advances by one frame, not by // the whole pause span. lastFrameTimeNs = frameTimeNanos @@ -204,17 +225,26 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { val sm = stateMachineHandle val rs = riveSurface + // Offscreen views keep advancing the state machine (events and + // data-binding listeners stay live) and only skip the draw — the draw + // is the dominant part of the offscreen cost. needsRedraw still forces + // a draw so pending content (initial frame, resize, rebinding) isn't + // lost while invisible. + val skipDraw = !renderEnabled || offscreen + if (worker != null && art != null && sm != null && rs != null) { try { - if (!paused && !settled) { + if (!paused && !settled && !pausedOffscreen) { worker.advanceStateMachine(sm, deltaTime) } - worker.draw(art, sm, rs, activeFit) - needsRedraw = false - frameCount++ - val isFirstFrame = frameCount == 1L - if (isFirstFrame) { - viewReadyDeferred.complete(true) + if (!skipDraw || needsRedraw) { + worker.draw(art, sm, rs, activeFit) + needsRedraw = false + frameCount++ + val isFirstFrame = frameCount == 1L + if (isFirstFrame) { + viewReadyDeferred.complete(true) + } } } catch (e: Exception) { Log.e(TAG, "Render loop error", e) diff --git a/example/src/reproducers/OffscreenBehavior.tsx b/example/src/reproducers/OffscreenBehavior.tsx new file mode 100644 index 00000000..449f719f --- /dev/null +++ b/example/src/reproducers/OffscreenBehavior.tsx @@ -0,0 +1,270 @@ +import { useRef, useState } from 'react'; +import { + View, + Text, + StyleSheet, + Pressable, + ScrollView, + Modal, +} from 'react-native'; +import { + RiveView, + useRiveFile, + Fit, + type OffscreenBehavior, + type RiveViewRef, +} from '@rive-app/react-native'; +import { type Metadata } from '../shared/metadata'; + +/** + * Manual verifier for offscreenBehavior and renderEnabled (issue #332 + * follow-up). + * + * Puts a looping animation into one of several visibility states so process + * CPU can be sampled externally (e.g. per-thread /proc//task stats on + * Android): + * + * - onscreen: playing, fully visible at the top of the ScrollView + * - offscreen: playing, scrolled fully out of the viewport (still mounted) + * - modal: playing, covered by a full-screen RN Modal + * - paused: pause() via ref, still visible + * - unmounted: RiveView removed from the tree + * + * With offscreenBehavior 'skip-draws' or 'pause' the offscreen scenario + * should drop close to the paused cost; scrolling back must resume the + * animation. The modal scenario is invisible to automatic detection — the + * renderEnabled←modal toggle wires renderEnabled to it, which is the pattern + * that prop exists for. + */ + +type Scenario = 'onscreen' | 'offscreen' | 'modal' | 'paused' | 'unmounted'; + +const SCENARIOS: { key: Scenario; label: string }[] = [ + { key: 'onscreen', label: 'Onscreen' }, + { key: 'offscreen', label: 'Offscreen (scrolled)' }, + { key: 'modal', label: 'Covered (modal)' }, + { key: 'paused', label: 'Paused' }, + { key: 'unmounted', label: 'Unmounted' }, +]; + +const BEHAVIORS: OffscreenBehavior[] = ['none', 'skip-draws', 'pause']; + +export default function OffscreenBehaviorPage() { + const [scenario, setScenario] = useState('onscreen'); + const [behavior, setBehavior] = useState('none'); + const [wireRenderEnabled, setWireRenderEnabled] = useState(false); + const viewRef = useRef(null); + const scrollRef = useRef(null); + const { riveFile } = useRiveFile(require('../../assets/rive/rewards.riv')); + + const mounted = scenario !== 'unmounted'; + + const applyScenario = (next: Scenario) => { + const prev = scenario; + setScenario(next); + if (prev === 'paused' && next !== 'paused') { + viewRef.current?.play(); + } + if (next === 'paused') { + viewRef.current?.pause(); + } + requestAnimationFrame(() => { + scrollRef.current?.scrollTo({ + y: next === 'offscreen' ? 1200 : 0, + animated: false, + }); + }); + }; + + return ( + + + {scenario.toUpperCase()} · {behavior} + {wireRenderEnabled ? ' +renderEnabled←modal' : ''} + + + {SCENARIOS.map(({ key, label }) => ( + applyScenario(key)} + > + + {label} + + + ))} + + + {BEHAVIORS.map((value) => ( + setBehavior(value)} + > + + {value} + + + ))} + setWireRenderEnabled((v) => !v)} + > + + renderEnabled←modal + + + + + + {mounted && riveFile ? ( + (viewRef.current = ref) }} + style={styles.rive} + /> + ) : ( + + {mounted ? 'Loading…' : 'Unmounted'} + + )} + + + + Scrolled content — the Rive view is above the viewport + + + + applyScenario('onscreen')} + > + + Covering modal + + The Rive view is mounted and playing underneath this modal. + + applyScenario('onscreen')} + > + Close + + + + + ); +} + +OffscreenBehaviorPage.metadata = { + name: 'Offscreen behavior', + description: + 'CPU cost of a playing looping animation when offscreen/covered, and the offscreenBehavior/renderEnabled props that reduce it (issue #332)', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + status: { + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + paddingVertical: 8, + backgroundColor: '#222', + color: '#0f0', + }, + buttonRow: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 6, + paddingHorizontal: 8, + paddingVertical: 4, + }, + button: { + paddingHorizontal: 10, + paddingVertical: 8, + backgroundColor: '#eee', + borderRadius: 8, + }, + buttonActive: { + backgroundColor: '#007AFF', + }, + buttonText: { + color: '#333', + fontWeight: '600', + fontSize: 12, + }, + buttonTextActive: { + color: '#fff', + }, + scroll: { + flex: 1, + }, + riveContainer: { + height: 300, + alignItems: 'center', + justifyContent: 'center', + }, + rive: { + width: 300, + height: 300, + }, + loading: { + fontSize: 16, + color: '#666', + }, + spacer: { + height: 1600, + alignItems: 'center', + paddingTop: 400, + }, + spacerText: { + color: '#999', + paddingHorizontal: 24, + textAlign: 'center', + }, + modalContent: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + gap: 12, + backgroundColor: '#fff', + }, + modalTitle: { + fontSize: 20, + fontWeight: 'bold', + }, + modalText: { + color: '#666', + paddingHorizontal: 32, + textAlign: 'center', + }, +}); diff --git a/ios/legacy/HybridRiveView.swift b/ios/legacy/HybridRiveView.swift index 87551008..e8e2a5c1 100644 --- a/ios/legacy/HybridRiveView.swift +++ b/ios/legacy/HybridRiveView.swift @@ -109,6 +109,10 @@ class HybridRiveView: HybridRiveViewSpec { // Accepted for API parity; frame-rate control is only implemented on the // experimental backends. var frameRate: Variant_Double_FrameRateRange? + // Accepted for API parity; offscreen handling and draw skipping are only + // implemented on the new runtimes. + var offscreenBehavior: OffscreenBehavior? + var renderEnabled: Bool? // Accepted for API parity; semantics are only available in the new Rive // runtime (the experimental backend). var semantics: Semantics? diff --git a/ios/new/HybridRiveView.swift b/ios/new/HybridRiveView.swift index c87091d5..46a77bae 100644 --- a/ios/new/HybridRiveView.swift +++ b/ios/new/HybridRiveView.swift @@ -99,6 +99,10 @@ class HybridRiveView: HybridRiveViewSpec { var fit: Fit? var layoutScaleFactor: Double? var frameRate: Variant_Double_FrameRateRange? + var offscreenBehavior: OffscreenBehavior? + // Accepted for API parity; the upstream iOS runtime couples advancing and + // drawing, so draw-only skipping isn't implementable yet. + var renderEnabled: Bool? var semantics: Semantics? var onError: (RiveError) -> Void = { _ in } var onStop: () -> Void = {} @@ -216,6 +220,7 @@ class HybridRiveView: HybridRiveViewSpec { fit: toRiveFit(fit, alignment: alignment, layoutScaleFactor: layoutScaleFactor), semantics: toRiveSemantics(semantics), frameRate: toRiveFrameRate(frameRate), + offscreenBehavior: offscreenBehavior ?? .none, bindData: try dataBind.toBindData() ) diff --git a/ios/new/RiveReactNativeView.swift b/ios/new/RiveReactNativeView.swift index bd2c3baf..da5bc1ba 100644 --- a/ios/new/RiveReactNativeView.swift +++ b/ios/new/RiveReactNativeView.swift @@ -17,6 +17,7 @@ struct ViewConfiguration { let fit: RiveRuntime.Fit let semantics: RiveRuntime.Semantics let frameRate: RiveRuntime.FrameRate + let offscreenBehavior: OffscreenBehavior let bindData: BindData } @@ -39,6 +40,68 @@ class RiveReactNativeView: UIView { } var autoPlay: Bool = true + // The upstream runtime couples advancing and drawing behind a single + // isPaused flag, so only .pause is implementable here — .skipDraws behaves + // like .none (iOS already throttles most offscreen rendering on its own). + // Never automatic — data-binding consumers may rely on the state machine + // advancing while offscreen. Scrolling produces no callback on this view, + // so visibility is polled on a low-frequency timer that only runs while + // opted in. + private var offscreenBehavior: OffscreenBehavior = .none { + didSet { + guard offscreenBehavior != oldValue else { return } + updateVisibilityPolling() + } + } + private var isOffscreen = false { + didSet { + guard isOffscreen != oldValue else { return } + applyPauseState() + } + } + private var visibilityTimer: Timer? + + private func updateVisibilityPolling() { + visibilityTimer?.invalidate() + visibilityTimer = nil + guard offscreenBehavior == .pause else { + isOffscreen = false + return + } + guard window != nil else { + isOffscreen = true + return + } + isOffscreen = !isInVisibleViewport() + let timer = Timer(timeInterval: 0.25, repeats: true) { [weak self] _ in + MainActor.assumeIsolated { + guard let self else { return } + self.isOffscreen = !self.isInVisibleViewport() + } + } + // .common so polling continues while the user is dragging a scroll view — + // that's exactly when visibility changes. + RunLoop.main.add(timer, forMode: .common) + visibilityTimer = timer + } + + private func isInVisibleViewport() -> Bool { + guard let window, !isHidden, alpha > 0, bounds.width > 0, bounds.height > 0 else { + return false + } + let frameInWindow = convert(bounds, to: window) + return frameInWindow.intersects(window.bounds) + } + + private func applyPauseState() { + riveUIView?.isPaused = isPaused || (offscreenBehavior == .pause && isOffscreen) + } + + override func didMoveToWindow() { + super.didMoveToWindow() + updateVisibilityPolling() + } + /// Configure failures are reported here (wired to the onError prop). var onLoadError: ((String) -> Void)? @@ -68,6 +131,7 @@ class RiveReactNativeView: UIView { semantics = config.semantics frameRate = config.frameRate + offscreenBehavior = config.offscreenBehavior if reload { cleanup() @@ -168,12 +232,12 @@ class RiveReactNativeView: UIView { func play() { isPaused = false - riveUIView?.isPaused = false + applyPauseState() } func pause() { isPaused = true - riveUIView?.isPaused = true + applyPauseState() } func reset() { @@ -184,7 +248,7 @@ class RiveReactNativeView: UIView { func playIfNeeded() { if isPaused { isPaused = false - riveUIView?.isPaused = false + applyPauseState() } } @@ -252,9 +316,9 @@ class RiveReactNativeView: UIView { // reconfigure, which previously caused orphaned draw calls ("state machine // not found") from the old MTKView after removeFromSuperview. existing.rive = rive - existing.isPaused = isPaused existing.semantics = semantics existing.frameRate = frameRate + applyPauseState() } else { let uiView = RiveUIView(rive: rive, isPaused: isPaused) uiView.semantics = semantics @@ -268,6 +332,7 @@ class RiveReactNativeView: UIView { uiView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) self.riveUIView = uiView + applyPauseState() } } @@ -290,6 +355,8 @@ class RiveReactNativeView: UIView { /// awaitViewReady() callers so their promises (which retain this view) /// don't hang forever. func detach() { + visibilityTimer?.invalidate() + visibilityTimer = nil resumeViewReadyContinuations(false) cleanup() } @@ -302,13 +369,16 @@ class RiveReactNativeView: UIView { let settled = settledTask let stopNotify = stopNotifyTask let uiView = riveUIView + let timer = visibilityTimer if Thread.isMainThread { + timer?.invalidate() task?.cancel() settled?.cancel() stopNotify?.cancel() uiView?.removeFromSuperview() } else { DispatchQueue.main.async { + timer?.invalidate() task?.cancel() settled?.cancel() stopNotify?.cancel() diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp index ee2ed1ce..962d34a3 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp @@ -15,6 +15,8 @@ namespace margelo::nitro::rive { enum class Alignment; } namespace margelo::nitro::rive { enum class Fit; } // Forward declaration of `FrameRateRange` to properly resolve imports. namespace margelo::nitro::rive { struct FrameRateRange; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -45,6 +47,8 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include #include "JVariant_Double_FrameRateRange.hpp" #include "JFrameRateRange.hpp" +#include "OffscreenBehavior.hpp" +#include "JOffscreenBehavior.hpp" #include "Semantics.hpp" #include "JSemantics.hpp" #include "HybridViewModelInstanceSpec.hpp" @@ -175,6 +179,24 @@ namespace margelo::nitro::rive { static const auto method = _javaPart->javaClassStatic()->getMethod /* frameRate */)>("setFrameRate"); method(_javaPart, frameRate.has_value() ? JVariant_Double_FrameRateRange::fromCpp(frameRate.value()) : nullptr); } + std::optional JHybridRiveViewSpec::getOffscreenBehavior() { + static const auto method = _javaPart->javaClassStatic()->getMethod()>("getOffscreenBehavior"); + auto __result = method(_javaPart); + return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt; + } + void JHybridRiveViewSpec::setOffscreenBehavior(std::optional offscreenBehavior) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* offscreenBehavior */)>("setOffscreenBehavior"); + method(_javaPart, offscreenBehavior.has_value() ? JOffscreenBehavior::fromCpp(offscreenBehavior.value()) : nullptr); + } + std::optional JHybridRiveViewSpec::getRenderEnabled() { + static const auto method = _javaPart->javaClassStatic()->getMethod()>("getRenderEnabled"); + auto __result = method(_javaPart); + return __result != nullptr ? std::make_optional(static_cast(__result->value())) : std::nullopt; + } + void JHybridRiveViewSpec::setRenderEnabled(std::optional renderEnabled) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* renderEnabled */)>("setRenderEnabled"); + method(_javaPart, renderEnabled.has_value() ? jni::JBoolean::valueOf(renderEnabled.value()) : nullptr); + } std::optional JHybridRiveViewSpec::getSemantics() { static const auto method = _javaPart->javaClassStatic()->getMethod()>("getSemantics"); auto __result = method(_javaPart); diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp index 91ea8d5c..fab11cff 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp @@ -66,6 +66,10 @@ namespace margelo::nitro::rive { void setLayoutScaleFactor(std::optional layoutScaleFactor) override; std::optional> getFrameRate() override; void setFrameRate(const std::optional>& frameRate) override; + std::optional getOffscreenBehavior() override; + void setOffscreenBehavior(std::optional offscreenBehavior) override; + std::optional getRenderEnabled() override; + void setRenderEnabled(std::optional renderEnabled) override; std::optional getSemantics() override; void setSemantics(std::optional semantics) override; std::optional, DataBindMode, DataBindByName>> getDataBind() override; diff --git a/nitrogen/generated/android/c++/JOffscreenBehavior.hpp b/nitrogen/generated/android/c++/JOffscreenBehavior.hpp new file mode 100644 index 00000000..f54b6a8d --- /dev/null +++ b/nitrogen/generated/android/c++/JOffscreenBehavior.hpp @@ -0,0 +1,61 @@ +/// +/// JOffscreenBehavior.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "OffscreenBehavior.hpp" + +namespace margelo::nitro::rive { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "OffscreenBehavior" and the the Kotlin enum "OffscreenBehavior". + */ + struct JOffscreenBehavior final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/OffscreenBehavior;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum OffscreenBehavior. + */ + [[maybe_unused]] + [[nodiscard]] + OffscreenBehavior toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(OffscreenBehavior value) { + static const auto clazz = javaClassStatic(); + switch (value) { + case OffscreenBehavior::NONE: + static const auto fieldNONE = clazz->getStaticField("NONE"); + return clazz->getStaticFieldValue(fieldNONE); + case OffscreenBehavior::SKIP_DRAWS: + static const auto fieldSKIP_DRAWS = clazz->getStaticField("SKIP_DRAWS"); + return clazz->getStaticFieldValue(fieldSKIP_DRAWS); + case OffscreenBehavior::PAUSE: + static const auto fieldPAUSE = clazz->getStaticField("PAUSE"); + return clazz->getStaticFieldValue(fieldPAUSE); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp index ecfe8860..25a775f7 100644 --- a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp +++ b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp @@ -69,6 +69,14 @@ void JHybridRiveViewStateUpdater::updateViewProps(jni::alias_ref /* hybridView->setFrameRate(props->frameRate.value); props->frameRate.isDirty = false; } + if (props->offscreenBehavior.isDirty) { + hybridView->setOffscreenBehavior(props->offscreenBehavior.value); + props->offscreenBehavior.isDirty = false; + } + if (props->renderEnabled.isDirty) { + hybridView->setRenderEnabled(props->renderEnabled.value); + props->renderEnabled.isDirty = false; + } if (props->semantics.isDirty) { hybridView->setSemantics(props->semantics.value); props->semantics.isDirty = false; 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..44023a13 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt @@ -75,6 +75,18 @@ abstract class HybridRiveViewSpec: HybridView() { @set:Keep abstract var frameRate: Variant_Double_FrameRateRange? + @get:DoNotStrip + @get:Keep + @set:DoNotStrip + @set:Keep + abstract var offscreenBehavior: OffscreenBehavior? + + @get:DoNotStrip + @get:Keep + @set:DoNotStrip + @set:Keep + abstract var renderEnabled: Boolean? + @get:DoNotStrip @get:Keep @set:DoNotStrip diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/OffscreenBehavior.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/OffscreenBehavior.kt new file mode 100644 index 00000000..0bf12487 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/OffscreenBehavior.kt @@ -0,0 +1,24 @@ +/// +/// OffscreenBehavior.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.rive + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "OffscreenBehavior". + */ +@DoNotStrip +@Keep +enum class OffscreenBehavior(@DoNotStrip @Keep val value: Int) { + NONE(0), + SKIP_DRAWS(1), + PAUSE(2); + + companion object +} diff --git a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp index f44c2c59..4eb26485 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp @@ -66,6 +66,8 @@ namespace margelo::nitro::rive { class HybridViewModelSpec; } namespace margelo::nitro::rive { class HybridViewModelStringPropertySpec; } // Forward declaration of `HybridViewModelTriggerPropertySpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridViewModelTriggerPropertySpec; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `ReferencedAssetsType` to properly resolve imports. namespace margelo::nitro::rive { struct ReferencedAssetsType; } // Forward declaration of `ResolvedReferencedAsset` to properly resolve imports. @@ -165,6 +167,7 @@ namespace RNRive { class HybridViewModelTriggerPropertySpec_cxx; } #include "HybridViewModelSpec.hpp" #include "HybridViewModelStringPropertySpec.hpp" #include "HybridViewModelTriggerPropertySpec.hpp" +#include "OffscreenBehavior.hpp" #include "ReferencedAssetsType.hpp" #include "ResolvedReferencedAsset.hpp" #include "RiveAssetType.hpp" @@ -1005,6 +1008,21 @@ namespace margelo::nitro::rive::bridge::swift { return optional.value(); } + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_OffscreenBehavior_ = std::optional; + inline std::optional create_std__optional_OffscreenBehavior_(const OffscreenBehavior& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_OffscreenBehavior_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline OffscreenBehavior get_std__optional_OffscreenBehavior_(const std::optional& optional) noexcept { + return optional.value(); + } + // pragma MARK: std::optional /** * Specialized version of `std::optional`. diff --git a/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp b/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp index 08c77231..54282881 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp @@ -68,6 +68,8 @@ namespace margelo::nitro::rive { class HybridViewModelSpec; } namespace margelo::nitro::rive { class HybridViewModelStringPropertySpec; } // Forward declaration of `HybridViewModelTriggerPropertySpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridViewModelTriggerPropertySpec; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `ReferencedAssetsType` to properly resolve imports. namespace margelo::nitro::rive { struct ReferencedAssetsType; } // Forward declaration of `ResolvedReferencedAsset` to properly resolve imports. @@ -122,6 +124,7 @@ namespace margelo::nitro::rive { enum class ViewModelPropertyType; } #include "HybridViewModelSpec.hpp" #include "HybridViewModelStringPropertySpec.hpp" #include "HybridViewModelTriggerPropertySpec.hpp" +#include "OffscreenBehavior.hpp" #include "ReferencedAssetsType.hpp" #include "ResolvedReferencedAsset.hpp" #include "RiveAssetType.hpp" diff --git a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp index 167b1ee5..dddeecfb 100644 --- a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp @@ -20,6 +20,8 @@ namespace margelo::nitro::rive { enum class Alignment; } namespace margelo::nitro::rive { enum class Fit; } // Forward declaration of `FrameRateRange` to properly resolve imports. namespace margelo::nitro::rive { struct FrameRateRange; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -45,6 +47,7 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include "Fit.hpp" #include "FrameRateRange.hpp" #include +#include "OffscreenBehavior.hpp" #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" @@ -159,6 +162,20 @@ namespace margelo::nitro::rive { inline void setFrameRate(const std::optional>& frameRate) noexcept override { _swiftPart.setFrameRate(frameRate); } + inline std::optional getOffscreenBehavior() noexcept override { + auto __result = _swiftPart.getOffscreenBehavior(); + return __result; + } + inline void setOffscreenBehavior(std::optional offscreenBehavior) noexcept override { + _swiftPart.setOffscreenBehavior(offscreenBehavior); + } + inline std::optional getRenderEnabled() noexcept override { + auto __result = _swiftPart.getRenderEnabled(); + return __result; + } + inline void setRenderEnabled(std::optional renderEnabled) noexcept override { + _swiftPart.setRenderEnabled(renderEnabled); + } inline std::optional getSemantics() noexcept override { auto __result = _swiftPart.getSemantics(); return __result; diff --git a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm index 03fdc83e..d4bf6b91 100644 --- a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm +++ b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm @@ -112,6 +112,16 @@ - (void) updateProps:(const std::shared_ptr&)props swiftPart.setFrameRate(newViewProps.frameRate.value); newViewProps.frameRate.isDirty = false; } + // offscreenBehavior: optional + if (newViewProps.offscreenBehavior.isDirty) { + swiftPart.setOffscreenBehavior(newViewProps.offscreenBehavior.value); + newViewProps.offscreenBehavior.isDirty = false; + } + // renderEnabled: optional + if (newViewProps.renderEnabled.isDirty) { + swiftPart.setRenderEnabled(newViewProps.renderEnabled.value); + newViewProps.renderEnabled.isDirty = false; + } // semantics: optional if (newViewProps.semantics.isDirty) { swiftPart.setSemantics(newViewProps.semantics.value); diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift index cdb1a81f..79f619bb 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift @@ -18,6 +18,8 @@ public protocol HybridRiveViewSpec_protocol: HybridObject, HybridView { var fit: Fit? { get set } var layoutScaleFactor: Double? { get set } var frameRate: Variant_Double_FrameRateRange? { get set } + var offscreenBehavior: OffscreenBehavior? { get set } + var renderEnabled: Bool? { get set } var semantics: Semantics? { get set } var dataBind: Variant__any_HybridViewModelInstanceSpec__DataBindMode_DataBindByName? { get set } var onError: (_ error: RiveError) -> Void { get set } diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift index bba3dd02..d46699a0 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift @@ -312,6 +312,47 @@ open class HybridRiveViewSpec_cxx { } } + public final var offscreenBehavior: bridge.std__optional_OffscreenBehavior_ { + @inline(__always) + get { + return { () -> bridge.std__optional_OffscreenBehavior_ in + if let __unwrappedValue = self.__implementation.offscreenBehavior { + return bridge.create_std__optional_OffscreenBehavior_(__unwrappedValue) + } else { + return .init() + } + }() + } + @inline(__always) + set { + self.__implementation.offscreenBehavior = newValue.value + } + } + + public final var renderEnabled: bridge.std__optional_bool_ { + @inline(__always) + get { + return { () -> bridge.std__optional_bool_ in + if let __unwrappedValue = self.__implementation.renderEnabled { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }() + } + @inline(__always) + set { + self.__implementation.renderEnabled = { () -> Bool? in + if bridge.has_value_std__optional_bool_(newValue) { + let __unwrapped = bridge.get_std__optional_bool_(newValue) + return __unwrapped + } else { + return nil + } + }() + } + } + public final var semantics: bridge.std__optional_Semantics_ { @inline(__always) get { diff --git a/nitrogen/generated/ios/swift/OffscreenBehavior.swift b/nitrogen/generated/ios/swift/OffscreenBehavior.swift new file mode 100644 index 00000000..fa88d159 --- /dev/null +++ b/nitrogen/generated/ios/swift/OffscreenBehavior.swift @@ -0,0 +1,44 @@ +/// +/// OffscreenBehavior.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `OffscreenBehavior`, backed by a C++ enum. + */ +public typealias OffscreenBehavior = margelo.nitro.rive.OffscreenBehavior + +public extension OffscreenBehavior { + /** + * Get a OffscreenBehavior for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "none": + self = .none + case "skip-draws": + self = .skipDraws + case "pause": + self = .pause + default: + return nil + } + } + + /** + * Get the String value this OffscreenBehavior represents. + */ + var stringValue: String { + switch self { + case .none: + return "none" + case .skipDraws: + return "skip-draws" + case .pause: + return "pause" + } + } +} diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp index 25276fca..628bd984 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp @@ -30,6 +30,10 @@ namespace margelo::nitro::rive { prototype.registerHybridSetter("layoutScaleFactor", &HybridRiveViewSpec::setLayoutScaleFactor); prototype.registerHybridGetter("frameRate", &HybridRiveViewSpec::getFrameRate); prototype.registerHybridSetter("frameRate", &HybridRiveViewSpec::setFrameRate); + prototype.registerHybridGetter("offscreenBehavior", &HybridRiveViewSpec::getOffscreenBehavior); + prototype.registerHybridSetter("offscreenBehavior", &HybridRiveViewSpec::setOffscreenBehavior); + prototype.registerHybridGetter("renderEnabled", &HybridRiveViewSpec::getRenderEnabled); + prototype.registerHybridSetter("renderEnabled", &HybridRiveViewSpec::setRenderEnabled); prototype.registerHybridGetter("semantics", &HybridRiveViewSpec::getSemantics); prototype.registerHybridSetter("semantics", &HybridRiveViewSpec::setSemantics); prototype.registerHybridGetter("dataBind", &HybridRiveViewSpec::getDataBind); diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp index 7fb6ab7b..36424f2b 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp @@ -21,6 +21,8 @@ namespace margelo::nitro::rive { enum class Alignment; } namespace margelo::nitro::rive { enum class Fit; } // Forward declaration of `FrameRateRange` to properly resolve imports. namespace margelo::nitro::rive { struct FrameRateRange; } +// Forward declaration of `OffscreenBehavior` to properly resolve imports. +namespace margelo::nitro::rive { enum class OffscreenBehavior; } // Forward declaration of `Semantics` to properly resolve imports. namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. @@ -42,6 +44,7 @@ namespace margelo::nitro::rive { struct UnifiedRiveEvent; } #include "Fit.hpp" #include "FrameRateRange.hpp" #include +#include "OffscreenBehavior.hpp" #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" @@ -94,6 +97,10 @@ namespace margelo::nitro::rive { virtual void setLayoutScaleFactor(std::optional layoutScaleFactor) = 0; virtual std::optional> getFrameRate() = 0; virtual void setFrameRate(const std::optional>& frameRate) = 0; + virtual std::optional getOffscreenBehavior() = 0; + virtual void setOffscreenBehavior(std::optional offscreenBehavior) = 0; + virtual std::optional getRenderEnabled() = 0; + virtual void setRenderEnabled(std::optional renderEnabled) = 0; virtual std::optional getSemantics() = 0; virtual void setSemantics(std::optional semantics) = 0; virtual std::optional, DataBindMode, DataBindByName>> getDataBind() = 0; diff --git a/nitrogen/generated/shared/c++/OffscreenBehavior.hpp b/nitrogen/generated/shared/c++/OffscreenBehavior.hpp new file mode 100644 index 00000000..a3f266ad --- /dev/null +++ b/nitrogen/generated/shared/c++/OffscreenBehavior.hpp @@ -0,0 +1,80 @@ +/// +/// OffscreenBehavior.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::rive { + + /** + * An enum which can be represented as a JavaScript union (OffscreenBehavior). + */ + enum class OffscreenBehavior { + NONE SWIFT_NAME(none) = 0, + SKIP_DRAWS SWIFT_NAME(skipDraws) = 1, + PAUSE SWIFT_NAME(pause) = 2, + } CLOSED_ENUM; + +} // namespace margelo::nitro::rive + +namespace margelo::nitro { + + // C++ OffscreenBehavior <> JS OffscreenBehavior (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::rive::OffscreenBehavior fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("none"): return margelo::nitro::rive::OffscreenBehavior::NONE; + case hashString("skip-draws"): return margelo::nitro::rive::OffscreenBehavior::SKIP_DRAWS; + case hashString("pause"): return margelo::nitro::rive::OffscreenBehavior::PAUSE; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum OffscreenBehavior - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::rive::OffscreenBehavior arg) { + switch (arg) { + case margelo::nitro::rive::OffscreenBehavior::NONE: return JSIConverter::toJSI(runtime, "none"); + case margelo::nitro::rive::OffscreenBehavior::SKIP_DRAWS: return JSIConverter::toJSI(runtime, "skip-draws"); + case margelo::nitro::rive::OffscreenBehavior::PAUSE: return JSIConverter::toJSI(runtime, "pause"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert OffscreenBehavior to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("none"): + case hashString("skip-draws"): + case hashString("pause"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp index 82757d46..47ad84bf 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp @@ -113,6 +113,28 @@ namespace margelo::nitro::rive::views { throw std::runtime_error(std::string("RiveView.frameRate: ") + exc.what()); } }()), + offscreenBehavior([&]() -> CachedProp> { + try { + const react::RawValue* rawValue = rawProps.at("offscreenBehavior", nullptr, nullptr); + if (rawValue == nullptr) return sourceProps.offscreenBehavior; + const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.offscreenBehavior); + return CachedProp>::fromRawValue(*runtime, value, sourceProps.offscreenBehavior); + } catch (const std::exception& exc) { + throw std::runtime_error(std::string("RiveView.offscreenBehavior: ") + exc.what()); + } + }()), + renderEnabled([&]() -> CachedProp> { + try { + const react::RawValue* rawValue = rawProps.at("renderEnabled", nullptr, nullptr); + if (rawValue == nullptr) return sourceProps.renderEnabled; + const auto& [runtime, value] = (std::pair)*rawValue; + if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.renderEnabled); + return CachedProp>::fromRawValue(*runtime, value, sourceProps.renderEnabled); + } catch (const std::exception& exc) { + throw std::runtime_error(std::string("RiveView.renderEnabled: ") + exc.what()); + } + }()), semantics([&]() -> CachedProp> { try { const react::RawValue* rawValue = rawProps.at("semantics", nullptr, nullptr); @@ -176,6 +198,8 @@ namespace margelo::nitro::rive::views { case hashString("fit"): return true; case hashString("layoutScaleFactor"): return true; case hashString("frameRate"): return true; + case hashString("offscreenBehavior"): return true; + case hashString("renderEnabled"): return true; case hashString("semantics"): return true; case hashString("dataBind"): return true; case hashString("onError"): return true; diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp index 27d708de..5bc76355 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp @@ -24,6 +24,7 @@ #include "Fit.hpp" #include "FrameRateRange.hpp" #include +#include "OffscreenBehavior.hpp" #include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" @@ -60,6 +61,8 @@ namespace margelo::nitro::rive::views { CachedProp> fit; CachedProp> layoutScaleFactor; CachedProp>> frameRate; + CachedProp> offscreenBehavior; + CachedProp> renderEnabled; CachedProp> semantics; CachedProp, DataBindMode, DataBindByName>>> dataBind; CachedProp> onError; diff --git a/nitrogen/generated/shared/json/RiveViewConfig.json b/nitrogen/generated/shared/json/RiveViewConfig.json index bed6f54b..dd8778ec 100644 --- a/nitrogen/generated/shared/json/RiveViewConfig.json +++ b/nitrogen/generated/shared/json/RiveViewConfig.json @@ -12,6 +12,8 @@ "fit": true, "layoutScaleFactor": true, "frameRate": true, + "offscreenBehavior": true, + "renderEnabled": true, "semantics": true, "dataBind": true, "onError": true, diff --git a/src/core/RiveView.tsx b/src/core/RiveView.tsx index e4ceeb26..ee21c4ef 100644 --- a/src/core/RiveView.tsx +++ b/src/core/RiveView.tsx @@ -45,6 +45,8 @@ const defaultOnStop = () => {}; * @property {Alignment} [alignment] - How the Rive graphic should be aligned within its container * @property {Fit} [fit] - How the Rive graphic should fit within its container * @property {number | FrameRateRange} [frameRate] - Preferred frame rate for the render loop (new runtimes only) + * @property {OffscreenBehavior} [offscreenBehavior='none'] - What to do while the view is outside the visible viewport: keep running, keep advancing but skip draws, or pause (new runtimes only) + * @property {boolean} [renderEnabled=true] - Set false to skip drawing while the state machine keeps advancing, for views covered by UI the view can't detect (new Android runtime 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) diff --git a/src/index.tsx b/src/index.tsx index bbcfa4ee..e7921651 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,7 +19,7 @@ export { NitroRiveView } from './core/NitroRiveViewComponent'; export { RiveView, type RiveViewProps } from './core/RiveView'; export type { RiveViewMethods }; export type RiveViewRef = HybridView; -export type { FrameRateRange } from './specs/RiveView.nitro'; +export type { FrameRateRange, OffscreenBehavior } from './specs/RiveView.nitro'; export type { RiveFile, RiveEnumDefinition, diff --git a/src/specs/RiveView.nitro.ts b/src/specs/RiveView.nitro.ts index 25d7b3dc..088886b6 100644 --- a/src/specs/RiveView.nitro.ts +++ b/src/specs/RiveView.nitro.ts @@ -29,6 +29,12 @@ export interface FrameRateRange { preferred?: number; } +/** + * What to do while the view is outside the visible viewport (scrolled out of + * view, hidden, or detached from a window). See RiveViewProps.offscreenBehavior. + */ +export type OffscreenBehavior = 'none' | 'skip-draws' | 'pause'; + /** * Props interface for the RiveView component. * Extends HybridViewProps to include Rive-specific properties. @@ -63,6 +69,39 @@ export interface RiveViewProps extends HybridViewProps { * @see https://rive.app/docs/runtimes/apple/apple#frame-rate */ frameRate?: number | FrameRateRange; + /** + * What to do while the view is outside the visible viewport (scrolled out + * of view, hidden, or detached from a window). Views covered by an overlay + * in the same window (e.g. a React Native Modal) still count as visible — + * use renderEnabled for occlusion the view cannot detect. + * + * - 'none' (default): keep advancing and drawing regardless of visibility. + * - 'skip-draws': keep advancing the state machine but skip drawing frames + * while offscreen. Events, data binding and playback time stay live, so + * this is safe even when other UI is driven from the state machine. + * - 'pause': stop advancing and drawing while offscreen; playback resumes + * from where it left off when the view becomes visible again. The state + * machine does not advance while offscreen, so don't use it when + * data-binding consumers rely on it advancing regardless of visibility. + * + * Only supported on the new (default) runtimes; the legacy backends ignore + * it. On iOS the upstream runtime couples advancing and drawing, so + * 'skip-draws' behaves like 'none' there ('pause' is fully supported, and + * iOS already throttles most offscreen rendering on its own). + */ + offscreenBehavior?: OffscreenBehavior; + /** + * When false, the view stops drawing frames while the state machine keeps + * advancing (events, data binding, and playback time stay live). Use it + * when the app knows the view can't be seen — e.g. covered by a Modal or a + * bottom sheet — which automatic visibility detection cannot observe. The + * view repaints on the next frame after re-enabling. Defaults to true. + * + * Only the new (default) Android runtime skips draws; iOS ignores it for + * now (the upstream runtime couples advancing and drawing, and its + * fullscreen modals already hide the covered hierarchy). + */ + renderEnabled?: boolean; /** * Exposes accessibility semantics authored in the Rive editor to the * platform screen reader (VoiceOver). Defaults to Semantics.Off. From 4466bfde20896d6b5b1773850e77c3e123c98319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 30 Jul 2026 16:50:40 +0200 Subject: [PATCH 2/2] feat: accept 'pause' in renderEnabled for a declarative full stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderEnabled={false} keeps the state machine advancing by design, so there was no declarative way to stop rendering and advancing for a view covered by UI it cannot detect — only the imperative pause(). Accept 'pause' as a third value: it stops draws and state machine advance, composing with the pause()/play() ref state rather than overwriting it. Works on both new backends (on iOS it maps onto the same isPaused lever the offscreen 'pause' uses; the boolean remains a no-op there). Nitro cannot mix string literals into a variant type, so the spec types the prop as boolean | string (Variant_Boolean_String) and the public RiveView component narrows it to boolean | 'pause'; unrecognized strings render normally. Emulator, modal-covered looping animation: 78.3% of a core unmitigated, 12.5% with renderEnabled={false} (state machine thread still ~2%), 11.5% with 'pause' (state machine thread at zero); rendering resumes when the prop returns to true. --- .../com/margelo/nitro/rive/HybridRiveView.kt | 2 +- .../com/margelo/nitro/rive/HybridRiveView.kt | 11 ++- .../new/java/com/rive/RiveReactNativeView.kt | 17 +++-- example/src/reproducers/OffscreenBehavior.tsx | 38 ++++++++--- ios/legacy/HybridRiveView.swift | 2 +- ios/new/HybridRiveView.swift | 15 +++- ios/new/RiveReactNativeView.swift | 15 +++- .../android/c++/JHybridRiveViewSpec.cpp | 13 ++-- .../android/c++/JHybridRiveViewSpec.hpp | 4 +- .../android/c++/JVariant_Boolean_String.cpp | 26 +++++++ .../android/c++/JVariant_Boolean_String.hpp | 68 +++++++++++++++++++ .../margelo/nitro/rive/HybridRiveViewSpec.kt | 2 +- .../nitro/rive/Variant_Boolean_String.kt | 53 +++++++++++++++ .../generated/android/rive+autolinking.cmake | 1 + .../generated/ios/RNRive-Swift-Cxx-Bridge.hpp | 44 ++++++++++++ .../ios/c++/HybridRiveViewSpecSwift.hpp | 4 +- .../ios/swift/HybridRiveViewSpec.swift | 2 +- .../ios/swift/HybridRiveViewSpec_cxx.swift | 33 +++++++-- .../ios/swift/Variant_Bool_String.swift | 18 +++++ .../shared/c++/HybridRiveViewSpec.hpp | 4 +- .../c++/views/HybridRiveViewComponent.cpp | 6 +- .../c++/views/HybridRiveViewComponent.hpp | 2 +- src/core/RiveView.tsx | 16 ++++- src/specs/RiveView.nitro.ts | 31 ++++++--- 24 files changed, 369 insertions(+), 58 deletions(-) create mode 100644 nitrogen/generated/android/c++/JVariant_Boolean_String.cpp create mode 100644 nitrogen/generated/android/c++/JVariant_Boolean_String.hpp create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Boolean_String.kt create mode 100644 nitrogen/generated/ios/swift/Variant_Bool_String.swift 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 982957d0..7292252c 100644 --- a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -97,7 +97,7 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { // Accepted for API parity; offscreen handling and draw skipping are only // implemented on the new runtimes. override var offscreenBehavior: OffscreenBehavior? = null - override var renderEnabled: Boolean? = null + override var renderEnabled: Variant_Boolean_String? = null // Accepted for API parity; semantics are only available in the new Rive // runtime, and Android support is pending upstream (iOS-only for now). 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 e51740c4..04f1add4 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -6,6 +6,7 @@ import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.core.Promise import com.rive.BindData +import com.rive.RenderMode import com.rive.RiveReactNativeView import com.rive.ViewConfiguration import app.rive.Fit as RiveFit @@ -117,10 +118,16 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { view.offscreenBehavior = value ?: OffscreenBehavior.NONE } - override var renderEnabled: Boolean? = null + override var renderEnabled: Variant_Boolean_String? = null set(value) { field = value - view.renderEnabled = value ?: true + view.renderMode = when { + value == null -> RenderMode.Enabled + value.asSecondOrNull() == "pause" -> RenderMode.Paused + value.asFirstOrNull() == false -> RenderMode.SkipDraws + // true, or an unrecognized string (the public API narrows to 'pause') + else -> RenderMode.Enabled + } } // Accepted for API parity; semantics support is pending in the upstream diff --git a/android/src/new/java/com/rive/RiveReactNativeView.kt b/android/src/new/java/com/rive/RiveReactNativeView.kt index 87b5844b..c16b25aa 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -36,6 +36,10 @@ import kotlinx.coroutines.withContext import kotlin.time.Duration import kotlin.time.Duration.Companion.nanoseconds +// The renderEnabled prop (boolean | 'pause') resolved into what the render +// loop should do: draw normally, keep advancing but skip draws, or stop both. +enum class RenderMode { Enabled, SkipDraws, Paused } + sealed class BindData { data object None : BindData() data object Auto : BindData() @@ -81,8 +85,10 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { var offscreenBehavior: OffscreenBehavior = OffscreenBehavior.NONE // Manual counterpart to offscreenBehavior for occlusion the view can't - // detect (RN Modal, bottom sheets): false = skip draws, keep advancing. - var renderEnabled: Boolean = true + // detect (RN Modal, bottom sheets): SkipDraws keeps the state machine + // advancing, Paused stops it too (composes with the pause()/play() state + // rather than overwriting it). + var renderMode: RenderMode = RenderMode.Enabled private val visibleRectBuffer = Rect() @@ -192,8 +198,9 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { val offscreen = offscreenBehavior != OffscreenBehavior.NONE && !isInVisibleViewport() val pausedOffscreen = offscreen && offscreenBehavior == OffscreenBehavior.PAUSE + val renderPaused = renderMode == RenderMode.Paused - if ((paused || pausedOffscreen) && !needsRedraw) { + if ((paused || pausedOffscreen || renderPaused) && !needsRedraw) { // Keep the timebase fresh so resuming advances by one frame, not by // the whole pause span. lastFrameTimeNs = frameTimeNanos @@ -230,11 +237,11 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { // is the dominant part of the offscreen cost. needsRedraw still forces // a draw so pending content (initial frame, resize, rebinding) isn't // lost while invisible. - val skipDraw = !renderEnabled || offscreen + val skipDraw = renderMode != RenderMode.Enabled || offscreen if (worker != null && art != null && sm != null && rs != null) { try { - if (!paused && !settled && !pausedOffscreen) { + if (!paused && !settled && !pausedOffscreen && !renderPaused) { worker.advanceStateMachine(sm, deltaTime) } if (!skipDraw || needsRedraw) { diff --git a/example/src/reproducers/OffscreenBehavior.tsx b/example/src/reproducers/OffscreenBehavior.tsx index 449f719f..4ad1250c 100644 --- a/example/src/reproducers/OffscreenBehavior.tsx +++ b/example/src/reproducers/OffscreenBehavior.tsx @@ -33,8 +33,8 @@ import { type Metadata } from '../shared/metadata'; * With offscreenBehavior 'skip-draws' or 'pause' the offscreen scenario * should drop close to the paused cost; scrolling back must resume the * animation. The modal scenario is invisible to automatic detection — the - * renderEnabled←modal toggle wires renderEnabled to it, which is the pattern - * that prop exists for. + * renderEnabled←modal toggle wires renderEnabled to it (false or 'pause' + * while covered), which is the pattern that prop exists for. */ type Scenario = 'onscreen' | 'offscreen' | 'modal' | 'paused' | 'unmounted'; @@ -49,10 +49,15 @@ const SCENARIOS: { key: Scenario; label: string }[] = [ const BEHAVIORS: OffscreenBehavior[] = ['none', 'skip-draws', 'pause']; +// What renderEnabled is set to while the modal covers the view: not wired at +// all, draw skipping only, or a declarative full pause. +const RENDER_WIRINGS = ['off', 'skip-draws', 'pause'] as const; +type RenderWiring = (typeof RENDER_WIRINGS)[number]; + export default function OffscreenBehaviorPage() { const [scenario, setScenario] = useState('onscreen'); const [behavior, setBehavior] = useState('none'); - const [wireRenderEnabled, setWireRenderEnabled] = useState(false); + const [renderWiring, setRenderWiring] = useState('off'); const viewRef = useRef(null); const scrollRef = useRef(null); const { riveFile } = useRiveFile(require('../../assets/rive/rewards.riv')); @@ -80,7 +85,9 @@ export default function OffscreenBehaviorPage() { {scenario.toUpperCase()} · {behavior} - {wireRenderEnabled ? ' +renderEnabled←modal' : ''} + {renderWiring !== 'off' + ? ` +renderEnabled←modal (${renderWiring})` + : ''} {SCENARIOS.map(({ key, label }) => ( @@ -121,16 +128,23 @@ export default function OffscreenBehaviorPage() { ))} setWireRenderEnabled((v) => !v)} + style={[styles.button, renderWiring !== 'off' && styles.buttonActive]} + onPress={() => + setRenderWiring( + (v) => + RENDER_WIRINGS[ + (RENDER_WIRINGS.indexOf(v) + 1) % RENDER_WIRINGS.length + ]! + ) + } > - renderEnabled←modal + renderEnabled←modal: {renderWiring} @@ -142,7 +156,13 @@ export default function OffscreenBehaviorPage() { fit={Fit.Contain} autoPlay={true} offscreenBehavior={behavior} - renderEnabled={wireRenderEnabled ? scenario !== 'modal' : true} + renderEnabled={ + renderWiring === 'off' || scenario !== 'modal' + ? true + : renderWiring === 'skip-draws' + ? false + : 'pause' + } hybridRef={{ f: (ref) => (viewRef.current = ref) }} style={styles.rive} /> diff --git a/ios/legacy/HybridRiveView.swift b/ios/legacy/HybridRiveView.swift index e8e2a5c1..f88c51e1 100644 --- a/ios/legacy/HybridRiveView.swift +++ b/ios/legacy/HybridRiveView.swift @@ -112,7 +112,7 @@ class HybridRiveView: HybridRiveViewSpec { // Accepted for API parity; offscreen handling and draw skipping are only // implemented on the new runtimes. var offscreenBehavior: OffscreenBehavior? - var renderEnabled: Bool? + var renderEnabled: Variant_Bool_String? // Accepted for API parity; semantics are only available in the new Rive // runtime (the experimental backend). var semantics: Semantics? diff --git a/ios/new/HybridRiveView.swift b/ios/new/HybridRiveView.swift index 46a77bae..e410c883 100644 --- a/ios/new/HybridRiveView.swift +++ b/ios/new/HybridRiveView.swift @@ -100,9 +100,10 @@ class HybridRiveView: HybridRiveViewSpec { var layoutScaleFactor: Double? var frameRate: Variant_Double_FrameRateRange? var offscreenBehavior: OffscreenBehavior? - // Accepted for API parity; the upstream iOS runtime couples advancing and - // drawing, so draw-only skipping isn't implementable yet. - var renderEnabled: Bool? + // 'pause' is honored; the boolean (draw-only skipping) is accepted for API + // parity — the upstream iOS runtime couples advancing and drawing, so + // false behaves like true. + var renderEnabled: Variant_Bool_String? var semantics: Semantics? var onError: (RiveError) -> Void = { _ in } var onStop: () -> Void = {} @@ -221,6 +222,7 @@ class HybridRiveView: HybridRiveViewSpec { semantics: toRiveSemantics(semantics), frameRate: toRiveFrameRate(frameRate), offscreenBehavior: offscreenBehavior ?? .none, + renderPaused: toRenderPaused(renderEnabled), bindData: try dataBind.toBindData() ) @@ -286,6 +288,13 @@ class HybridRiveView: HybridRiveViewSpec { } } + private func toRenderPaused(_ renderEnabled: Variant_Bool_String?) -> Bool { + if case .some(.second("pause")) = renderEnabled { + return true + } + return false + } + private func toRiveSemantics(_ semantics: Semantics?) -> RiveRuntime.Semantics { switch semantics { case .off: return .off diff --git a/ios/new/RiveReactNativeView.swift b/ios/new/RiveReactNativeView.swift index da5bc1ba..30b77473 100644 --- a/ios/new/RiveReactNativeView.swift +++ b/ios/new/RiveReactNativeView.swift @@ -18,6 +18,7 @@ struct ViewConfiguration { let semantics: RiveRuntime.Semantics let frameRate: RiveRuntime.FrameRate let offscreenBehavior: OffscreenBehavior + let renderPaused: Bool let bindData: BindData } @@ -59,6 +60,16 @@ class RiveReactNativeView: UIView { applyPauseState() } } + + // renderEnabled='pause': a declarative full stop for occlusion the view + // can't detect. Kept separate from isPaused so it composes with the + // pause()/play() ref methods instead of overwriting their state. + private var renderPaused = false { + didSet { + guard renderPaused != oldValue else { return } + applyPauseState() + } + } private var visibilityTimer: Timer? private func updateVisibilityPolling() { @@ -94,7 +105,8 @@ class RiveReactNativeView: UIView { } private func applyPauseState() { - riveUIView?.isPaused = isPaused || (offscreenBehavior == .pause && isOffscreen) + riveUIView?.isPaused = + isPaused || renderPaused || (offscreenBehavior == .pause && isOffscreen) } override func didMoveToWindow() { @@ -132,6 +144,7 @@ class RiveReactNativeView: UIView { semantics = config.semantics frameRate = config.frameRate offscreenBehavior = config.offscreenBehavior + renderPaused = config.renderPaused if reload { cleanup() diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp index 962d34a3..18a093b7 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp @@ -49,6 +49,7 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include "JFrameRateRange.hpp" #include "OffscreenBehavior.hpp" #include "JOffscreenBehavior.hpp" +#include "JVariant_Boolean_String.hpp" #include "Semantics.hpp" #include "JSemantics.hpp" #include "HybridViewModelInstanceSpec.hpp" @@ -188,14 +189,14 @@ namespace margelo::nitro::rive { static const auto method = _javaPart->javaClassStatic()->getMethod /* offscreenBehavior */)>("setOffscreenBehavior"); method(_javaPart, offscreenBehavior.has_value() ? JOffscreenBehavior::fromCpp(offscreenBehavior.value()) : nullptr); } - std::optional JHybridRiveViewSpec::getRenderEnabled() { - static const auto method = _javaPart->javaClassStatic()->getMethod()>("getRenderEnabled"); + std::optional> JHybridRiveViewSpec::getRenderEnabled() { + static const auto method = _javaPart->javaClassStatic()->getMethod()>("getRenderEnabled"); auto __result = method(_javaPart); - return __result != nullptr ? std::make_optional(static_cast(__result->value())) : std::nullopt; + return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt; } - void JHybridRiveViewSpec::setRenderEnabled(std::optional renderEnabled) { - static const auto method = _javaPart->javaClassStatic()->getMethod /* renderEnabled */)>("setRenderEnabled"); - method(_javaPart, renderEnabled.has_value() ? jni::JBoolean::valueOf(renderEnabled.value()) : nullptr); + void JHybridRiveViewSpec::setRenderEnabled(const std::optional>& renderEnabled) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* renderEnabled */)>("setRenderEnabled"); + method(_javaPart, renderEnabled.has_value() ? JVariant_Boolean_String::fromCpp(renderEnabled.value()) : nullptr); } std::optional JHybridRiveViewSpec::getSemantics() { static const auto method = _javaPart->javaClassStatic()->getMethod()>("getSemantics"); diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp index fab11cff..d7c0b9e5 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp @@ -68,8 +68,8 @@ namespace margelo::nitro::rive { void setFrameRate(const std::optional>& frameRate) override; std::optional getOffscreenBehavior() override; void setOffscreenBehavior(std::optional offscreenBehavior) override; - std::optional getRenderEnabled() override; - void setRenderEnabled(std::optional renderEnabled) override; + std::optional> getRenderEnabled() override; + void setRenderEnabled(const std::optional>& renderEnabled) override; std::optional getSemantics() override; void setSemantics(std::optional semantics) override; std::optional, DataBindMode, DataBindByName>> getDataBind() override; diff --git a/nitrogen/generated/android/c++/JVariant_Boolean_String.cpp b/nitrogen/generated/android/c++/JVariant_Boolean_String.cpp new file mode 100644 index 00000000..dce76765 --- /dev/null +++ b/nitrogen/generated/android/c++/JVariant_Boolean_String.cpp @@ -0,0 +1,26 @@ +/// +/// JVariant_Boolean_String.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "JVariant_Boolean_String.hpp" + +namespace margelo::nitro::rive { + /** + * Converts JVariant_Boolean_String to std::variant + */ + std::variant JVariant_Boolean_String::toCpp() const { + if (isInstanceOf(JVariant_Boolean_String_impl::First::javaClassStatic())) { + // It's a `bool` + auto jniValue = static_cast(this)->getValue(); + return static_cast(jniValue); + } else if (isInstanceOf(JVariant_Boolean_String_impl::Second::javaClassStatic())) { + // It's a `std::string` + auto jniValue = static_cast(this)->getValue(); + return jniValue->toStdString(); + } + throw std::invalid_argument("Variant is unknown Kotlin instance!"); + } +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/JVariant_Boolean_String.hpp b/nitrogen/generated/android/c++/JVariant_Boolean_String.hpp new file mode 100644 index 00000000..0f62aebc --- /dev/null +++ b/nitrogen/generated/android/c++/JVariant_Boolean_String.hpp @@ -0,0 +1,68 @@ +/// +/// JVariant_Boolean_String.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include +#include + +namespace margelo::nitro::rive { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ std::variant and the Java class "Variant_Boolean_String". + */ + class JVariant_Boolean_String: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Variant_Boolean_String;"; + + static jni::local_ref create_0(jboolean value) { + static const auto method = javaClassStatic()->getStaticMethod("create"); + return method(javaClassStatic(), value); + } + static jni::local_ref create_1(jni::alias_ref value) { + static const auto method = javaClassStatic()->getStaticMethod)>("create"); + return method(javaClassStatic(), value); + } + + static jni::local_ref fromCpp(const std::variant& variant) { + switch (variant.index()) { + case 0: return create_0(std::get<0>(variant)); + case 1: return create_1(jni::make_jstring(std::get<1>(variant))); + default: throw std::invalid_argument("Variant holds unknown index! (" + std::to_string(variant.index()) + ")"); + } + } + + [[nodiscard]] std::variant toCpp() const; + }; + + namespace JVariant_Boolean_String_impl { + class First final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Variant_Boolean_String$First;"; + + [[nodiscard]] jboolean getValue() const { + static const auto field = javaClassStatic()->getField("value"); + return getFieldValue(field); + } + }; + + class Second final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Variant_Boolean_String$Second;"; + + [[nodiscard]] jni::local_ref getValue() const { + static const auto field = javaClassStatic()->getField("value"); + return getFieldValue(field); + } + }; + } // namespace JVariant_Boolean_String_impl +} // namespace margelo::nitro::rive 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 44023a13..bbd7e66d 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt @@ -85,7 +85,7 @@ abstract class HybridRiveViewSpec: HybridView() { @get:Keep @set:DoNotStrip @set:Keep - abstract var renderEnabled: Boolean? + abstract var renderEnabled: Variant_Boolean_String? @get:DoNotStrip @get:Keep diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Boolean_String.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Boolean_String.kt new file mode 100644 index 00000000..f2517add --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Variant_Boolean_String.kt @@ -0,0 +1,53 @@ +/// +/// Variant_Boolean_String.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.rive + +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the TypeScript variant "Boolean | String". + */ +@Suppress("ClassName") +@DoNotStrip +sealed class Variant_Boolean_String { + @DoNotStrip + data class First(@DoNotStrip val value: Boolean): Variant_Boolean_String() + @DoNotStrip + data class Second(@DoNotStrip val value: String): Variant_Boolean_String() + + val isFirst: Boolean + get() = this is First + val isSecond: Boolean + get() = this is Second + + fun asFirstOrNull(): Boolean? { + val value = (this as? First)?.value ?: return null + return value + } + fun asSecondOrNull(): String? { + val value = (this as? Second)?.value ?: return null + return value + } + + inline fun match(first: (Boolean) -> R, second: (String) -> R): R { + return when (this) { + is First -> first(value) + is Second -> second(value) + } + } + + companion object { + @JvmStatic + @DoNotStrip + fun create(value: Boolean): Variant_Boolean_String = First(value) + @JvmStatic + @DoNotStrip + fun create(value: String): Variant_Boolean_String = Second(value) + } +} diff --git a/nitrogen/generated/android/rive+autolinking.cmake b/nitrogen/generated/android/rive+autolinking.cmake index 31833465..d7383b7c 100644 --- a/nitrogen/generated/android/rive+autolinking.cmake +++ b/nitrogen/generated/android/rive+autolinking.cmake @@ -68,6 +68,7 @@ target_sources( ../nitrogen/generated/android/c++/JHybridRiveRuntimeSpec.cpp ../nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp ../nitrogen/generated/android/c++/JVariant_Double_FrameRateRange.cpp + ../nitrogen/generated/android/c++/JVariant_Boolean_String.cpp ../nitrogen/generated/android/c++/JVariant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.cpp ../nitrogen/generated/android/c++/JEventPropertiesOutput.cpp ../nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp diff --git a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp index 4eb26485..1320332b 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp @@ -1023,6 +1023,50 @@ namespace margelo::nitro::rive::bridge::swift { return optional.value(); } + // pragma MARK: std::variant + /** + * Wrapper struct for `std::variant`. + * std::variant cannot be used in Swift because of a Swift bug. + * Not even specializing it works. So we create a wrapper struct. + */ + struct std__variant_bool__std__string_ final { + std::variant variant; + std__variant_bool__std__string_(std::variant variant): variant(variant) { } + operator std::variant() const noexcept { + return variant; + } + inline size_t index() const noexcept { + return variant.index(); + } + inline bool get_0() const noexcept { + return std::get<0>(variant); + } + inline std::string get_1() const noexcept { + return std::get<1>(variant); + } + }; + inline std__variant_bool__std__string_ create_std__variant_bool__std__string_(bool value) noexcept { + return std__variant_bool__std__string_(value); + } + inline std__variant_bool__std__string_ create_std__variant_bool__std__string_(const std::string& value) noexcept { + return std__variant_bool__std__string_(value); + } + + // pragma MARK: std::optional> + /** + * Specialized version of `std::optional>`. + */ + using std__optional_std__variant_bool__std__string__ = std::optional>; + inline std::optional> create_std__optional_std__variant_bool__std__string__(const std::variant& value) noexcept { + return std::optional>(value); + } + inline bool has_value_std__optional_std__variant_bool__std__string__(const std::optional>& optional) noexcept { + return optional.has_value(); + } + inline std::variant get_std__optional_std__variant_bool__std__string__(const std::optional>& optional) noexcept { + return optional.value(); + } + // pragma MARK: std::optional /** * Specialized version of `std::optional`. diff --git a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp index dddeecfb..9c195f1c 100644 --- a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp @@ -169,11 +169,11 @@ namespace margelo::nitro::rive { inline void setOffscreenBehavior(std::optional offscreenBehavior) noexcept override { _swiftPart.setOffscreenBehavior(offscreenBehavior); } - inline std::optional getRenderEnabled() noexcept override { + inline std::optional> getRenderEnabled() noexcept override { auto __result = _swiftPart.getRenderEnabled(); return __result; } - inline void setRenderEnabled(std::optional renderEnabled) noexcept override { + inline void setRenderEnabled(const std::optional>& renderEnabled) noexcept override { _swiftPart.setRenderEnabled(renderEnabled); } inline std::optional getSemantics() noexcept override { diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift index 79f619bb..b99de630 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift @@ -19,7 +19,7 @@ public protocol HybridRiveViewSpec_protocol: HybridObject, HybridView { var layoutScaleFactor: Double? { get set } var frameRate: Variant_Double_FrameRateRange? { get set } var offscreenBehavior: OffscreenBehavior? { get set } - var renderEnabled: Bool? { get set } + var renderEnabled: Variant_Bool_String? { get set } var semantics: Semantics? { get set } var dataBind: Variant__any_HybridViewModelInstanceSpec__DataBindMode_DataBindByName? { get set } var onError: (_ error: RiveError) -> Void { get set } diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift index d46699a0..1b11efb5 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift @@ -329,12 +329,19 @@ open class HybridRiveViewSpec_cxx { } } - public final var renderEnabled: bridge.std__optional_bool_ { + public final var renderEnabled: bridge.std__optional_std__variant_bool__std__string__ { @inline(__always) get { - return { () -> bridge.std__optional_bool_ in + return { () -> bridge.std__optional_std__variant_bool__std__string__ in if let __unwrappedValue = self.__implementation.renderEnabled { - return bridge.create_std__optional_bool_(__unwrappedValue) + return bridge.create_std__optional_std__variant_bool__std__string__({ () -> bridge.std__variant_bool__std__string_ in + switch __unwrappedValue { + case .first(let __value): + return bridge.create_std__variant_bool__std__string_(__value) + case .second(let __value): + return bridge.create_std__variant_bool__std__string_(std.string(__value)) + } + }().variant) } else { return .init() } @@ -342,10 +349,22 @@ open class HybridRiveViewSpec_cxx { } @inline(__always) set { - self.__implementation.renderEnabled = { () -> Bool? in - if bridge.has_value_std__optional_bool_(newValue) { - let __unwrapped = bridge.get_std__optional_bool_(newValue) - return __unwrapped + self.__implementation.renderEnabled = { () -> Variant_Bool_String? in + if bridge.has_value_std__optional_std__variant_bool__std__string__(newValue) { + let __unwrapped = bridge.get_std__optional_std__variant_bool__std__string__(newValue) + return { () -> Variant_Bool_String in + let __variant = bridge.std__variant_bool__std__string_(__unwrapped) + switch __variant.index() { + case 0: + let __actual = __variant.get_0() + return .first(__actual) + case 1: + let __actual = __variant.get_1() + return .second(String(__actual)) + default: + fatalError("Variant can never have index \(__variant.index())!") + } + }() } else { return nil } diff --git a/nitrogen/generated/ios/swift/Variant_Bool_String.swift b/nitrogen/generated/ios/swift/Variant_Bool_String.swift new file mode 100644 index 00000000..0d912bf7 --- /dev/null +++ b/nitrogen/generated/ios/swift/Variant_Bool_String.swift @@ -0,0 +1,18 @@ +/// +/// Variant_Bool_String.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + + + +/** + * An Swift enum with associated values representing a Variant/Union type. + * JS type: `boolean | string` + */ +@frozen +public enum Variant_Bool_String { + case first(Bool) + case second(String) +} diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp index 36424f2b..ef001c9c 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp @@ -99,8 +99,8 @@ namespace margelo::nitro::rive { virtual void setFrameRate(const std::optional>& frameRate) = 0; virtual std::optional getOffscreenBehavior() = 0; virtual void setOffscreenBehavior(std::optional offscreenBehavior) = 0; - virtual std::optional getRenderEnabled() = 0; - virtual void setRenderEnabled(std::optional renderEnabled) = 0; + virtual std::optional> getRenderEnabled() = 0; + virtual void setRenderEnabled(const std::optional>& renderEnabled) = 0; virtual std::optional getSemantics() = 0; virtual void setSemantics(std::optional semantics) = 0; virtual std::optional, DataBindMode, DataBindByName>> getDataBind() = 0; diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp index 47ad84bf..f70c0e1d 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp @@ -124,13 +124,13 @@ namespace margelo::nitro::rive::views { throw std::runtime_error(std::string("RiveView.offscreenBehavior: ") + exc.what()); } }()), - renderEnabled([&]() -> CachedProp> { + renderEnabled([&]() -> CachedProp>> { try { const react::RawValue* rawValue = rawProps.at("renderEnabled", nullptr, nullptr); if (rawValue == nullptr) return sourceProps.renderEnabled; const auto& [runtime, value] = (std::pair)*rawValue; - if (value.isNull()) return CachedProp>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.renderEnabled); - return CachedProp>::fromRawValue(*runtime, value, sourceProps.renderEnabled); + if (value.isNull()) return CachedProp>>::fromRawValue(*runtime, jsi::Value::undefined(), sourceProps.renderEnabled); + return CachedProp>>::fromRawValue(*runtime, value, sourceProps.renderEnabled); } catch (const std::exception& exc) { throw std::runtime_error(std::string("RiveView.renderEnabled: ") + exc.what()); } diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp index 5bc76355..ced31065 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp @@ -62,7 +62,7 @@ namespace margelo::nitro::rive::views { CachedProp> layoutScaleFactor; CachedProp>> frameRate; CachedProp> offscreenBehavior; - CachedProp> renderEnabled; + CachedProp>> renderEnabled; CachedProp> semantics; CachedProp, DataBindMode, DataBindByName>>> dataBind; CachedProp> onError; diff --git a/src/core/RiveView.tsx b/src/core/RiveView.tsx index ee21c4ef..08acb8c3 100644 --- a/src/core/RiveView.tsx +++ b/src/core/RiveView.tsx @@ -5,7 +5,10 @@ import { callDispose } from './callDispose'; import type { RiveViewRef } from '../index'; export interface RiveViewProps - extends Omit, 'onError' | 'onStop'> { + extends Omit< + ComponentProps, + 'onError' | 'onStop' | 'renderEnabled' + > { onError?: (error: RiveError) => void; /** * Called when the animation/state machine stops playing, e.g. when a @@ -14,6 +17,15 @@ export interface RiveViewProps * animations where you want to navigate away once playback finishes. */ onStop?: () => void; + /** + * Manual control over rendering, for occlusion the view cannot detect + * itself (e.g. covered by a Modal or a bottom sheet): false skips draws + * while the state machine keeps advancing, 'pause' also stops advancing — + * like an imperative pause() that composes with the ref methods. Defaults + * to true. New runtimes only; on iOS false behaves like true ('pause' is + * fully supported). + */ + renderEnabled?: boolean | 'pause'; } const defaultOnError = (error: RiveError) => @@ -46,7 +58,7 @@ const defaultOnStop = () => {}; * @property {Fit} [fit] - How the Rive graphic should fit within its container * @property {number | FrameRateRange} [frameRate] - Preferred frame rate for the render loop (new runtimes only) * @property {OffscreenBehavior} [offscreenBehavior='none'] - What to do while the view is outside the visible viewport: keep running, keep advancing but skip draws, or pause (new runtimes only) - * @property {boolean} [renderEnabled=true] - Set false to skip drawing while the state machine keeps advancing, for views covered by UI the view can't detect (new Android runtime only) + * @property {boolean | 'pause'} [renderEnabled=true] - false skips drawing while the state machine keeps advancing; 'pause' also stops advancing, for views covered by UI the view can't detect (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) diff --git a/src/specs/RiveView.nitro.ts b/src/specs/RiveView.nitro.ts index 088886b6..97bda2aa 100644 --- a/src/specs/RiveView.nitro.ts +++ b/src/specs/RiveView.nitro.ts @@ -91,17 +91,30 @@ export interface RiveViewProps extends HybridViewProps { */ offscreenBehavior?: OffscreenBehavior; /** - * When false, the view stops drawing frames while the state machine keeps - * advancing (events, data binding, and playback time stay live). Use it - * when the app knows the view can't be seen — e.g. covered by a Modal or a - * bottom sheet — which automatic visibility detection cannot observe. The - * view repaints on the next frame after re-enabling. Defaults to true. + * Manual control over rendering, for occlusion the view cannot detect + * itself — e.g. covered by a Modal or a bottom sheet. Use + * offscreenBehavior instead for visibility the view can detect (scrolling, + * hiding). Defaults to true. * - * Only the new (default) Android runtime skips draws; iOS ignores it for - * now (the upstream runtime couples advancing and drawing, and its - * fullscreen modals already hide the covered hierarchy). + * - true (default): render normally. + * - false: stop drawing frames while the state machine keeps advancing + * (events, data binding, and playback time stay live). The view repaints + * on the next frame after re-enabling. + * - 'pause': also stop advancing the state machine, like an imperative + * pause(); playback resumes where it left off when the prop changes + * back. Unlike pause()/play() this doesn't touch the playback state the + * ref methods control — the two combine. + * + * Only supported on the new (default) runtimes; the legacy backends ignore + * it. On iOS the upstream runtime couples advancing and drawing, so false + * behaves like true there ('pause' is fully supported, and fullscreen iOS + * modals already hide the covered hierarchy). + * + * Typed as `boolean | string` because Nitro cannot mix string literals + * into a variant; the public RiveView component narrows it to + * `boolean | 'pause'`. Strings other than 'pause' render normally. */ - renderEnabled?: boolean; + renderEnabled?: boolean | string; /** * Exposes accessibility semantics authored in the Rive editor to the * platform screen reader (VoiceOver). Defaults to Semantics.Off.