react-native-animation-scenario
Version:
Declarative animation scenario hook for React Native
450 lines (352 loc) โข 13.2 kB
Markdown
# ๐ฌ react-native-animation-scenario
A lightweight, declarative animation timeline hook for React Native โ perfect for scenarios like onboarding, tutorials, interactive demos, and more. Easily control complex UI animations step-by-step or automatically.
Works with [React Native](https://reactnative.dev/) and [Expo](https://expo.dev).
---
## โจ Features
- โ
Declarative animation timeline with step-by-step control
- ๐ง Supports `set`, `move`, `delay`, `parallel`, `callback`, `vibrate`, `hold`, `label`, `goto`, `resume`, `use`, `ifJump`, `ifThen` and `use` steps
- ๐ Works in both `"auto"` and `"manual"` step modes
- ๐ Loopable scenarios
- ๐ฆ Minimal dependencies โ just React Native + Animated
- ๐งฉ Drop-in `TimelineView` for debug or dev overlay
- ๐ Safe callback and ref validation
---
## โก Try it live
[Snack demo on Expo](https://snack.expo.dev/@cadam68/animation-demo)
---
## ๐ฅ What's New in 1.4.3
- โ
documentation added.
## ๐ฅ What's New in 1.4.2 `Patch Release`
- ๐ Fixed an issue where the `stop() step didnโt always correctly interrupt the scenario execution.
- โ
Improved overall code structure and performance with internal refactoring.
## ๐ฅ What's New in 1.4.0
- โ
`ifThen`, `ifElse`, `ifEnd`: New block-style conditional logic, familiar to developers. Example:
```
ifThen(() => score.current > 5),
move("x", 100, 500),
ifElse(),
move("x", -100, 500),
ifEnd()
```
- โ
Nested conditionals are supported and validated at compile time.
- โ
Improved compiler validation to detect malformed blocks or unclosed conditionals.
- โ
Relative values in `move()` using helper functions like `inc(value)` or `dec(value)`. Example:
```
move("x", inc(50), 500)
```
- โ
Arguments in `callback()` steps now supported.
- โ
Jump to label in `nextStep(label)`: You can resume or redirect flow from UI or interaction code.
## ๐ฅ What's New in 1.3.0
- โ
`resume` step: jump back to the point after the last `goto()` call
- โ
`set` step: set a value directly or from a function
- โ
`stop` step: stop and reset the animation
- โ
`ifJump(condition, labelTrue, labelFalse?)` step: jump based on condition
- ๐ก Label validation for `goto` and `ifJump` steps now included in scenario compilation
- โ๏ธ Cleaned and centralized all validation logic in `compileScenario()`
## ๐ฅ What's New in 1.2.0
- โ
`goto("label")` step: jump to a specific label in your scenario
- โ
`label("name")` step: define named labels
- โ
Nested reusable blocks with `use("blockName")`
- โ
Full `reset()` and `stop()` logic
- โ
`TimelineView` for visual debug
- โ
`useScreenLifecycle(onFocus, onBlur)` helper
---
## ๐ฆ Installation
```
npm install react-native-animation-scenario
```
---
## ๐ฆ Exports
```
useAnimationScenario(); // Main hook
defineScenario([]); // Safely define your scenario
move(), delay(), callback(), ... // Step helpers
useScreenLifecycle(); // Hook for screen focus lifecycle
```
---
## ๐ Usage
```js
import {
useAnimationScenario,
defineScenario,
move,
delay,
parallel,
callback,
hold,
} from "react-native-animation-scenario";
const scenario = defineScenario([
move("opacity", 1, 500),
delay(300),
parallel([
move("scale", 1.1, 500),
move("translateY", 50, 500),
]),
hold("waitForTap"),
callback("onFinish"),
]);
const MyComponent = () => {
const {
refs,
start,
reset,
nextStep,
TimelineView,
} = useAnimationScenario({
scenario,
initialValues: {
opacity: 0,
scale: 1,
translateY: 0,
},
callbacks: {
onFinish: () => console.log("Done!"),
},
});
useEffect(() => { start(); }, []);
return (
<Animated.View style={{
opacity: refs.opacity,
transform: [
{ scale: refs.scale },
{ translateY: refs.translateY }
]
}}>
<Text>Hello Animation</Text>
<TimelineView />
</Animated.View>
);
};
```
---
## ๐ง Step Types
| Type | Description | Version |
|------------|----------------------------------------------|---------|
| `move` | Animate a ref value to a target | 1.0 |
| `delay` | Pause for a given duration | 1.0 |
| `parallel` | Animate multiple `values` simultaneously | 1.0 |
| `callback` | Run external logic (sync or async) | 1.0 |
| `vibrate` | Trigger haptic feedback | 1.0 |
| `hold` | Pause animation until `nextStep()` is called | 1.0 |
| `label` | Mark jump targets | 1.2 |
| `goto` | Jump to a label | 1.2 |
| `use` | Insert a block | 1.2 |
| `set` | Set a value directly or from a function | 1.3 |
| `stop` | Stop and reset the animation | 1.3 |
| `resume` | Continue from the point after last `goto()` | 1.3 |
| `ifJump` | Conditionally jump to a label | 1.3 |
| `ifThen` | Block-style conditional logic | 1.4 |
---
## ๐ง Modes
- mode: "auto" โ steps play sequentially
- mode: "manual" โ steps only run via nextStep()
- loop: true โ repeat the scenario forever
---
## ๐งฉ Reusable Blocks
```js
const blocks = {
bounce: defineScenario([
move("y", -30, 200),
move("y", 0, 200),
]),
};
const scenario = defineScenario([
callback("showStart"),
use("bounce"),
delay(500),
use("bounce"),
]);
```
---
## ๐ค Conditional Branching with `ifJump()`
Use `ifJump()` to dynamically choose the next step at runtime.
```js
const MyComponent = () => {
const directionRef = useRef(1);
const scenario = defineScenario([
hold(), // Wait for user input before branching
ifJump(() => directionRef.current === 1, "moveRight", "moveLeft"),
label("moveRight"),
move("x", 200, 500),
goto("resumePoint"),
label("moveLeft"),
move("x", 0, 500),
label("resumePoint"),
]);
const { refs, start, nextStep } = useAnimationScenario({
scenario,
initialValues: { x: 100 },
});
useEffect(() => { start(); }, []);
return (
<View>
<Animated.View
style={{
width: 50,
height: 50,
backgroundColor: "blue",
transform: [{ translateX: refs.x }],
marginBottom: 20,
}}
/>
<Text>Choose Direction:</Text>
<View style={{ flexDirection: "row", gap: 10 }}>
<Button title="Left" onPress={() => {
directionRef.current = 0;
nextStep();
}} />
<Button title="Right" onPress={() => {
directionRef.current = 1;
nextStep();
}} />
</View>
</View>
);
};
```
## ๐ `ifJump()` using a named callback
You can define reusable branching logic in `callbacks`:
```js
const scenario = defineScenario([
hold(),
ifJump("checkDirection", "goRight", "goLeft"),
label("goRight"),
move("x", 100, 300),
goto("end"),
label("goLeft"),
move("x", -100, 300),
label("end"),
]);
const { refs, nextStep, start } = useAnimationScenario({
scenario,
initialValues: { x: 0 },
callbacks: {
checkDirection: () => Math.random() > 0.5,
},
});
```
---
## ๐งช Advanced Flow Example: `set`, `goto`, `resume`, and `stop`
This scenario demonstrates how to use the new set, goto, resume, and stop steps for conditional or non-linear animation flows.
```js
import { useAnimationScenario,defineScenario, label, vibrate, move, goto, stop, set, resume } from "react-native-animation-scenario";
const scenario = defineScenario([
label("start"),
vibrate(),
move("x", 100, 2000),
goto("reverse"), // Jump to reverse path
move("x", 200, 500, "after-resume"), // Will resume here after `resume`
stop(), // Stops the flow here
label("reverse"),
set("x", 0), // Instantly reset position
move("x", -100, 500),
resume(),
]);
const MyComponent = () => {
const {refs,start,TimelineView,} = useAnimationScenario({
scenario,
initialValues: { x: 0, direction: 0 },
callbacks: {
checkDirection: () => {
// Example: dynamically decide to reverse
if (Math.random() > 0.5) return goto("reverse");
},
},
});
return (
<Animated.View style={{ transform: [{ translateX: refs.x }] }} >
<Button title="Start" onPress={start} />
<TimelineView />
</Animated.View>
);
};
```
---
## ๐งช Timeline Debug
Add the TimelineView to display step progress:
```js
<TimelineView /> <!-- Optional -->
```
Great for development or visual debugging of onboarding flows.
---
## ๐ก Safeguards
- ๐ซ Throws error if an initialValue or callback is missing
- ๐ All animations use Animated.Value and native drivers by default
---
## ๐งฉ Optional: Screen Lifecycle Hook
This helper hook requires `@react-navigation/native` and should be imported separately:
```js
import { useScreenLifecycle } from 'react-native-animation-scenario/src/useScreenLifecycle';
```
It allows you to trigger logic when the screen gains or loses focus.
---
## ๐ License
MIT โ Created by Cyril Adam
---
## ๐ฆ CHANGELOG for `v1.4.2` and `v1.4.1`
### ๐ง Fixes
- Fixed a bug where the `stop()` step didn't correctly interrupt scenario execution in all cases.
- Improved internal handling of the `shouldStop` flag to prevent race conditions and ensure proper reset behavior.
- `reset()` now explicitly clears `holdResolver` and `callingStepIndexRef`, making the scenario restart more predictable and clean.
- Minor log improvements under debug mode.
### ๐ก Internal Improvements
- Factored out reusable logic into `evalStepValue()` and `evalStepCondition()` to unify async/sync step evaluation across `set`, `ifThen`, and `callback` steps.
- Jump logic now centralized via `jumpTo()` utility, ensuring consistent block traversal across `ifThen`, `ifElse`, and `ifEnd`.
- Internal state handling made more robust for future scalability and debugging ease.
- Scenario validation (`compileScenario`) now checks `parallel` content blocks.
## ๐ CHANGELOG for `v1.4.0`
### โจ Added
- `ifThen(condition)`, `ifElse()`, `ifEnd()` block structure: familiar conditional logic using nested blocks.
- `nextStep(label)`: allows you to jump to a specific label manually, even after a `hold()` step.
- `move(...)` now supports relative values using `inc(x)` and `dec(x)`.
- Extended support for `callback()` to accept parameters, including dynamic values.
- Scenario validation (`compileScenario`) now checks for properly closed `ifThen/ifElse/ifEnd` blocks.
- Modular refactoring for better extensibility.
### ๐ Fixed
- Ensured `ifJump()` does not rely on refs directly from `defineScenario()`.
- Improved safety and label resolution for nested conditional logic.
- Fixed corner cases in `hold()` + jump logic for `nextStep()`.
### ๐งช Testing
- Jest tests added for malformed conditional structures (`ifThen` without `ifEnd`, duplicate `ifElse`, etc.)
- Unit tests extended to cover relative moves.
## ๐ CHANGELOG for `v1.3.1`
### ๐ Fixed
- Move useScreenLifecycle to a separate export to avoid dependency issues on Snack
## ๐ CHANGELOG for `v1.3.0`
### โจ Added
- `set(target, value)`: set a ref value directly, from a function, or from an async callback
- `stop()`: terminates the current animation sequence cleanly
- `resume()`: continues execution from the step after the last `goto()` jump
- `ifJump(condition, labelTrue, labelFalse?)`: conditional branching based on a sync or async function or a callback name
- Scenario validation now checks label existence for `goto` and `ifJump`
- Improved label jump tracking with `callingStepIndexRef` for `resume()
### ๐ Fixed
- Ensured `nextStep()` respects the `shouldStop` flag
- Cleaned up validation logic (moved to `compileScenario`)
- Deduplicated validation errors for clearer debug output
### ๐งช Testing
- New unit test coverage for scenario compilation including nested `use() blocks and label resolution.
- Some validation moved from runtime to compile-time, simplifying runtime hook logic.
## ๐ CHANGELOG for `v1.2.0`
### โจ Added
- `goto("label")` step to jump within the scenario
- `label("name")` step to declare jump targets
- Support for `use("blockName")` reusable block structure
- `__sourceBlock` annotation for debugging block origins
- Visual timeline label display (`TimelineView`)
- Full `reset()` support restores animated values
- `stop()` now halts looping scenarios safely
- `useScreenLifecycle(onFocus, onBlur)` hook
### ๐ Fixed
- Scenario would continue running after screen unmount (fixed with `shouldStop`)
- `reset()` didn't clear active animation state properly
- Label detection and step validation improved
### ๐งช Testing
- Added Jest tests and modular `compileScenario` system
- Internal compilation now flattens nested `use()` blocks
---
## ๐ค Contributing
- PRs welcome! Especially for:
- TypeScript types
- New step types (e.g. spring)
- Helper tooling