Skip to content

Commit 5006f9b

Browse files
committed
feat: ViewModelImageProperty
1 parent 3bf54ec commit 5006f9b

33 files changed

Lines changed: 949 additions & 2 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.margelo.nitro.rive
2+
3+
import androidx.annotation.Keep
4+
import app.rive.runtime.kotlin.core.ViewModelImageProperty
5+
import com.facebook.proguard.annotations.DoNotStrip
6+
import kotlinx.coroutines.flow.map
7+
8+
@Keep
9+
@DoNotStrip
10+
class HybridViewModelImageProperty(private val viewModelImage: ViewModelImageProperty) :
11+
HybridViewModelImagePropertySpec(),
12+
BaseHybridViewModelProperty<Unit> by BaseHybridViewModelPropertyImpl() {
13+
override fun set(image: HybridRiveImageSpec?) {
14+
viewModelImage.value = (image as? HybridRiveImage)?.renderImage
15+
}
16+
17+
override fun addListener(onChanged: () -> Unit) {
18+
listeners.add(onChanged)
19+
ensureValueListenerJob(viewModelImage.valueFlow.map { })
20+
}
21+
}

android/src/main/java/com/margelo/nitro/rive/HybridViewModelInstance.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,13 @@ class HybridViewModelInstance(val viewModelInstance: ViewModelInstance) : Hybrid
6464
return null
6565
}
6666
}
67+
68+
override fun imageProperty(path: String): HybridViewModelImagePropertySpec? {
69+
try {
70+
val imageProperty = viewModelInstance.getImageProperty(path)
71+
return HybridViewModelImageProperty(imageProperty)
72+
} catch (e: ViewModelException) {
73+
return null
74+
}
75+
}
6776
}
2.2 MB
Binary file not shown.

example/src/pages/ManyViewModels.tsx

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
2-
import { useState, useMemo } from 'react';
1+
import { StyleSheet, View, Text, TouchableOpacity, Button } from 'react-native';
2+
import { useState, useMemo, useRef } from 'react';
3+
import { hybridRef } from 'react-native-nitro-modules';
34
import type { Metadata } from '../helpers/metadata';
45
import {
56
DataBindMode,
67
RiveView,
78
useRiveFile,
89
type ViewModelInstance,
10+
RiveImages,
11+
type RiveViewRef,
912
} from '@rive-app/react-native';
1013

1114
type BindModeOption =
@@ -77,6 +80,9 @@ export default function ManyViewModels() {
7780
require('../../assets/rive/many_viewmodels.riv')
7881
);
7982
const [bindMode, setBindMode] = useState<BindModeOption>('none');
83+
const [isLoadingImage, setIsLoadingImage] = useState(false);
84+
const [imageError, setImageError] = useState<string | null>(null);
85+
const riveViewRef = useRef<RiveViewRef>(undefined);
8086

8187
// Create a ViewModelInstance for "green" to demonstrate instance binding
8288
const greenInstance = useMemo(() => {
@@ -91,13 +97,59 @@ export default function ManyViewModels() {
9197
}
9298
}, [riveFile]);
9399

100+
const handleLoadImage = async () => {
101+
if (!riveViewRef.current) return;
102+
103+
setIsLoadingImage(true);
104+
setImageError(null);
105+
try {
106+
const viewModelInstance = riveViewRef.current.getViewModelInstance();
107+
if (!viewModelInstance) {
108+
setImageError('No view model instance found in view');
109+
return;
110+
}
111+
112+
const imgProp = viewModelInstance.imageProperty('imageValue');
113+
if (!imgProp) {
114+
setImageError('Image property "imageValue" not found');
115+
return;
116+
}
117+
118+
const riveImage = await RiveImages.loadFromURLAsync(
119+
'https://picsum.photos/id/372/500/500'
120+
);
121+
imgProp.set(riveImage);
122+
123+
imgProp.addListener(() => {
124+
console.log('Image property changed!');
125+
});
126+
127+
console.log('Image loaded and set successfully');
128+
} catch (err) {
129+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
130+
setImageError(errorMsg);
131+
console.error('Failed to load image:', errorMsg);
132+
} finally {
133+
setIsLoadingImage(false);
134+
}
135+
};
136+
94137
const dataBindValue = getDataBindValue(bindMode, greenInstance);
95138

