Skip to content
This repository has been archived by the owner on Aug 9, 2023. It is now read-only.

Add useFrameCallback #31

Merged
merged 5 commits into from
Jul 18, 2023
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
4 changes: 4 additions & 0 deletions docs/advanced/measure.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
sidebar_position: 1
---

# measure

`measure` lets you synchronously get the dimensions and the position of a view on the screen on the [UI thread](/docs/fundamentals/glossary#ui-thread).
Expand Down
4 changes: 4 additions & 0 deletions docs/advanced/useAnimatedReaction.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
sidebar_position: 3
---

# useAnimatedReaction

`useAnimatedReaction` lets you react to a change of a [shared value](/docs/fundamentals/glossary#shared-value). Especially useful when comparing values previously stored in shared value with the current one.
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced/useAnimatedRef.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 1
sidebar_position: 2
---

# useAnimatedRef
Expand Down
106 changes: 106 additions & 0 deletions docs/advanced/useFrameCallback.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
sidebar_position: 4
---

# useFrameCallback

`useFrameCallback` lets you run a function on every frame update.

import FrameCallbackSimple from "@site/src/examples/FrameCallbackSimple";
import FrameCallbackSimpleSrc from "!!raw-loader!@site/src/examples/FrameCallbackSimple";

<InteractiveExample
src={FrameCallbackSimpleSrc}
component={<FrameCallbackSimple />}
/>

## Reference

```javascript
import { useFrameCallback } from "react-native-reanimated";

function App() {
const frameCallback = useFrameCallback((frameInfo) => {
// Increment a value on every frame update
sv.value += 1;
});

return (
<Button
title="Start/Stop"
onPress={() => frameCallback.setActive(!frameCallback.isActive)}
/>
);
}
```

<details>
<summary>Type definitions</summary>

```typescript
type FrameInfo = {
timestamp: number;
timeSincePreviousFrame: number | null;
timeSinceFirstFrame: number;
};

type FrameCallback = {
setActive: (isActive: boolean) => void;
isActive: boolean;
callbackId: number;
};

function useFrameCallback(
callback: (frameInfo: FrameInfo) => void,
autostart = true
): FrameCallback;
```

</details>

### Arguments

#### `callback`

A function to be executed on every frame update. This function receives a `frameInfo` object containing fields:

- `timestamp` a number indicating the system time (in milliseconds) when the last frame was rendered.
- `timeSincePreviousFrame` a number indicating the time (in milliseconds) since last frame. This value will be null on the first frame after activation. Starting from the second frame, it should be ~16 ms on 60 Hz, and ~8 ms on 120 Hz displays (provided there are no frame dropped).
- `timeSinceFirstFrame` a number indicating the time (in milliseconds) since the callback was activated.

#### `autostart` <Optional />

Whether the callback should start automatically. Defaults to `true`.

### Returns

`useFrameCallback` returns an object containing these fields:

- `setActive` a function that lets you start the frame callback or stop it from running
- `isActive` a boolean indicating whether a callback is running
- `callbackId` a number indicating a unique identifier of the frame callback

## Example

import FrameCallbackDino from "@site/src/examples/FrameCallbackDino";
import FrameCallbackDinoSrc from "!!raw-loader!@site/src/examples/FrameCallbackDino";

<InteractiveExample
src={FrameCallbackDinoSrc}
component={<FrameCallbackDino />}
label="Tap to jump"
/>

## Remarks

- A function passed to the `callback` argument is automatically [workletized](/docs/fundamentals/glossary#to-workletize) and ran on the [UI thread](/docs/fundamentals/glossary#ui-thread).

## Platform compatibility

<div className="compatibility">

| Android | iOS | Web |
| ------- | --- | --- |
| ✅ | ✅ | ✅ |

</div>
180 changes: 180 additions & 0 deletions src/examples/FrameCallbackDino.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import Animated, {
useSharedValue,
useFrameCallback,
useAnimatedStyle,
withSequence,
withTiming,
Easing,
} from "react-native-reanimated";
import { View, StyleSheet, Pressable, Text } from "react-native";
import React from "react";

const HEIGHT = 200;
const DEFAULT_VELOCITY = 10;
const VELOCITY_INCREMENT = 0.005;
const GROUND_LEVEL = 80;
const DEFAULT_Y = HEIGHT - GROUND_LEVEL - 80;
const DEFAULT_X = 1000;

const DEFAULT_OBSTACLE = {
height: 85,
width: 82,
x: 0,
y: DEFAULT_Y,
};
const DEFAULT_HORSE = {
height: 85,
width: 82,
x: 0,
y: DEFAULT_Y,
};

export default function FrameCallbackDino() {
const x = useSharedValue(0);
const vx = useSharedValue(DEFAULT_VELOCITY);

const width = useSharedValue(0);

const getDimensions = (event) => {
width.value = event.nativeEvent.layout.width;
};

const obstacleX = useSharedValue(DEFAULT_X);
const horseY = useSharedValue(DEFAULT_Y);

const gameOver = useSharedValue(false);

// highlight-next-line
useFrameCallback((frameInfo) => {
const { timeSincePreviousFrame: dt } = frameInfo;
if (dt == null) {
return;
}

const horse = { ...DEFAULT_HORSE, y: horseY.value };
const obstacle = { ...DEFAULT_OBSTACLE, x: obstacleX.value };

if (isColliding(horse, obstacle) || gameOver.value) {
gameOver.value = true;
return;
}

x.value += dt * vx.value;
obstacleX.value =
obstacleX.value > -100 ? obstacleX.value - vx.value : width.value;

vx.value += VELOCITY_INCREMENT;
// highlight-next-line
});

const obstacleStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: obstacleX.value },
{ translateY: DEFAULT_OBSTACLE.y },
],
}));

