Skip to content
Closed
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
74 changes: 30 additions & 44 deletions example/src/pages/DataBindingListExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
ActivityIndicator,
TouchableOpacity,
} from 'react-native';
import { useMemo, useState, useCallback, useRef } from 'react';
import { useMemo, useState, useRef } from 'react';
import {
Fit,
RiveView,
type ViewModelInstance,
type RiveFile,
type RiveViewRef,
useRiveFile,
useRiveList,
} from '@rive-app/react-native';
import { type Metadata } from '../helpers/metadata';

Expand Down Expand Up @@ -65,18 +66,10 @@ function ListExample({
}) {
const riveRef = useRef<RiveViewRef>(null);
const [isPlaying, setIsPlaying] = useState(true);
const listProperty = useMemo(
() => instance.listProperty('ListItemVM'),
[instance]
);
const [listLength, setListLength] = useState(listProperty?.length ?? 0);

const refreshLength = useCallback(() => {
setListLength(listProperty?.length ?? 0);
}, [listProperty]);
const { length, addInstance, removeInstanceAt, swap, getInstanceAt, error } =
useRiveList('ListItemVM', instance);

const handleAddItem = useCallback(() => {
if (!listProperty) return;
const handleAddItem = () => {
const buttonVM = file.viewModelByName('button VM');
if (!buttonVM) {
console.error('button VM view model not found');
Expand All @@ -91,54 +84,47 @@ function ListExample({
if (stringProp) {
stringProp.value = 'new btn';
}
listProperty.addInstance(newInstance);
addInstance(newInstance);
riveRef.current?.playIfNeeded();
refreshLength();
}, [listProperty, file, refreshLength]);
};

const handleRemoveFirst = useCallback(() => {
if (!listProperty || listProperty.length === 0) return;
listProperty.removeInstanceAt(0);
const handleRemoveFirst = () => {
if (length === 0) return;
removeInstanceAt(0);
riveRef.current?.playIfNeeded();
refreshLength();
}, [listProperty, refreshLength]);
};

const handleRemoveLast = useCallback(() => {
if (!listProperty || listProperty.length === 0) return;
listProperty.removeInstanceAt(listProperty.length - 1);
const handleRemoveLast = () => {
if (length === 0) return;
removeInstanceAt(length - 1);
riveRef.current?.playIfNeeded();
refreshLength();
}, [listProperty, refreshLength]);
};

const handleSwapFirstTwo = useCallback(() => {
if (!listProperty || listProperty.length < 2) return;
listProperty.swap(0, 1);
const handleSwapFirstTwo = () => {
if (length < 2) return;
swap(0, 1);
riveRef.current?.playIfNeeded();
refreshLength();
}, [listProperty, refreshLength]);

const logListItems = useCallback(() => {
if (!listProperty) return;
console.log(`List has ${listProperty.length} items:`);
for (let i = 0; i < listProperty.length; i++) {
const item = listProperty.getInstanceAt(i);
};

const logListItems = () => {
console.log(`List has ${length} items:`);
for (let i = 0; i < length; i++) {
const item = getInstanceAt(i);
console.log(` [${i}]: ${item?.instanceName ?? 'undefined'}`);
}
}, [listProperty]);
};

const handlePlayPause = useCallback(() => {
const handlePlayPause = () => {
if (isPlaying) {
riveRef.current?.pause();
} else {
riveRef.current?.play();
}
setIsPlaying(!isPlaying);
}, [isPlaying]);
};

if (!listProperty) {
return (
<Text style={styles.errorText}>ListItemVM list property not found</Text>
);
if (error) {
return <Text style={styles.errorText}>{error.message}</Text>;
}

return (
Expand All @@ -156,7 +142,7 @@ function ListExample({
file={file}
/>
<View style={styles.controls}>
<Text style={styles.infoText}>List length: {listLength}</Text>
<Text style={styles.infoText}>List length: {length}</Text>
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.button} onPress={handleAddItem}>
<Text style={styles.buttonText}>Add Item</Text>
Expand Down
85 changes: 71 additions & 14 deletions example/src/pages/ResponsiveLayouts.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { Fit, RiveView, useRiveFile } from '@rive-app/react-native';
import { useState, useRef, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
Button,
ActivityIndicator,
useWindowDimensions,
} from 'react-native';
import {
Fit,
RiveView,
useRiveFile,
type RiveViewRef,
} from '@rive-app/react-native';
import { type Metadata } from '../helpers/metadata';

/**
Expand All @@ -11,6 +24,22 @@ export default function ResponsiveLayoutsExample() {
const { riveFile, isLoading, error } = useRiveFile(
require('../../assets/rive/layouts_demo.riv')
);
const [scaleFactor, setScaleFactor] = useState(4.0);
const riveRef = useRef<RiveViewRef>(null);
const { width, height } = useWindowDimensions();

useEffect(() => {
riveRef.current?.playIfNeeded();
}, [width, height]);

const increaseScale = () => {
setScaleFactor((prev) => prev + 0.5);
riveRef.current?.playIfNeeded();
};
const decreaseScale = () => {
setScaleFactor((prev) => Math.max(0.5, prev - 0.5));
riveRef.current?.playIfNeeded();
};

return (
<View style={styles.container}>
Expand All @@ -20,36 +49,64 @@ export default function ResponsiveLayoutsExample() {
<Text style={styles.errorText}>{error}</Text>
) : riveFile ? (
<RiveView
hybridRef={{ f: (ref) => (riveRef.current = ref) }}
file={riveFile}
fit={Fit.Layout}
layoutScaleFactor={1} // adjust the layout scale factor to control the layout scale
layoutScaleFactor={scaleFactor}
style={styles.rive}
autoPlay={true}
/>
) : null}
<View style={styles.controls}>
<Text style={styles.label}>Layout Scale Factor</Text>
<View style={styles.scaleControls}>
<Button title="-" onPress={decreaseScale} />
<View style={styles.scaleText}>
<Text>{scaleFactor.toFixed(1)}x</Text>
</View>
<Button title="+" onPress={increaseScale} />
</View>
</View>
</View>
);
}

ResponsiveLayoutsExample.metadata = {
name: 'Responsive Layouts',
description: 'Interactive layout scale factor controls',
} satisfies Metadata;

const styles = StyleSheet.create({
// Adjust the container size and the layout will adjust based on the .riv file layout rules
container: {
width: '100%',
height: '100%',
flex: 1,
},
rive: {
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
flex: 1,
},
controls: {
padding: 16,
alignItems: 'center',
},
scaleControls: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 16,
gap: 16,
},
scaleText: {
minWidth: 50,
alignItems: 'center',
},
label: {
fontSize: 16,
fontWeight: '500',
marginTop: 16,
},
errorText: {
color: 'red',
textAlign: 'center',
padding: 20,
flex: 1,
},
});

ResponsiveLayoutsExample.metadata = {
name: 'Responsive Layouts',
description: 'Sample .riv file with responsive layouts',
} satisfies Metadata;
8 changes: 7 additions & 1 deletion expo-example/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "expo-example",
"slug": "expo-example",
"version": "1.0.0",
"orientation": "portrait",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "expoexample",
"userInterfaceStyle": "automatic",
Expand Down Expand Up @@ -40,6 +40,12 @@
"backgroundColor": "#000000"
}
}
],
[
"expo-screen-orientation",
{
"initialOrientation": "DEFAULT"
}
]
],
"experiments": {
Expand Down
1 change: 1 addition & 0 deletions expo-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"expo-image": "~3.0.10",
"expo-linking": "~8.0.8",
"expo-router": "~6.0.13",
"expo-screen-orientation": "~9.0.8",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"expo-symbols": "~1.0.7",
Expand Down
109 changes: 109 additions & 0 deletions src/hooks/useRiveList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { useCallback, useEffect, useState, useMemo } from 'react';
import type { ViewModelInstance } from '../specs/ViewModel.nitro';
import type { UseRiveListResult } from '../types';

/**
* Hook for interacting with list ViewModel instance properties.
*
* @param path - The path to the list property
* @param viewModelInstance - The ViewModelInstance containing the list property
* @returns An object with list length, manipulation methods, and error state
*/
export function useRiveList(
path: string,
viewModelInstance?: ViewModelInstance | null
): UseRiveListResult {
const [error, setError] = useState<Error | null>(null);
const [revision, setRevision] = useState(0);

useEffect(() => {
setError(null);
}, [path, viewModelInstance]);

const property = useMemo(() => {
if (!viewModelInstance) return undefined;
return viewModelInstance.listProperty(path);
}, [viewModelInstance, path]);

useEffect(() => {
if (viewModelInstance && !property) {
setError(
new Error(`List property "${path}" not found in the ViewModel instance`)
);
}
}, [viewModelInstance, property, path]);

useEffect(() => {
if (!property) return;

const removeListener = property.addListener(() => {
setRevision((r) => r + 1);
});

return () => {
removeListener();
property.removeListeners();
property.dispose();
};
}, [property]);

const length = useMemo(() => {
// revision is used to trigger re-computation when list changes

revision;
return property?.length ?? 0;
}, [property, revision]);

const getInstanceAt = useCallback(
(index: number) => {
return property?.getInstanceAt(index);
},
[property]
);

const addInstance = useCallback(
(instance: ViewModelInstance) => {
property?.addInstance(instance);
},
[property]
);

const addInstanceAt = useCallback(
(instance: ViewModelInstance, index: number) => {
return property?.addInstanceAt(instance, index) ?? false;
},
[property]
);

const removeInstance = useCallback(
(instance: ViewModelInstance) => {
property?.removeInstance(instance);
},
[property]
);

const removeInstanceAt = useCallback(
(index: number) => {
property?.removeInstanceAt(index);
},
[property]
);

const swap = useCallback(
(index1: number, index2: number) => {
return property?.swap(index1, index2) ?? false;
},
[property]
);

return {
length,
getInstanceAt,
addInstance,
addInstanceAt,
removeInstance,
removeInstanceAt,
swap,
error,
};
}
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export { useRiveBoolean } from './hooks/useRiveBoolean';
export { useRiveEnum } from './hooks/useRiveEnum';
export { useRiveColor } from './hooks/useRiveColor';
export { useRiveTrigger } from './hooks/useRiveTrigger';
export { useRiveList } from './hooks/useRiveList';
export { useRiveFile } from './hooks/useRiveFile';
export { type RiveFileInput } from './hooks/useRiveFile';
export { DataBindMode };
Loading
Loading