react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
194 lines (182 loc) • 6.19 kB
text/typescript
import { runOnUIImmediately } from '../../threads';
import type { ProgressAnimation } from '../animationBuilder/commonTypes';
import { registerEventHandler, unregisterEventHandler } from '../../core';
import { Platform } from 'react-native';
type TransitionProgressEvent = {
closing: number;
goingForward: number;
eventName: string;
progress: number;
target: number;
};
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) {
this.unregisterEventHandlers();
runOnUIImmediately(() => {
'worklet';
global.ProgressTransitionRegister.removeProgressAnimation(viewTag);
})();
}
private registerEventHandlers() {
this._sharedElementCount++;
const eventHandler = this._eventHandler;
if (!eventHandler.isRegistered) {
eventHandler.isRegistered = true;
const eventPrefix = Platform.OS === 'android' ? 'on' : 'top';
let lastProgressValue = -1;
eventHandler.onTransitionProgress = registerEventHandler(
eventPrefix + 'TransitionProgress',
(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);
}
);
eventHandler.onAppear = registerEventHandler(
eventPrefix + 'Appear',
() => {
'worklet';
global.ProgressTransitionRegister.onTransitionEnd();
}
);
if (Platform.OS === 'android') {
// onFinishTransitioning event is available only on Android and
// is used to handle closing modals
eventHandler.onDisappear = registerEventHandler(
'onFinishTransitioning',
() => {
'worklet';
global.ProgressTransitionRegister.onAndroidFinishTransitioning();
}
);
} else if (Platform.OS === 'ios') {
// topDisappear event is required to handle closing modals on iOS
eventHandler.onDisappear = registerEventHandler('topDisappear', () => {
'worklet';
global.ProgressTransitionRegister.onTransitionEnd(true);
});
eventHandler.onSwipeDismiss = registerEventHandler(
'topGestureCancel',
() => {
'worklet';
global.ProgressTransitionRegister.onTransitionEnd();
}
);
}
}
}
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, any>();
const currentTransitions = new Set<number>();
const toRemove = new Set<number>();
const progressTransitionManager = {
addProgressAnimation: (
viewTag: number,
progressAnimation: ProgressAnimation
) => {
progressAnimations.set(viewTag, progressAnimation);
},
removeProgressAnimation: (viewTag: number) => {
if (progressAnimations.size > 1) {
// Remove the animation config after the transition is finished
toRemove.add(viewTag);
} else {
progressAnimations.delete(viewTag);
}
},
onTransitionStart: (viewTag: number, snapshot: any) => {
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);
const snapshot = snapshots.get(viewTag);
progressAnimation!(viewTag, snapshot, progress);
}
},
onAndroidFinishTransitioning: () => {
if (toRemove.size > 0) {
// it should be ran only on modal closing
progressTransitionManager.onTransitionEnd();
}
},
onTransitionEnd: (removeViews = false) => {
for (const viewTag of currentTransitions) {
_notifyAboutEnd(viewTag, removeViews);
}
currentTransitions.clear();
snapshots.clear();
if (toRemove.size > 0) {
for (const viewTag of toRemove) {
progressAnimations.delete(viewTag);
}
toRemove.clear();
}
},
};
return progressTransitionManager;
}
runOnUIImmediately(() => {
'worklet';
global.ProgressTransitionRegister = createProgressTransitionRegister();
})();
export type ProgressTransitionRegister = ReturnType<
typeof createProgressTransitionRegister
>;