const horseStyles = useAnimatedStyle(() => ({
transform: [
{ translateX: DEFAULT_HORSE.x },
{ translateY: horseY.value },
{ rotateY: "180deg" },
],
}));

const overlayStyles = useAnimatedStyle(() => ({
transform: [{ translateY: gameOver.value === true ? 0 : -1000 }],
}));

const handleTap = () => {
if (gameOver.value) {
handleRestart();
} else {
handleJump();
}
};

const handleJump = () => {
if (horseY.value === DEFAULT_Y) {
horseY.value = withSequence(
withTiming(DEFAULT_Y - 120, {
easing: Easing.bezier(0.3, 0.11, 0.15, 0.97),
}),
withTiming(DEFAULT_Y, { easing: Easing.poly(4) })
);
}
};

const handleRestart = () => {
gameOver.value = false;
obstacleX.value = DEFAULT_X;
horseY.value = DEFAULT_Y;
vx.value = DEFAULT_VELOCITY;
x.value = 0;
};

return (
<>
<Pressable
style={styles.container}
onLayout={getDimensions}
onPressIn={handleTap}
>
<Animated.View style={[styles.overlay, overlayStyles]}>
<Text style={styles.text}>Game Over</Text>
</Animated.View>
<Animated.Text style={[styles.obstacle, obstacleStyles]}>
🌵
</Animated.Text>
<Animated.Text style={[styles.horse, horseStyles]}>🐎</Animated.Text>
<View style={styles.ground} />
</Pressable>
</>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
height: 200,
},
horse: {
position: "absolute",
fontSize: 80,
},
ground: {
position: "absolute",
right: 0,
bottom: GROUND_LEVEL - 10,
width: "100%",
height: 2,
backgroundColor: "#000",
},
obstacle: {
position: "absolute",
fontSize: 80,
},
text: {
fontSize: 40,
color: "white",
backgroundColor: "rgba(0,0,0,0.5)",
paddingHorizontal: 8,
},
overlay: {
justifyContent: "center",
alignItems: "center",
position: "absolute",
width: "100%",
zIndex: 1,
},
});

function isColliding(obj1, obj2) {
"worklet";
return (
obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y
);
}
49 changes: 49 additions & 0 deletions src/examples/FrameCallbackSimple.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";
import { StyleSheet, View } from "react-native";
import Animated, {
useFrameCallback,
useSharedValue,
useAnimatedStyle,
} from "react-native-reanimated";

export default function App() {
const t = useSharedValue(0);

// highlight-start
useFrameCallback((frameInfo) => {
t.value = frameInfo.timeSinceFirstFrame / 350;
});
// highlight-end

const infinityStyle = useAnimatedStyle(() => {
const scale = (2 / (3 - Math.cos(2 * t.value))) * 200;
return {
transform: [
{ translateX: scale * Math.cos(t.value) },
{ translateY: scale * (Math.sin(2 * t.value) / 2) },
],
};
});

return (
<View style={styles.container}>
<Animated.View style={[styles.dot, infinityStyle]} />
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
height: 150,
},
dot: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "#b58df1",
position: "absolute",
},
});