Skip to content

Commit

Permalink
constructed custom stagger / sequence composite animations due to iss…
Browse files Browse the repository at this point in the history
…ue with default react-native implementation
  • Loading branch information
lbuljan committed Dec 13, 2023
1 parent bb5c96c commit da2f1e4
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 71 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ letterSpacing?: TextStyle["letterSpacing"];
color?: string;
textAlign?: TextStyle["textAlign"];
opacity?: TextStyle["opacity"];
animations?: Partial<ViewStyle>;
```

#### Usage
Expand Down Expand Up @@ -135,6 +136,7 @@ This is a flexible and customizable React Native component that can be used as e
```javascript
/** Determine if Block is scrollable or not. If scrollable, extends ScrollView props. */
scrollable?: boolean;
animations?: Partial<ViewStyle>;
/** Whether the element is absolutely positioned. */
absolute?: boolean;
zIndex?: number;
Expand Down Expand Up @@ -355,6 +357,34 @@ export const Component: React.FC = () => {
}
```
### Reversing element animations.
Instead of just reseting the animation, which does not play the animation back in reverse, the utility also exposes a `reverse` function which will animate the element back to it's initial values.
Instead of `element.reset()`, use `element.reverse()`. This can also be used on timelines.
Usage
```javascript
const element = animateStagger(
{
opacity: [0, 1],
translateX: [20, 0],
translateY: [20, 0],
},
{ stagger: 1200, duration: 800 },
);

...

