From 425f870969f25e4fb2fc90f9735ee47854fc02ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 13 Oct 2026 08:25:57 +0200 Subject: [PATCH] fix(android): resize the render surface when the view size changes Fixes #418 --- .../new/java/com/rive/RiveReactNativeView.kt | 8 +- .../new/java/com/rive/RiveSurfaceResize.kt | 40 +++++++++ .../src/reproducers/Issue418SurfaceResize.tsx | 90 +++++++++++++++++++ 3 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 android/src/new/java/com/rive/RiveSurfaceResize.kt create mode 100644 example/src/reproducers/Issue418SurfaceResize.tsx diff --git a/android/src/new/java/com/rive/RiveReactNativeView.kt b/android/src/new/java/com/rive/RiveReactNativeView.kt index 36d0baf4..69f81bb9 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -151,9 +151,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() } @@ -331,6 +329,10 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { val rs = riveSurface ?: return val art = artboard ?: return art.resizeArtboard(rs, fit.scaleFactor) + // The resize applies on the next advance, which the render loop skips once settled or paused. + val worker = riveWorker ?: return + val sm = stateMachineHandle ?: return + worker.advanceStateMachine(sm, Duration.ZERO) } } diff --git a/android/src/new/java/com/rive/RiveSurfaceResize.kt b/android/src/new/java/com/rive/RiveSurfaceResize.kt new file mode 100644 index 00000000..edf57fe4 --- /dev/null +++ b/android/src/new/java/com/rive/RiveSurfaceResize.kt @@ -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 + } + } +} diff --git a/example/src/reproducers/Issue418SurfaceResize.tsx b/example/src/reproducers/Issue418SurfaceResize.tsx new file mode 100644 index 00000000..87e2cbe5 --- /dev/null +++ b/example/src/reproducers/Issue418SurfaceResize.tsx @@ -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 ( + + + target {isTall ? TALL : SHORT}pt ยท generation {generation} + + {error ? {String(error)} : null} + + + Toggle height + + setGeneration((g) => g + 1)} + > + Remount + + + + {riveFile ? ( + + ) : null} + + + ); +} + +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' }, +});