Skip to content
Merged
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 @@ -10,6 +10,7 @@ import com.rive.ViewConfiguration
import app.rive.runtime.kotlin.core.Fit as RiveFit
import app.rive.runtime.kotlin.core.Alignment as RiveAlignment
import app.rive.runtime.kotlin.core.errors.*
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

Expand Down Expand Up @@ -268,6 +269,7 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
val (errorType, errorDescription) = detectErrorType(e)
val noteString = note?.let { " $it" } ?: ""
val errorMessage = "[RIVE] $tag$noteString $errorDescription"
Log.e(TAG, errorMessage, e)
val riveError = RiveError(
type = errorType,
message = errorMessage
Expand Down
31 changes: 8 additions & 23 deletions android/src/main/java/com/rive/RiveReactNativeView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
private var eventListeners: MutableList<RiveFileController.RiveEventListener> = mutableListOf()
private val viewReadyDeferred = CompletableDeferred<Boolean>()
private var _activeStateMachineName: String? = null
private var _pendingBindData: BindData? = null
private var willDispose = false

init {
Expand All @@ -104,11 +103,17 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {

fun configure(config: ViewConfiguration, dataBindingChanged: Boolean, reload: Boolean = false, initialUpdate: Boolean = false) {
if (reload) {
val hasDataBinding = when (config.bindData) {
is BindData.None -> false
is BindData.Auto -> config.riveFile.viewModelCount > 0
is BindData.Instance, is BindData.ByName -> true
}
riveAnimationView?.setRiveFile(
config.riveFile,
artboardName = config.artboardName,
stateMachineName = config.stateMachineName,
autoplay = config.autoPlay,
autoBind = hasDataBinding,
alignment = config.alignment,
fit = config.fit
)
Expand All @@ -121,7 +126,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
}

if (dataBindingChanged || initialUpdate || reload) {
applyDataBinding(config.bindData, config.autoPlay)
applyDataBinding(config.bindData)
}

viewReadyDeferred.complete(true)
Expand All @@ -143,35 +148,15 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
}
}

fun applyDataBinding(bindData: BindData, autoPlay: Boolean) {
val stateMachines = riveAnimationView?.controller?.stateMachines
if (stateMachines.isNullOrEmpty()) {
_pendingBindData = bindData
return
}

fun applyDataBinding(bindData: BindData) {
bindToStateMachine(bindData)

if (autoPlay) {
stateMachines.first().name.let { smName ->
riveAnimationView?.play(smName, isStateMachine = true)
}
}
}

fun play() {
if (_activeStateMachineName == null) {
_activeStateMachineName = getSafeStateMachineName()
}
riveAnimationView?.play()
applyPendingBindData()
}

private fun applyPendingBindData() {
_pendingBindData?.let { bindData ->
_pendingBindData = null
bindToStateMachine(bindData)
}
}

fun pause() = riveAnimationView?.pause()
Expand Down
168 changes: 168 additions & 0 deletions example/__tests__/autoplay-false-trigger.harness.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
describe,
it,
expect,
render,
waitFor,
cleanup,
} from 'react-native-harness';
import { useEffect } from 'react';
import { View } from 'react-native';
import {
RiveView,
RiveFileFactory,
Fit,
type RiveFile,
type RiveViewRef,
} from '@rive-app/react-native';
import type { ViewModelInstance } from '@rive-app/react-native';

// quick_start.riv has:
// - "health" number property (default 50)
// - "gameOver" trigger property
// - state machine named "State Machine 1"
const QUICK_START = require('../assets/rive/quick_start.riv');
const RATING = require('../assets/rive/rating.riv');

function expectDefined<T>(value: T): asserts value is NonNullable<T> {
expect(value).toBeDefined();
}

type TestContext = {
ref: RiveViewRef | null;
error: string | null;
};

function AutoPlayFalseView({
file,
instance,
context,
}: {
file: RiveFile;
instance: ViewModelInstance;
context: TestContext;
}) {
useEffect(() => {
return () => {
context.ref = null;
};
}, [context]);

return (
<View style={{ width: 200, height: 200 }}>
<RiveView
hybridRef={{
f: (ref: RiveViewRef | null) => {
context.ref = ref;
},
}}
style={{ flex: 1 }}
file={file}
autoPlay={false}
dataBind={instance}
fit={Fit.Contain}
stateMachineName="State Machine 1"
onError={(e) => {
context.error = e.message;
}}
/>
</View>
);
}

async function loadQuickStart() {
const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
const vm = file.defaultArtboardViewModel();
expectDefined(vm);
const instance = vm.createDefaultInstance();
expectDefined(instance);
return { file, instance };
}

function SimpleRiveView({
file,
context,
}: {
file: RiveFile;
context: TestContext;
}) {
useEffect(() => {
return () => {
context.ref = null;
};
}, [context]);

return (
<View style={{ width: 200, height: 200 }}>
<RiveView
hybridRef={{
f: (ref: RiveViewRef | null) => {
context.ref = ref;
},
}}
style={{ flex: 1 }}
file={file}
fit={Fit.Contain}
onError={(e) => {
context.error = e.message;
}}
/>
</View>
);
}

describe('autoPlay={false} + dataBind (issue #156)', () => {
it('VMI is bound to state machine without play()', async () => {
const { file, instance } = await loadQuickStart();
const context: TestContext = { ref: null, error: null };

const health = instance.numberProperty('health');
expectDefined(health);
health.value = 25;

await render(
<AutoPlayFalseView file={file} instance={instance} context={context} />
);

await waitFor(
() => {
expect(context.ref).not.toBeNull();
},
{ timeout: 5000 }
);

await context.ref!.awaitViewReady();

// Without fix: getViewModelInstance() returns null because
// the SDK never created state machines (autoPlay=false, no autoBind)
const boundVmi = context.ref!.getViewModelInstance();
expect(boundVmi).not.toBeNull();

// The health value we set should be readable
const boundHealth = boundVmi!.numberProperty('health');
expectDefined(boundHealth);
expect(boundHealth.value).toBe(25);

expect(context.error).toBeNull();
cleanup();
});

it('file without data binding renders without error', async () => {
const file = await RiveFileFactory.fromSource(RATING, undefined);
const context: TestContext = { ref: null, error: null };

await render(<SimpleRiveView file={file} context={context} />);

await waitFor(
() => {
expect(context.ref).not.toBeNull();
},
{ timeout: 5000 }
);

// Give it time to render and potentially throw
await new Promise((r) => setTimeout(r, 500));
expect(context.error).toBeNull();
cleanup();
});
});
1 change: 1 addition & 0 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './src/polyfills';
import { AppRegistry } from 'react-native';
import App from './src/App';
import { name as appName } from './app.json';
Expand Down
34 changes: 34 additions & 0 deletions example/src/polyfills.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* global globalThis */
// Polyfill EventTarget and Event for chai 6.x (used by react-native-harness).
// Hermes on RN 0.79 doesn't have these Web APIs.
// Must load before any react-native-harness import.
if (!globalThis.Event) {
globalThis.Event = class Event {
constructor(type) {
this.type = type;
}
};
}
if (!globalThis.EventTarget) {
globalThis.EventTarget = class EventTarget {
constructor() {
this._listeners = {};
}
addEventListener(type, listener) {
(this._listeners[type] ??= []).push(listener);
}
removeEventListener(type, listener) {
const list = this._listeners[type];
if (list) {
this._listeners[type] = list.filter((l) => l !== listener);
}
}
dispatchEvent(event) {
const list = this._listeners[event.type];
if (list) {
list.forEach((l) => l(event));
}
return true;
}
};
}
Loading