useEffect(() => {
if (startAnimation) {
element.start();
} else {
element.reverse();
}
}, [startAnimation]);
```
## Hooks
### useForm & useFormUtils
Expand Down
7 changes: 5 additions & 2 deletions src/models/Animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ export type Animation<Keys extends keyof ViewStyle = keyof ViewStyle> = {

export type ElementAnimation<Keys extends keyof ViewStyle> = {
animations: Animation<Keys>;
composition: Animated.CompositeAnimation;
forward: Animated.CompositeAnimation;
backward: Animated.CompositeAnimation;
/** Start animation with onFinished callback. Using forward.start() */
start(onFinished?: () => void): void;
/** Reverse all animation values to initial value and reset main trigger. */
/** Reverse all animation values to initial value and reset main trigger. Using backward.start() */
reverse: () => void;
/** Reset animation to initial value. Using forward.reset() */
reset: Animated.CompositeAnimation["reset"];
};

Expand Down
179 changes: 110 additions & 69 deletions src/utils/animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function processStyles<Keys extends keyof ViewStyle>(
const keys = Object.keys(styles) as Keys[];
const values = keys.map(() => new Animated.Value(0));
const compositions: Animated.CompositeAnimation[] = [];
const reverseCompositions: Animated.CompositeAnimation[] = [];
const animations: Partial<Animation<Keys>> = {};

keys.forEach((key, index) => {
Expand All @@ -37,13 +38,22 @@ function processStyles<Keys extends keyof ViewStyle>(
composition = Animated.loop(composition);
}

const reverseComposition = Animated.timing(value, {
toValue: 0,
duration: config.duration,
useNativeDriver: !config.loop,
easing: config.easing || Easing.inOut(Easing.quad),
});

animations[key] = animation;
compositions.push(composition);
reverseCompositions.push(reverseComposition);
});

return {
values,
compositions,
reverseCompositions: reverseCompositions.reverse(),
animations: animations as Animation<Keys>,
};
}
Expand All @@ -54,8 +64,12 @@ export function animateParallel<Styles extends keyof ViewStyle>(
styles: AnimationStyle<Styles>,
config: AnimationConfiguration = { duration: 800 },
): ElementAnimation<Styles> {
const { animations, values, compositions } = processStyles(styles, config);
const { animations, reverseCompositions, compositions } = processStyles(
styles,
config,
);
const trigger = Animated.parallel(compositions);
const reverseTrigger = Animated.parallel(reverseCompositions);

function start(onFinished?: () => void) {
trigger.start(({ finished }) => {
Expand All @@ -65,40 +79,52 @@ export function animateParallel<Styles extends keyof ViewStyle>(
});
}

function reverse() {
const reversedCompositions = values.map((_, index) => {
const value = values[index];
let composition = Animated.timing(value, {
toValue: 0,
duration: config.duration,
useNativeDriver: !config.loop,
easing: config.easing || Easing.inOut(Easing.quad),
});

return composition;
});

const reversedTrigger = Animated.parallel(reversedCompositions);
reversedTrigger.start();
}

return {
start,
reverse,
reverse: reverseTrigger.start,
reset: trigger.reset,
composition: trigger,
forward: trigger,
backward: reverseTrigger,
animations,
};
}

function createStaggerComposition(
compositions: Animated.CompositeAnimation[],
stagger: number,
): Animated.CompositeAnimation {
return {
start: (callback?: Animated.EndCallback) => {
Animated.parallel(
compositions.map((c, i) => {
return createSequenceComposition([Animated.delay(stagger * i), c]);
}),
).start(callback);
},
stop: () => {
for (const composition of compositions) composition.stop();
},
reset: () => {
for (const composition of compositions) composition.reset();
},
};
}

/** Stagger defined styles animations.
* Example: if you define opacity and top styles, this will start the opacity animation and stagger the top animation by stagger amount. */
export function animateStagger<Styles extends keyof ViewStyle>(
styles: AnimationStyle<Styles>,
config: StaggerAnimationConfiguration = { duration: 800, stagger: 400 },
): ElementAnimation<Styles> {
const { animations, values, compositions } = processStyles(styles, config);
const trigger = Animated.stagger(config.stagger, compositions);
const { animations, reverseCompositions, compositions } = processStyles(
styles,
config,
);
const trigger = createStaggerComposition(compositions, config.stagger);
const reverseTrigger = createStaggerComposition(
reverseCompositions,
config.stagger,
);

function start(onFinished?: () => void) {
trigger.start(({ finished }) => {
Expand All @@ -108,44 +134,59 @@ export function animateStagger<Styles extends keyof ViewStyle>(
});
}

function reverse() {
const reversedCompositions = values.map((_, index) => {
const value = values[index];
let composition = Animated.timing(value, {
toValue: 0,
duration: config.duration,
useNativeDriver: !config.loop,
easing: config.easing || Easing.inOut(Easing.quad),
});

return composition;
});

const reversedTrigger = Animated.stagger(
config.stagger,
reversedCompositions,
);

reversedTrigger.start();
}

return {
start,
reverse,
reverse: reverseTrigger.start,
reset: trigger.reset,
composition: trigger,
forward: trigger,
backward: reverseTrigger,
animations,
};
}

function createSequenceComposition(
compositions: Animated.CompositeAnimation[],
): Animated.CompositeAnimation {
return {
start: (callback?: Animated.EndCallback) => {
function startComposition(index: number) {
const composition = compositions[index];
composition.start(({ finished }) => {
if (finished) {
const nextIndex = index + 1;
if (nextIndex < compositions.length) {
startComposition(nextIndex);
} else {
callback?.({ finished: true });
}
}
});
}

startComposition(0);
},
stop: () => {
for (const composition of compositions) composition.stop();
},
reset: () => {
for (const composition of compositions) composition.reset();
},
};
}

/** This will animate the passed in styles in sequence.
* Example: if you define opacity and top styles, this will start the opacity animation and then start the top animation when the opacity animation finishes. */
export function animateSequence<Styles extends keyof ViewStyle>(
styles: AnimationStyle<Styles>,
config: AnimationConfiguration = { duration: 800 },
): ElementAnimation<Styles> {
const { animations, values, compositions } = processStyles(styles, config);
const trigger = Animated.sequence(compositions);
const { animations, reverseCompositions, compositions } = processStyles(
styles,
config,
);
const trigger = createSequenceComposition(compositions);
const reverseTrigger = createSequenceComposition(reverseCompositions);

function start(onFinished?: () => void) {
trigger.start(({ finished }) => {
if (finished) {
Expand All @@ -154,28 +195,12 @@ export function animateSequence<Styles extends keyof ViewStyle>(
});
}

function reverse() {
const reversedCompositions = values.map((_, index) => {
const value = values[index];
let composition = Animated.timing(value, {
toValue: 0,
duration: config.duration,
useNativeDriver: !config.loop,
easing: config.easing || Easing.inOut(Easing.quad),
});

return composition;
});

const reversedTrigger = Animated.sequence(reversedCompositions);
reversedTrigger.start();
}

return {
start,
reverse,
reverse: reverseTrigger.start,
reset: trigger.reset,
composition: trigger,
forward: trigger,
backward: reverseTrigger,
animations,
};
}
Expand All @@ -192,11 +217,27 @@ export function createAnimationTimeline<K extends keyof ViewStyle>(
const compositions = times
.map(ms => {
const elements = timeline[ms];
const trigger = Animated.parallel(elements.map(e => e.composition));
const trigger = Animated.parallel(elements.map(e => e.forward));
if (!ms) return trigger;
return Animated.sequence([Animated.delay(ms), trigger]);
return createSequenceComposition([Animated.delay(ms), trigger]);
})
.flat();

return Animated.parallel(compositions);
let lastTime = times[times.length - 1];
const reverseCompositions = times.reverse().map(ms => {
const delay = lastTime - ms;
const elements = timeline[ms];
const trigger = Animated.parallel(elements.map(e => e.backward));
if (!delay) return trigger;
return createSequenceComposition([Animated.delay(delay), trigger]);
});

const forward = Animated.parallel(compositions);
const backward = Animated.parallel(reverseCompositions);

return {
start: forward.start,
reverse: backward.start,
reset: forward.reset,
};
}

0 comments on commit da2f1e4

Please sign in to comment.