react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
239 lines (229 loc) • 7.57 kB
text/typescript
'use strict';
import type {
LockAxis,
PanGestureHandlerEventPayload,
ScreenTransitionConfig,
} from './commonTypes';
import { RNScreensTurboModule } from './RNScreensTurboModule';
import { applyStyle, applyStyleForBelowTopScreen } from './styleUpdater';
const BASE_VELOCITY = 400;
const ADDITIONAL_VELOCITY_FACTOR_X = 400;
const ADDITIONAL_VELOCITY_FACTOR_Y = 500;
const ADDITIONAL_VELOCITY_FACTOR_XY = 600;
function computeEasingProgress(
startingTimestamp: number,
distance: number,
velocity: number
) {
'worklet';
if (Math.abs(distance) < 1) {
return 1;
}
const elapsedTime = (_getAnimationTimestamp() - startingTimestamp) / 1000;
const currentPosition = velocity * elapsedTime;
const progress = currentPosition / distance;
return progress;
}
function easing(x: number): number {
'worklet';
// based on https://easings.net/#easeOutQuart
return 1 - Math.pow(1 - x, 5);
}
function computeProgress(
screenTransitionConfig: ScreenTransitionConfig,
event: PanGestureHandlerEventPayload,
isTransitionCanceled: boolean
) {
'worklet';
const screenDimensions = screenTransitionConfig.screenDimensions;
const progressX = Math.abs(event.translationX / screenDimensions.width);
const progressY = Math.abs(event.translationY / screenDimensions.height);
const maxProgress = Math.max(progressX, progressY);
const progress = isTransitionCanceled ? maxProgress / 2 : maxProgress;
return progress;
}
function maybeScheduleNextFrame(
step: () => void,
didScreenReachDestination: boolean,
screenTransitionConfig: ScreenTransitionConfig,
event: PanGestureHandlerEventPayload,
isTransitionCanceled: boolean
) {
'worklet';
if (!didScreenReachDestination) {
const stackTag = screenTransitionConfig.stackTag;
const progress = computeProgress(
screenTransitionConfig,
event,
isTransitionCanceled
);
RNScreensTurboModule.updateTransition(stackTag, progress);
requestAnimationFrame(step);
} else {
screenTransitionConfig.onFinishAnimation?.();
}
}
export function getSwipeSimulator(
event: PanGestureHandlerEventPayload,
screenTransitionConfig: ScreenTransitionConfig,
lockAxis?: LockAxis
) {
'worklet';
const screenDimensions = screenTransitionConfig.screenDimensions;
const startTimestamp = _getAnimationTimestamp();
const { isTransitionCanceled } = screenTransitionConfig;
const startingPosition = {
x: event.translationX,
y: event.translationY,
};
const direction = {
x: Math.sign(event.translationX),
y: Math.sign(event.translationY),
};
const finalPosition = isTransitionCanceled
? { x: 0, y: 0 }
: {
x: direction.x * screenDimensions.width,
y: direction.y * screenDimensions.height,
};
const distance = {
x: Math.abs(finalPosition.x - startingPosition.x),
y: Math.abs(finalPosition.y - startingPosition.y),
};
const didScreenReachDestination = {
x: false,
y: false,
};
const velocity = { x: BASE_VELOCITY, y: BASE_VELOCITY };
if (lockAxis === 'x') {
velocity.y = 0;
velocity.x +=
(ADDITIONAL_VELOCITY_FACTOR_X * distance.x) / screenDimensions.width;
} else if (lockAxis === 'y') {
velocity.x = 0;
velocity.y +=
(ADDITIONAL_VELOCITY_FACTOR_Y * distance.y) / screenDimensions.height;
} else {
const euclideanDistance = Math.sqrt(distance.x ** 2 + distance.y ** 2);
const screenDiagonal = Math.sqrt(
screenDimensions.width ** 2 + screenDimensions.height ** 2
);
const velocityVectorLength =
BASE_VELOCITY +
(ADDITIONAL_VELOCITY_FACTOR_XY * euclideanDistance) / screenDiagonal;
if (Math.abs(startingPosition.x) > Math.abs(startingPosition.y)) {
velocity.x = velocityVectorLength;
velocity.y =
velocityVectorLength *
Math.abs(startingPosition.y / startingPosition.x);
} else {
velocity.x =
velocityVectorLength *
Math.abs(startingPosition.x / startingPosition.y);
velocity.y = velocityVectorLength;
}
}
if (isTransitionCanceled) {
function didScreenReachDestinationCheck() {
if (lockAxis === 'x') {
return didScreenReachDestination.x;
} else if (lockAxis === 'y') {
return didScreenReachDestination.y;
} else {
return didScreenReachDestination.x && didScreenReachDestination.y;
}
}
function restoreOriginalStyleForBelowTopScreen() {
event.translationX = direction.x * screenDimensions.width;
event.translationY = direction.y * screenDimensions.height;
applyStyleForBelowTopScreen(screenTransitionConfig, event);
}
const computeFrame = () => {
const progress = {
x: computeEasingProgress(startTimestamp, distance.x, velocity.x),
y: computeEasingProgress(startTimestamp, distance.y, velocity.y),
};
event.translationX =
startingPosition.x - direction.x * distance.x * easing(progress.x);
event.translationY =
startingPosition.y - direction.y * distance.y * easing(progress.y);
if (direction.x > 0) {
if (event.translationX <= 0) {
didScreenReachDestination.x = true;
event.translationX = 0;
}
} else {
if (event.translationX >= 0) {
didScreenReachDestination.x = true;
event.translationX = 0;
}
}
if (direction.y > 0) {
if (event.translationY <= 0) {
didScreenReachDestination.y = true;
event.translationY = 0;
}
} else {
if (event.translationY >= 0) {
didScreenReachDestination.y = true;
event.translationY = 0;
}
}
applyStyle(screenTransitionConfig, event);
const finished = didScreenReachDestinationCheck();
if (finished) {
restoreOriginalStyleForBelowTopScreen();
}
maybeScheduleNextFrame(
computeFrame,
finished,
screenTransitionConfig,
event,
isTransitionCanceled
);
};
return computeFrame;
} else {
const computeFrame = () => {
const progress = {
x: computeEasingProgress(startTimestamp, distance.x, velocity.x),
y: computeEasingProgress(startTimestamp, distance.y, velocity.y),
};
event.translationX =
startingPosition.x + direction.x * distance.x * easing(progress.x);
event.translationY =
startingPosition.y + direction.y * distance.y * easing(progress.y);
if (direction.x > 0) {
if (event.translationX >= screenDimensions.width) {
didScreenReachDestination.x = true;
event.translationX = screenDimensions.width;
}
} else {
if (event.translationX <= -screenDimensions.width) {
didScreenReachDestination.x = true;
event.translationX = -screenDimensions.width;
}
}
if (direction.y > 0) {
if (event.translationY >= screenDimensions.height) {
didScreenReachDestination.y = true;
event.translationY = screenDimensions.height;
}
} else {
if (event.translationY <= -screenDimensions.height) {
didScreenReachDestination.y = true;
event.translationY = -screenDimensions.height;
}
}
applyStyle(screenTransitionConfig, event);
maybeScheduleNextFrame(
computeFrame,
didScreenReachDestination.x || didScreenReachDestination.y,
screenTransitionConfig,
event,
isTransitionCanceled
);
};
return computeFrame;
}
}