UNPKG

8.11 kBPlain TextView Raw
1'use strict';
2import { runOnUIImmediately } from '../../threads';
3import type {
4 ProgressAnimation,
5 SharedTransitionAnimationsValues,
6} from '../animationBuilder/commonTypes';
7import { registerEventHandler, unregisterEventHandler } from '../../core';
8import { Platform } from 'react-native';
9import { isJest, shouldBeUseWeb } from '../../PlatformChecker';
10
11type TransitionProgressEvent = {
12 closing: number;
13 goingForward: number;
14 eventName: string;
15 progress: number;
16 target: number;
17};
18
19const IS_ANDROID = Platform.OS === 'android';
20
21export class ProgressTransitionManager {
22 private _sharedElementCount = 0;
23 private _eventHandler = {
24 isRegistered: false,
25 onTransitionProgress: -1,
26 onAppear: -1,
27 onDisappear: -1,
28 onSwipeDismiss: -1,
29 };
30
31 public addProgressAnimation(
32 viewTag: number,
33 progressAnimation: ProgressAnimation
34 ) {
35 runOnUIImmediately(() => {
36 'worklet';
37 global.ProgressTransitionRegister.addProgressAnimation(
38 viewTag,
39 progressAnimation
40 );
41 })();
42
43 this.registerEventHandlers();
44 }
45
46 public removeProgressAnimation(viewTag: number, isUnmounting = true) {
47 this.unregisterEventHandlers();
48 runOnUIImmediately(() => {
49 'worklet';
50 global.ProgressTransitionRegister.removeProgressAnimation(
51 viewTag,
52 isUnmounting
53 );
54 })();
55 }
56
57 private registerEventHandlers() {
58 this._sharedElementCount++;
59 const eventHandler = this._eventHandler;
60 if (!eventHandler.isRegistered) {
61 eventHandler.isRegistered = true;
62 const eventPrefix = IS_ANDROID ? 'on' : 'top';
63 let lastProgressValue = -1;
64 eventHandler.onTransitionProgress = registerEventHandler(
65 (event: TransitionProgressEvent) => {
66 'worklet';
67 const progress = event.progress;
68 if (progress === lastProgressValue) {
69 // During screen transition, handler receives two events with the same progress
70 // value for both screens, but for modals, there is only one event. To optimize
71 // performance and avoid unnecessary worklet calls, let's skip the second event.
72 return;
73 }
74 lastProgressValue = progress;
75 global.ProgressTransitionRegister.frame(progress);
76 },
77 eventPrefix + 'TransitionProgress'
78 );
79 eventHandler.onAppear = registerEventHandler(() => {
80 'worklet';
81 global.ProgressTransitionRegister.onTransitionEnd();
82 }, eventPrefix + 'Appear');
83
84 if (IS_ANDROID) {
85 // onFinishTransitioning event is available only on Android and
86 // is used to handle closing modals
87 eventHandler.onDisappear = registerEventHandler(() => {
88 'worklet';
89 global.ProgressTransitionRegister.onAndroidFinishTransitioning();
90 }, 'onFinishTransitioning');
91 } else if (Platform.OS === 'ios') {
92 // topDisappear event is required to handle closing modals on iOS
93 eventHandler.onDisappear = registerEventHandler(() => {
94 'worklet';
95 global.ProgressTransitionRegister.onTransitionEnd(true);
96 }, 'topDisappear');
97 eventHandler.onSwipeDismiss = registerEventHandler(() => {
98 'worklet';
99 global.ProgressTransitionRegister.onTransitionEnd();
100 }, 'topGestureCancel');
101 }
102 }
103 }
104
105 private unregisterEventHandlers(): void {
106 this._sharedElementCount--;
107 if (this._sharedElementCount === 0) {
108 const eventHandler = this._eventHandler;
109 eventHandler.isRegistered = false;
110 if (eventHandler.onTransitionProgress !== -1) {
111 unregisterEventHandler(eventHandler.onTransitionProgress);
112 eventHandler.onTransitionProgress = -1;
113 }
114 if (eventHandler.onAppear !== -1) {
115 unregisterEventHandler(eventHandler.onAppear);
116 eventHandler.onAppear = -1;
117 }
118 if (eventHandler.onDisappear !== -1) {
119 unregisterEventHandler(eventHandler.onDisappear);
120 eventHandler.onDisappear = -1;
121 }
122 if (eventHandler.onSwipeDismiss !== -1) {
123 unregisterEventHandler(eventHandler.onSwipeDismiss);
124 eventHandler.onSwipeDismiss = -1;
125 }
126 }
127 }
128}
129
130function createProgressTransitionRegister() {
131 'worklet';
132 const progressAnimations = new Map<number, ProgressAnimation>();
133 const snapshots = new Map<
134 number,
135 Partial<SharedTransitionAnimationsValues>
136 >();
137 const currentTransitions = new Set<number>();
138 const toRemove = new Set<number>();
139
140 let skipCleaning = false;
141 let isTransitionRestart = false;
142
143 const progressTransitionManager = {
144 addProgressAnimation: (
145 viewTag: number,
146 progressAnimation: ProgressAnimation
147 ) => {
148 if (currentTransitions.size > 0 && !progressAnimations.has(viewTag)) {
149 // there is no need to prevent cleaning on android
150 isTransitionRestart = !IS_ANDROID;
151 }
152 progressAnimations.set(viewTag, progressAnimation);
153 },
154 removeProgressAnimation: (viewTag: number, isUnmounting: boolean) => {
155 if (currentTransitions.size > 0) {
156 // there is no need to prevent cleaning on android
157 isTransitionRestart = !IS_ANDROID;
158 }
159 if (isUnmounting) {
160 // Remove the animation config after the transition is finished
161 toRemove.add(viewTag);
162 } else {
163 // if the animation is removed, without ever being started, it can be removed immediately
164 progressAnimations.delete(viewTag);
165 }
166 },
167 onTransitionStart: (
168 viewTag: number,
169 snapshot: Partial<SharedTransitionAnimationsValues>
170 ) => {
171 skipCleaning = isTransitionRestart;
172 snapshots.set(viewTag, snapshot);
173 currentTransitions.add(viewTag);
174 // set initial style for re-parented components
175 progressTransitionManager.frame(0);
176 },
177 frame: (progress: number) => {
178 for (const viewTag of currentTransitions) {
179 const progressAnimation = progressAnimations.get(viewTag);
180 if (!progressAnimation) {
181 continue;
182 }
183 const snapshot = snapshots.get(
184 viewTag
185 )! as SharedTransitionAnimationsValues;
186 progressAnimation(viewTag, snapshot, progress);
187 }
188 },
189 onAndroidFinishTransitioning: () => {
190 if (toRemove.size > 0) {
191 // it should be ran only on modal closing
192 progressTransitionManager.onTransitionEnd();
193 }
194 },
195 onTransitionEnd: (removeViews = false) => {
196 if (currentTransitions.size === 0) {
197 toRemove.clear();
198 return;
199 }
200 if (skipCleaning) {
201 skipCleaning = false;
202 isTransitionRestart = false;
203 return;
204 }
205 for (const viewTag of currentTransitions) {
206 global._notifyAboutEnd(viewTag, removeViews);
207 }
208 currentTransitions.clear();
209 if (isTransitionRestart) {
210 // on transition restart, progressAnimations should be saved
211 // because they potentially can be used in the next transition
212 return;
213 }
214 snapshots.clear();
215 if (toRemove.size > 0) {
216 for (const viewTag of toRemove) {
217 progressAnimations.delete(viewTag);
218 global._notifyAboutEnd(viewTag, removeViews);
219 }
220 toRemove.clear();
221 }
222 },
223 };
224 return progressTransitionManager;
225}
226
227if (shouldBeUseWeb()) {
228 const maybeThrowError = () => {
229 // Jest attempts to access a property of this object to check if it is a Jest mock
230 // so we can't throw an error in the getter.
231 if (!isJest()) {
232 throw new Error(
233 '[Reanimated] `ProgressTransitionRegister` is not available on non-native platform.'
234 );
235 }
236 };
237 global.ProgressTransitionRegister = new Proxy(
238 {} as ProgressTransitionRegister,
239 {
240 get: maybeThrowError,
241 set: () => {
242 maybeThrowError();
243 return false;
244 },
245 }
246 );
247} else {
248 runOnUIImmediately(() => {
249 'worklet';
250 global.ProgressTransitionRegister = createProgressTransitionRegister();
251 })();
252}
253
254export type ProgressTransitionRegister = ReturnType<
255 typeof createProgressTransitionRegister
256>;