Skip to content

Commit 70e34cf

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/rive-ios-experimental
# Conflicts: # example/package.json # package.json # yarn.lock
2 parents 78a68fa + 11f5c36 commit 70e34cf

6 files changed

Lines changed: 173 additions & 9 deletions

File tree

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.4.10"
2+
".": "0.4.11"
33
}

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [0.4.11](https://github.com/rive-app/rive-nitro-react-native/compare/v0.4.10...v0.4.11) (2026-06-08)
4+
5+
6+
### Bug Fixes
7+
8+
* **android:** defer autoplay until after VM instance is bound ([#283](https://github.com/rive-app/rive-nitro-react-native/issues/283)) ([f600a47](https://github.com/rive-app/rive-nitro-react-native/commit/f600a471bcb55f4cf3b981794f6560b7ac251e92))
9+
310
## [0.4.10](https://github.com/rive-app/rive-nitro-react-native/compare/v0.4.9...v0.4.10) (2026-05-28)
411

512

android/src/legacy/java/com/rive/RiveReactNativeView.kt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,29 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
106106
riveAnimationView?.layoutScaleFactor = config.layoutScaleFactor
107107
?: resources.displayMetrics.density
108108

109+
val hasDataBinding = when (config.bindData) {
110+
is BindData.None -> false
111+
is BindData.Auto -> false
112+
is BindData.Instance, is BindData.ByName -> true
113+
}
114+
109115
if (reload) {
110-
val hasDataBinding = when (config.bindData) {
111-
is BindData.None -> false
112-
is BindData.Auto -> false
113-
is BindData.Instance, is BindData.ByName -> true
114-
}
116+
val deferAutoPlay = hasDataBinding && config.autoPlay
115117
riveAnimationView?.setRiveFile(
116118
config.riveFile,
117119
artboardName = config.artboardName,
118120
stateMachineName = config.stateMachineName,
119-
autoplay = config.autoPlay,
120-
autoBind = hasDataBinding,
121+
autoplay = if (deferAutoPlay) false else config.autoPlay,
122+
autoBind = if (deferAutoPlay) false else hasDataBinding,
121123
alignment = config.alignment,
122124
fit = config.fit
123125
)
126+
if (deferAutoPlay) {
127+
// Create the state machine without advancing Entry transitions.
128+
// The first Choreographer frame will advance it with the user's
129+
// VM instance already bound via applyDataBinding() below.
130+
riveAnimationView?.play(settleInitialState = false)
131+
}
124132
_activeStateMachineName = getSafeStateMachineName()
125133
} else {
126134
riveAnimationView?.alignment = config.alignment
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import {
2+
describe,
3+
it,
4+
expect,
5+
render,
6+
waitFor,
7+
cleanup,
8+
} from 'react-native-harness';
9+
import { View } from 'react-native';
10+
import {
11+
RiveView,
12+
RiveFileFactory,
13+
Fit,
14+
type RiveFile,
15+
type RiveViewRef,
16+
type ViewModelInstance,
17+
} from '@rive-app/react-native';
18+
19+
// .riv file with two ViewModel number properties:
20+
// - input: value we set before mount (default -1)
21+
// - inputOnEntry: state machine copies input into this on Entry transition (default -100)
22+
// Source: https://rive.app/community/files/27852-52628-on-entry-actions-reproducer
23+
const ON_ENTRY_TEST = require('../assets/rive/on_entry_test.riv');
24+
25+
async function loadFile() {
26+
const file = await RiveFileFactory.fromSource(ON_ENTRY_TEST, undefined);
27+
return file;
28+
}
29+
30+
function expectDefined<T>(value: T): asserts value is NonNullable<T> {
31+
expect(value).toBeDefined();
32+
}
33+
34+
type TestContext = {
35+
ref: RiveViewRef | null;
36+
error: string | null;
37+
};
38+
39+
function TestView({
40+
file,
41+
instance,
42+
context,
43+
}: {
44+
file: RiveFile;
45+
instance: ViewModelInstance;
46+
context: TestContext;
47+
}) {
48+
return (
49+
<View style={{ width: 200, height: 200 }}>
50+
<RiveView
51+
hybridRef={{
52+
f: (ref: RiveViewRef | null) => {
53+
context.ref = ref;
54+
},
55+
}}
56+
style={{ flex: 1 }}
57+
file={file}
58+
autoPlay={true}
59+
dataBind={instance}
60+
fit={Fit.Contain}
61+
onError={(e) => {
62+
context.error = e.message;
63+
}}
64+
/>
65+
</View>
66+
);
67+
}
68+
69+
function waitForInputOnEntry(
70+
instance: ViewModelInstance,
71+
timeout = 5000
72+
): Promise<number> {
73+
return new Promise((resolve, reject) => {
74+
const prop = instance.numberProperty('inputOnEntry');
75+
if (!prop) {
76+
reject(new Error("Property 'inputOnEntry' not found"));
77+
return;
78+
}
79+
80+
function done(value: number) {
81+
clearTimeout(timer);
82+
clearInterval(pollTimer);
83+
removeListener();
84+
resolve(value);
85+
}
86+
87+
const timer = setTimeout(() => {
88+
clearInterval(pollTimer);
89+
removeListener();
90+
// If still -100 after timeout, resolve with current value (Entry never ran)
91+
resolve(prop.value);
92+
}, timeout);
93+
94+
const removeListener = prop.addListener((newValue: number) => {
95+
if (newValue !== -100) {
96+
done(newValue);
97+
}
98+
});
99+
100+
const pollTimer = setInterval(() => {
101+
const val = prop.value;
102+
if (val !== -100) {
103+
done(val);
104+
}
105+
}, 50);
106+
});
107+
}
108+
109+
describe('Entry transition uses user VM values (issue #282)', () => {
110+
it('inputOnEntry reflects user-set input value, not the default', async () => {
111+
const file = await loadFile();
112+
const vm = file.defaultArtboardViewModel();
113+
expectDefined(vm);
114+
const instance = vm.createDefaultInstance();
115+
expectDefined(instance);
116+
117+
// Set input to 42 before mounting — Entry should copy this into inputOnEntry
118+
const inputProp = instance.numberProperty('input');
119+
expectDefined(inputProp);
120+
// input default may vary per .riv file
121+
inputProp.value = 42;
122+
123+
const inputOnEntryProp = instance.numberProperty('inputOnEntry');
124+
expectDefined(inputOnEntryProp);
125+
expect(inputOnEntryProp.value).toBe(-100); // default, not yet evaluated
126+
127+
const context: TestContext = { ref: null, error: null };
128+
await render(
129+
<TestView file={file} instance={instance} context={context} />
130+
);
131+
132+
await waitFor(
133+
() => {
134+
expect(context.ref).not.toBeNull();
135+
},
136+
{ timeout: 5000 }
137+
);
138+
139+
const inputOnEntry = await waitForInputOnEntry(instance);
140+
141+
// Entry should have seen input=42 and copied it to inputOnEntry
142+
// Bug: on Android, Entry evaluates with the default VM instance (input=-1)
143+
// before the user's instance (input=42) is bound
144+
expect(inputOnEntry).toBe(42);
145+
expect(context.error).toBeNull();
146+
147+
cleanup();
148+
});
149+
});
848 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rive-app/react-native",
3-
"version": "0.4.10",
3+
"version": "0.4.11",
44
"description": "Rive React Native",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

0 commit comments

Comments
 (0)