96139
return (
97140
<View style={styles.container}>
98141
<BindModeSelector selectedMode={bindMode} onModeChange={setBindMode} />
142+
<View style={styles.imageButtonContainer}>
143+
<Button
144+
title={isLoadingImage ? 'Loading Image...' : 'Load Test Image'}
145+
onPress={handleLoadImage}
146+
disabled={isLoadingImage || !riveFile}
147+
/>
148+
{imageError && <Text style={styles.errorText}>{imageError}</Text>}
149+
</View>
99150
{riveFile && (
100151
<RiveView
152+
hybridRef={{ f: (ref) => (riveViewRef.current = ref) }}
101153
style={styles.rive}
102154
file={riveFile}
103155
dataBind={dataBindValue}
@@ -119,6 +171,17 @@ const styles = StyleSheet.create({
119171
flex: 1,
120172
backgroundColor: '#fff',
121173
},
174+
imageButtonContainer: {
175+
padding: 16,
176+
backgroundColor: '#fff',
177+
borderBottomWidth: 1,
178+
borderBottomColor: '#e0e0e0',
179+
},
180+
errorText: {
181+
color: 'red',
182+
marginTop: 8,
183+
fontSize: 12,
184+
},
122185
rive: {
123186
flex: 1,
124187
width: '100%',
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import RiveRuntime
2+
3+
class HybridViewModelImageProperty: HybridViewModelImagePropertySpec {
4+
var property: RiveDataBindingViewModel.Instance.ImageProperty!
5+
private var listenerIds: [UUID] = []
6+
7+
init(property: RiveDataBindingViewModel.Instance.ImageProperty) {
8+
self.property = property
9+
super.init()
10+
}
11+
12+
/// ⚠️ DO NOT REMOVE
13+
/// Nitro requires a parameterless initializer for JS bridging.
14+
/// This is invoked automatically during hybrid module construction.
15+
/// Internally we always use `init(property:)`
16+
override init() {
17+
super.init()
18+
}
19+
20+
func set(image: HybridRiveImageSpec?) throws {
21+
if let hybridImage = image as? HybridRiveImage {
22+
property.setValue(hybridImage.renderImage)
23+
} else {
24+
property.setValue(nil)
25+
}
26+
}
27+
28+
func addListener(onChanged: @escaping () -> Void) throws {
29+
let id = property.addListener({
30+
onChanged()
31+
})
32+
33+
listenerIds.append(id)
34+
}
35+
36+
func removeListeners() throws {
37+
for id in listenerIds {
38+
property.removeListener(id)
39+
}
40+
listenerIds.removeAll()
41+
}
42+
43+
func dispose() throws {
44+
try? removeListeners()
45+
}
46+
}

ios/HybridViewModelInstance.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,9 @@ class HybridViewModelInstance: HybridViewModelInstanceSpec {
4343
guard let property = viewModelInstance?.triggerProperty(fromPath: path) else { return nil }
4444
return HybridViewModelTriggerProperty(property: property)
4545
}
46+
47+
func imageProperty(path: String) throws -> (any HybridViewModelImagePropertySpec)? {
48+
guard let property = viewModelInstance?.imageProperty(fromPath: path) else { return nil }
49+
return HybridViewModelImageProperty(property: property)
50+
}
4651
}

nitro.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
"swift": "HybridViewModelTriggerProperty",
5454
"kotlin": "HybridViewModelTriggerProperty"
5555
},
56+
"ViewModelImageProperty": {
57+
"swift": "HybridViewModelImageProperty",
58+
"kotlin": "HybridViewModelImageProperty"
59+
},
5660
"RiveView": {
5761
"swift": "HybridRiveView",
5862
"kotlin": "HybridRiveView"

nitrogen/generated/android/c++/JHybridViewModelImagePropertySpec.cpp

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nitrogen/generated/android/c++/JHybridViewModelImagePropertySpec.hpp

Lines changed: 68 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nitrogen/generated/android/c++/JHybridViewModelInstanceSpec.cpp

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)