react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
259 lines (243 loc) • 8.14 kB
text/typescript
import { Platform } from 'react-native';
import type {
ProgressAnimation,
SharedTransitionAnimationsValues,
} from '../../commonTypes';
import { registerEventHandler, unregisterEventHandler } from '../../core';
import { ReanimatedError } from '../../errors';
import { isJest, shouldBeUseWeb } from '../../PlatformChecker';
import { runOnUIImmediately } from '../../threads';
type TransitionProgressEvent = {
closing: number;
goingForward: number;
eventName: string;
progress: number;
target: number;
};
const IS_ANDROID = Platform.OS === 'android';
export class ProgressTransitionManager {
private _sharedElementCount = 0;
private _eventHandler = {
isRegistered: false,
onTransitionProgress: -1,
onAppear: -1,
onDisappear: -1,
onSwipeDismiss: -1,
};
public addProgressAnimation(
viewTag: number,
progressAnimation: ProgressAnimation
) {
runOnUIImmediately(() => {
'worklet';
global.ProgressTransitionRegister.addProgressAnimation(
viewTag,
progressAnimation
);
})();
this.registerEventHandlers();
}
public removeProgressAnimation(viewTag: number, isUnmounting = true) {
this.unregisterEventHandlers();
runOnUIImmediately(() => {
'worklet';
global.ProgressTransitionRegister.removeProgressAnimation(
viewTag,
isUnmounting
);
})();
}
private registerEventHandlers() {
this._sharedElementCount++;
const eventHandler = this._eventHandler;
if (!eventHandler.isRegistered) {
eventHandler.isRegistered = true;
const eventPrefix = IS_ANDROID ? 'on' : 'top';
let lastProgressValue = -1;
eventHandler.onTransitionProgress = registerEventHandler(
(event: TransitionProgressEvent) => {
'worklet';
const progress = event.progress;
if (progress === lastProgressValue) {
// During screen transition, handler receives two events with the same progress
// value for both screens, but for modals, there is only one event. To optimize
// performance and avoid unnecessary worklet calls, let's skip the second event.
return;
}
lastProgressValue = progress;
global.ProgressTransitionRegister.frame(progress);
},
eventPrefix + 'TransitionProgress'
);
eventHandler.onAppear = registerEventHandler(() => {
'worklet';
global.ProgressTransitionRegister.onTransitionEnd();
}, eventPrefix + 'Appear');
if (IS_ANDROID) {
// onFinishTransitioning event is available only on Android and
// is used to handle closing modals
eventHandler.onDisappear = registerEventHandler(() => {
'worklet';
global.ProgressTransitionRegister.onAndroidFinishTransitioning();
}, 'onFinishTransitioning');
} else if (Platform.OS === 'ios') {
// topDisappear event is required to handle closing modals on iOS
eventHandler.onDisappear = registerEventHandler(() => {
'worklet';
global.ProgressTransitionRegister.onTransitionEnd(true);
}, 'topDisappear');
eventHandler.onSwipeDismiss = registerEventHandler(() => {
'worklet';
global.ProgressTransitionRegister.onTransitionEnd();
}, 'topGestureCancel');
}
}
}
private unregisterEventHandlers(): void {
this._sharedElementCount--;
if (this._sharedElementCount === 0) {
const eventHandler = this._eventHandler;
eventHandler.isRegistered = false;
if (eventHandler.onTransitionProgress !== -1) {
unregisterEventHandler(eventHandler.onTransitionProgress);
eventHandler.onTransitionProgress = -1;
}
if (eventHandler.onAppear !== -1) {
unregisterEventHandler(eventHandler.onAppear);
eventHandler.onAppear = -1;
}
if (eventHandler.onDisappear !== -1) {
unregisterEventHandler(eventHandler.onDisappear);
eventHandler.onDisappear = -1;
}
if (eventHandler.onSwipeDismiss !== -1) {
unregisterEventHandler(eventHandler.onSwipeDismiss);
eventHandler.onSwipeDismiss = -1;
}
}
}
}
function createProgressTransitionRegister() {
'worklet';
const progressAnimations = new Map<number, ProgressAnimation>();
const snapshots = new Map<
number,
Partial<SharedTransitionAnimationsValues>
>();
const currentTransitions = new Set<number>();
const toRemove = new Set<number>();
let skipCleaning = false;
let isTransitionRestart = false;
const progressTransitionManager = {
addProgressAnimation: (
viewTag: number,
progressAnimation: ProgressAnimation
) => {
if (currentTransitions.size > 0 && !progressAnimations.has(viewTag)) {
// there is no need to prevent cleaning on android
isTransitionRestart = !IS_ANDROID;
}
progressAnimations.set(viewTag, progressAnimation);
},
removeProgressAnimation: (viewTag: number, isUnmounting: boolean) => {
if (currentTransitions.size > 0) {
// there is no need to prevent cleaning on android
isTransitionRestart = !IS_ANDROID;
}
if (isUnmounting) {
// Remove the animation config after the transition is finished
toRemove.add(viewTag);
} else {
// if the animation is removed, without ever being started, it can be removed immediately
progressAnimations.delete(viewTag);
}
},
onTransitionStart: (
viewTag: number,
snapshot: Partial<SharedTransitionAnimationsValues>
) => {
skipCleaning = isTransitionRestart;
snapshots.set(viewTag, snapshot);
currentTransitions.add(viewTag);
// set initial style for re-parented components
progressTransitionManager.frame(0);
},
frame: (progress: number) => {
for (const viewTag of currentTransitions) {
const progressAnimation = progressAnimations.get(viewTag);
if (!progressAnimation) {
continue;
}
const snapshot = snapshots.get(
viewTag
)! as SharedTransitionAnimationsValues;
progressAnimation(viewTag, snapshot, progress);
}
},
onAndroidFinishTransitioning: () => {
if (toRemove.size > 0) {
// it should be ran only on modal closing
progressTransitionManager.onTransitionEnd();
}
},
onTransitionEnd: (removeViews = false) => {
if (currentTransitions.size === 0) {
toRemove.clear();
return;
}
if (skipCleaning) {
skipCleaning = false;
isTransitionRestart = false;
return;
}
for (const viewTag of currentTransitions) {
global._notifyAboutEnd(viewTag, removeViews);
}
currentTransitions.clear();
if (isTransitionRestart) {
// on transition restart, progressAnimations should be saved
// because they potentially can be used in the next transition
return;
}
snapshots.clear();
if (toRemove.size > 0) {
for (const viewTag of toRemove) {
progressAnimations.delete(viewTag);
global._notifyAboutEnd(viewTag, removeViews);
}
toRemove.clear();
}
},
};
return progressTransitionManager;
}
if (shouldBeUseWeb()) {
const maybeThrowError = () => {
// Jest attempts to access a property of this object to check if it is a Jest mock
// so we can't throw an error in the getter.
if (!isJest()) {
throw new ReanimatedError(
'`ProgressTransitionRegister` is not available on non-native platform.'
);
}
};
global.ProgressTransitionRegister = new Proxy(
{} as ProgressTransitionRegister,
{
get: maybeThrowError,
set: () => {
maybeThrowError();
return false;
},
}
);
} else {
runOnUIImmediately(() => {
'worklet';
global.ProgressTransitionRegister = createProgressTransitionRegister();
})();
}
export type ProgressTransitionRegister = ReturnType<
typeof createProgressTransitionRegister
>;
;