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
31 changes: 28 additions & 3 deletions android/src/new/java/com/rive/RiveReactNativeView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
// re-rendering identical frames at full refresh rate.
private var needsRedraw = true

private data class LayoutSize(
val artboard: Artboard,
val surface: RiveSurface,
val width: Int,
val height: Int,
val fit: Fit.Layout
)

private var appliedLayout: LayoutSize? = null

// A resize only takes effect on an advance, so a paused or settled view needs one zero advance.
private var layoutAdvancePending = false

@Volatile
private var paused = false

Expand Down Expand Up @@ -151,9 +164,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
this@RiveReactNativeView.surfaceWidth = w
this@RiveReactNativeView.surfaceHeight = h
this@RiveReactNativeView.needsRedraw = true
// Since 11.7.x the render target keeps its creation-time size and
// RiveSurface.resize() is internal to the SDK, so only the artboard
// is resized here (same behavior as before the 11.7.2 bump).
this@RiveReactNativeView.riveSurface?.let { RiveSurfaceResize.resize(it, w, h) }
resizeArtboardIfLayout()
}

Expand Down Expand Up @@ -205,7 +216,10 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
try {
if (!paused && !settled) {
worker.advanceStateMachine(sm, deltaTime)
} else if (layoutAdvancePending) {
worker.advanceStateMachine(sm, Duration.ZERO)
Comment thread
mfazekas marked this conversation as resolved.
Resolved
}
layoutAdvancePending = false
worker.draw(art, sm, rs, activeFit)
needsRedraw = false
frameCount++
Expand Down Expand Up @@ -265,6 +279,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
)

if (reload) {
layoutAdvancePending = false
RiveErrorLogger.resetReportedErrors()
RiveErrorLogger.addListener(errorListener)
stateMachineHandle?.let { old ->
Expand Down Expand Up @@ -330,7 +345,17 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
if (fit is Fit.Layout) {
val rs = riveSurface ?: return
val art = artboard ?: return
val layout = LayoutSize(art, rs, surfaceWidth, surfaceHeight, fit)
if (layout == appliedLayout) return
appliedLayout = layout
art.resizeArtboard(rs, fit.scaleFactor)
// A responsive layout may start animating, so resume advancing like upstream's unsettle().
settled = false
Comment thread
mfazekas marked this conversation as resolved.
Resolved
layoutAdvancePending = true
needsRedraw = true
} else {
appliedLayout = null
layoutAdvancePending = false
Comment thread
mfazekas marked this conversation as resolved.
Resolved
}
}

Expand Down
40 changes: 40 additions & 0 deletions android/src/new/java/com/rive/RiveSurfaceResize.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.rive

import android.util.Log
import app.rive.core.RiveSurface
import java.lang.reflect.InvocationTargetException

/**
* Upstream `RiveView` resizes its surface on every size change through the
* Kotlin-internal `RiveSurface.resize`, so reflection reaches it under its
* mangled JVM name (rive-app/rive-nitro-react-native#418).
*/
internal object RiveSurfaceResize {
private const val TAG = "RiveSurfaceResize"

private val resizeMethod by lazy {
val signature = arrayOf(Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
val method = RiveSurface::class.java.declaredMethods.firstOrNull {
(it.name == "resize" || it.name.startsWith("resize$")) &&
it.parameterTypes.contentEquals(signature)
}
if (method == null) Log.w(TAG, "RiveSurface.resize not found")
method
}

/** Returns false when the surface keeps its previous size. */
fun resize(surface: RiveSurface, width: Int, height: Int): Boolean {
if (width <= 0 || height <= 0) return false
val method = resizeMethod ?: return false
return try {
method.invoke(surface, width, height)
true
} catch (e: InvocationTargetException) {
Log.w(TAG, "RiveSurface.resize(${width}x$height) failed", e.targetException)
false
} catch (e: ReflectiveOperationException) {
Log.w(TAG, "RiveSurface.resize(${width}x$height) failed", e)
false
}
}
}
90 changes: 90 additions & 0 deletions example/src/reproducers/Issue418SurfaceResize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useCallback, useRef, useState } from 'react';
import { Animated, Pressable, Text, View, StyleSheet } from 'react-native';
import { Fit, RiveView, useRiveFile } from '@rive-app/react-native';
import { type Metadata } from '../shared/metadata';

/**
* Issue #418: on the Android new backend, resizing a mounted RiveView kept
* rendering into a surface sized at creation time, so Fit.Layout content
* looked scaled and offset until remount.
*
* Toggle the height, then Remount at the same height: both should look
* identical.
*/

const SHORT = 200;
const TALL = 400;

export default function Issue418SurfaceResize() {
const { riveFile, error } = useRiveFile(
require('../../assets/rive/layout_test.riv')
);
const [height] = useState(() => new Animated.Value(SHORT));
const tall = useRef(false);
const [isTall, setIsTall] = useState(false);
const [generation, setGeneration] = useState(0);

const toggle = useCallback(() => {
tall.current = !tall.current;
setIsTall(tall.current);
Animated.timing(height, {
toValue: tall.current ? TALL : SHORT,
duration: 400,
useNativeDriver: false,
}).start();
}, [height]);

return (
<View style={styles.container}>
<Text style={styles.subtitle}>
target {isTall ? TALL : SHORT}pt · generation {generation}
</Text>
{error ? <Text style={styles.error}>{String(error)}</Text> : null}
<View style={styles.buttons}>
<Pressable style={styles.button} onPress={toggle}>
<Text style={styles.buttonText}>Toggle height</Text>
</Pressable>
<Pressable
style={styles.button}
onPress={() => setGeneration((g) => g + 1)}
>
<Text style={styles.buttonText}>Remount</Text>
</Pressable>
</View>
<Animated.View style={[styles.box, { height }]}>
{riveFile ? (
<RiveView
key={generation}
file={riveFile}
fit={Fit.Layout}
autoPlay
style={styles.rive}
/>
) : null}
</Animated.View>
</View>
);
}

Issue418SurfaceResize.metadata = {
name: 'Issue #418: Surface resize',
description:
'Android Fit.Layout content stale after the view resizes until remount',
} satisfies Metadata;

const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: '#fff' },
subtitle: { fontSize: 14, color: '#666' },
error: { color: 'red', marginTop: 8 },
buttons: { flexDirection: 'row', gap: 8 },
box: { marginTop: 16, backgroundColor: '#1a2a3a' },
rive: { flex: 1 },
button: {
backgroundColor: '#007AFF',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
marginTop: 8,
},
buttonText: { color: '#fff', fontWeight: '600' },
});
Loading