Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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).
override var semantics: Semantics? = null
Expand Down
19 changes: 19 additions & 0 deletions android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -111,6 +112,24 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
)
}

override var offscreenBehavior: OffscreenBehavior? = null
set(value) {
field = value
view.offscreenBehavior = value ?: OffscreenBehavior.NONE
}

override var renderEnabled: Variant_Boolean_String? = null
set(value) {
field = value
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
// rive-android runtime (iOS-only for now).
override var semantics: Semantics? = null
Expand Down
53 changes: 45 additions & 8 deletions android/src/new/java/com/rive/RiveReactNativeView.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -34,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()
Expand Down Expand Up @@ -71,6 +77,24 @@ 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): 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()

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
Expand Down Expand Up @@ -172,7 +196,11 @@ 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
val renderPaused = renderMode == RenderMode.Paused

if ((paused || pausedOffscreen || renderPaused) && !needsRedraw) {
// Keep the timebase fresh so resuming advances by one frame, not by
// the whole pause span.
lastFrameTimeNs = frameTimeNanos
Expand Down Expand Up @@ -204,17 +232,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 = renderMode != RenderMode.Enabled || offscreen

if (worker != null && art != null && sm != null && rs != null) {
try {
if (!paused && !settled) {
if (!paused && !settled && !pausedOffscreen && !renderPaused) {
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)
Expand Down
Loading
Loading