1 | import React from 'react';
|
2 | import { findNodeHandle, Platform, StyleSheet } from 'react-native';
|
3 | import ReanimatedEventEmitter from './ReanimatedEventEmitter';
|
4 |
|
5 | import AnimatedEvent from './core/AnimatedEvent';
|
6 | import AnimatedNode from './core/AnimatedNode';
|
7 | import AnimatedValue from './core/AnimatedValue';
|
8 | import { createOrReusePropsNode } from './core/AnimatedProps';
|
9 | import WorkletEventHandler from './reanimated2/WorkletEventHandler';
|
10 | import setAndForwardRef from './setAndForwardRef';
|
11 |
|
12 | import invariant from 'fbjs/lib/invariant';
|
13 | import { adaptViewConfig } from './ConfigHelper';
|
14 | import { RNRenderer } from './reanimated2/platform-specific/RNRenderer';
|
15 |
|
16 | const NODE_MAPPING = new Map();
|
17 |
|
18 | function listener(data) {
|
19 | const component = NODE_MAPPING.get(data.viewTag);
|
20 | component && component._updateFromNative(data.props);
|
21 | }
|
22 |
|
23 | function dummyListener() {
|
24 |
|
25 |
|
26 | }
|
27 |
|
28 | function hasAnimatedNodes(value) {
|
29 | if (value instanceof AnimatedNode) {
|
30 | return true;
|
31 | }
|
32 | if (Array.isArray(value)) {
|
33 | return value.some((item) => hasAnimatedNodes(item));
|
34 | }
|
35 | if (value && typeof value === 'object') {
|
36 | return Object.keys(value).some((key) => hasAnimatedNodes(value[key]));
|
37 | }
|
38 | return false;
|
39 | }
|
40 |
|
41 | function flattenArray(array) {
|
42 | if (!Array.isArray(array)) {
|
43 | return array;
|
44 | }
|
45 | const resultArr = [];
|
46 |
|
47 | const _flattenArray = (arr) => {
|
48 | arr.forEach((item) => {
|
49 | if (Array.isArray(item)) {
|
50 | _flattenArray(item);
|
51 | } else {
|
52 | resultArr.push(item);
|
53 | }
|
54 | });
|
55 | };
|
56 | _flattenArray(array);
|
57 | return resultArr;
|
58 | }
|
59 |
|
60 | export default function createAnimatedComponent(Component) {
|
61 | invariant(
|
62 | typeof Component !== 'function' ||
|
63 | (Component.prototype && Component.prototype.isReactComponent),
|
64 | '`createAnimatedComponent` does not support stateless functional components; ' +
|
65 | 'use a class component instead.'
|
66 | );
|
67 |
|
68 | class AnimatedComponent extends React.Component {
|
69 | _invokeAnimatedPropsCallbackOnMount = false;
|
70 |
|
71 | constructor(props) {
|
72 | super(props);
|
73 | this._attachProps(this.props);
|
74 | if (process.env.JEST_WORKER_ID) {
|
75 | this.animatedStyle = { value: {} };
|
76 | }
|
77 | }
|
78 |
|
79 | componentWillUnmount() {
|
80 | this._detachPropUpdater();
|
81 | this._propsAnimated && this._propsAnimated.__detach();
|
82 | this._detachNativeEvents();
|
83 | }
|
84 |
|
85 | componentDidMount() {
|
86 | if (this._invokeAnimatedPropsCallbackOnMount) {
|
87 | this._invokeAnimatedPropsCallbackOnMount = false;
|
88 | this._animatedPropsCallback();
|
89 | }
|
90 |
|
91 | this._propsAnimated && this._propsAnimated.setNativeView(this._component);
|
92 | this._attachNativeEvents();
|
93 | this._attachPropUpdater();
|
94 | this._attachAnimatedStyles();
|
95 | }
|
96 |
|
97 | _getEventViewRef() {
|
98 |
|
99 |
|
100 | return this._component.getScrollableNode
|
101 | ? this._component.getScrollableNode()
|
102 | : this._component;
|
103 | }
|
104 |
|
105 | _attachNativeEvents() {
|
106 | const node = this._getEventViewRef();
|
107 | const viewTag = findNodeHandle(node);
|
108 |
|
109 | for (const key in this.props) {
|
110 | const prop = this.props[key];
|
111 | if (prop instanceof AnimatedEvent) {
|
112 | prop.attachEvent(node, key);
|
113 | } else if (
|
114 | prop?.current &&
|
115 | prop.current instanceof WorkletEventHandler
|
116 | ) {
|
117 | prop.current.registerForEvents(viewTag, key);
|
118 | }
|
119 | }
|
120 | }
|
121 |
|
122 | _detachNativeEvents() {
|
123 | const node = this._getEventViewRef();
|
124 |
|
125 | for (const key in this.props) {
|
126 | const prop = this.props[key];
|
127 | if (prop instanceof AnimatedEvent) {
|
128 | prop.detachEvent(node, key);
|
129 | } else if (
|
130 | prop?.current &&
|
131 | prop.current instanceof WorkletEventHandler
|
132 | ) {
|
133 | prop.current.unregisterFromEvents();
|
134 | }
|
135 | }
|
136 | }
|
137 |
|
138 | _reattachNativeEvents(prevProps) {
|
139 | const node = this._getEventViewRef();
|
140 | const attached = new Set();
|
141 | const nextEvts = new Set();
|
142 | let viewTag;
|
143 |
|
144 | for (const key in this.props) {
|
145 | const prop = this.props[key];
|
146 | if (prop instanceof AnimatedEvent) {
|
147 | nextEvts.add(prop.__nodeID);
|
148 | } else if (
|
149 | prop?.current &&
|
150 | prop.current instanceof WorkletEventHandler
|
151 | ) {
|
152 | if (viewTag === undefined) {
|
153 | viewTag = prop.current.viewTag;
|
154 | }
|
155 | }
|
156 | }
|
157 | for (const key in prevProps) {
|
158 | const prop = this.props[key];
|
159 | if (prop instanceof AnimatedEvent) {
|
160 | if (!nextEvts.has(prop.__nodeID)) {
|
161 |
|
162 | prop.detachEvent(node, key);
|
163 | } else {
|
164 |
|
165 | attached.add(prop.__nodeID);
|
166 | }
|
167 | } else if (
|
168 | prop?.current &&
|
169 | prop.current instanceof WorkletEventHandler &&
|
170 | prop.current.reattachNeeded
|
171 | ) {
|
172 | prop.current.unregisterFromEvents();
|
173 | }
|
174 | }
|
175 |
|
176 | for (const key in this.props) {
|
177 | const prop = this.props[key];
|
178 | if (prop instanceof AnimatedEvent && !attached.has(prop.__nodeID)) {
|
179 |
|
180 | prop.attachEvent(node, key);
|
181 | } else if (
|
182 | prop?.current &&
|
183 | prop.current instanceof WorkletEventHandler &&
|
184 | prop.current.reattachNeeded
|
185 | ) {
|
186 | prop.current.registerForEvents(viewTag, key);
|
187 | prop.current.reattachNeeded = false;
|
188 | }
|
189 | }
|
190 | }
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | _animatedPropsCallback = () => {
|
198 | if (this._component == null) {
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | this._invokeAnimatedPropsCallbackOnMount = true;
|
205 | } else if (typeof this._component.setNativeProps !== 'function') {
|
206 | this.forceUpdate();
|
207 | } else {
|
208 | this._component.setNativeProps(this._propsAnimated.__getValue());
|
209 | }
|
210 | };
|
211 |
|
212 | _attachProps(nextProps) {
|
213 | const oldPropsAnimated = this._propsAnimated;
|
214 |
|
215 | this._propsAnimated = createOrReusePropsNode(
|
216 | nextProps,
|
217 | this._animatedPropsCallback,
|
218 | oldPropsAnimated
|
219 | );
|
220 |
|
221 | if (oldPropsAnimated !== this._propsAnimated) {
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 | oldPropsAnimated && oldPropsAnimated.__detach();
|
231 | }
|
232 | }
|
233 |
|
234 | _updateFromNative(props) {
|
235 |
|
236 | this._component.setNativeProps?.(props);
|
237 | }
|
238 |
|
239 | _attachPropUpdater() {
|
240 | const viewTag = findNodeHandle(this);
|
241 | NODE_MAPPING.set(viewTag, this);
|
242 | if (NODE_MAPPING.size === 1) {
|
243 | ReanimatedEventEmitter.addListener('onReanimatedPropsChange', listener);
|
244 | }
|
245 | }
|
246 |
|
247 | _attachAnimatedStyles() {
|
248 | let styles = Array.isArray(this.props.style)
|
249 | ? this.props.style
|
250 | : [this.props.style];
|
251 | styles = flattenArray(styles);
|
252 | let viewTag, viewName;
|
253 | if (Platform.OS === 'web') {
|
254 | viewTag = findNodeHandle(this);
|
255 | viewName = null;
|
256 | } else {
|
257 |
|
258 | const hostInstance = RNRenderer.findHostInstance_DEPRECATED(this);
|
259 | if (!hostInstance) {
|
260 | throw new Error(
|
261 | 'Cannot find host instance for this component. Maybe it renders nothing?'
|
262 | );
|
263 | }
|
264 |
|
265 | viewTag = hostInstance?._nativeTag;
|
266 | |
267 |
|
268 |
|
269 |
|
270 | viewName = hostInstance?.viewConfig?.uiViewClassName;
|
271 |
|
272 | if (
|
273 | hostInstance &&
|
274 | this._hasReanimated2Props(styles) &&
|
275 | hostInstance.viewConfig
|
276 | ) {
|
277 | adaptViewConfig(hostInstance.viewConfig);
|
278 | }
|
279 | }
|
280 |
|
281 | styles.forEach((style) => {
|
282 | if (style?.viewDescriptor) {
|
283 | style.viewDescriptor.value = { tag: viewTag, name: viewName };
|
284 | if (process.env.JEST_WORKER_ID) {
|
285 | |
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 | this.animatedStyle.value = {
|
292 | ...this.animatedStyle.value,
|
293 | ...style.initial,
|
294 | };
|
295 | style.animatedStyle.current = this.animatedStyle;
|
296 | }
|
297 | }
|
298 | });
|
299 |
|
300 | if (this.props.animatedProps?.viewDescriptor) {
|
301 | this.props.animatedProps.viewDescriptor.value = {
|
302 | tag: viewTag,
|
303 | name: viewName,
|
304 | };
|
305 | }
|
306 | }
|
307 |
|
308 | _hasReanimated2Props(flattenStyles) {
|
309 | if (this.props.animatedProps?.viewDescriptor) {
|
310 | return true;
|
311 | }
|
312 | if (this.props.style) {
|
313 | for (const style of flattenStyles) {
|
314 |
|
315 | if (style?.hasOwnProperty('viewDescriptor')) {
|
316 | return true;
|
317 | }
|
318 | }
|
319 | }
|
320 | return false;
|
321 | }
|
322 |
|
323 | _detachPropUpdater() {
|
324 | const viewTag = findNodeHandle(this);
|
325 | NODE_MAPPING.delete(viewTag);
|
326 | if (NODE_MAPPING.size === 0) {
|
327 | ReanimatedEventEmitter.removeAllListeners('onReanimatedPropsChange');
|
328 | }
|
329 | }
|
330 |
|
331 | componentDidUpdate(prevProps) {
|
332 | this._attachProps(this.props);
|
333 | this._reattachNativeEvents(prevProps);
|
334 |
|
335 | this._propsAnimated && this._propsAnimated.setNativeView(this._component);
|
336 | }
|
337 |
|
338 | _setComponentRef = setAndForwardRef({
|
339 | getForwardedRef: () => this.props.forwardedRef,
|
340 | setLocalRef: (ref) => {
|
341 | if (ref !== this._component) {
|
342 | this._component = ref;
|
343 | }
|
344 |
|
345 |
|
346 | if (ref != null && ref.getNode == null) {
|
347 | ref.getNode = () => {
|
348 | console.warn(
|
349 | '%s: Calling %s on the ref of an Animated component ' +
|
350 | 'is no longer necessary. You can now directly use the ref ' +
|
351 | 'instead. This method will be removed in a future release.',
|
352 | ref.constructor.name ?? '<<anonymous>>',
|
353 | 'getNode()'
|
354 | );
|
355 | return ref;
|
356 | };
|
357 | }
|
358 | },
|
359 | });
|
360 |
|
361 | _filterNonAnimatedStyle(inputStyle) {
|
362 | const style = {};
|
363 | for (const key in inputStyle) {
|
364 | const value = inputStyle[key];
|
365 | if (!hasAnimatedNodes(value)) {
|
366 | style[key] = value;
|
367 | } else if (value instanceof AnimatedValue) {
|
368 |
|
369 |
|
370 | style[key] = value._startingValue;
|
371 | }
|
372 | }
|
373 | return style;
|
374 | }
|
375 |
|
376 | _filterNonAnimatedProps(inputProps) {
|
377 | const props = {};
|
378 | for (const key in inputProps) {
|
379 | const value = inputProps[key];
|
380 | if (key === 'style') {
|
381 | const styles = Array.isArray(value) ? value : [value];
|
382 | const processedStyle = styles.map((style) => {
|
383 | if (style && style.viewDescriptor) {
|
384 |
|
385 | if (style.viewRef.current === null) {
|
386 | style.viewRef.current = this;
|
387 | }
|
388 | return style.initial;
|
389 | } else {
|
390 | return style;
|
391 | }
|
392 | });
|
393 | props[key] = this._filterNonAnimatedStyle(
|
394 | StyleSheet.flatten(processedStyle)
|
395 | );
|
396 | } else if (key === 'animatedProps') {
|
397 | Object.keys(value.initial).forEach((key) => {
|
398 | props[key] = value.initial[key];
|
399 | });
|
400 | } else if (value instanceof AnimatedEvent) {
|
401 |
|
402 |
|
403 |
|
404 |
|
405 | props[key] = dummyListener;
|
406 | } else if (
|
407 | value?.current &&
|
408 | value.current instanceof WorkletEventHandler
|
409 | ) {
|
410 | if (value.current.eventNames.length > 0) {
|
411 | value.current.eventNames.forEach((eventName) => {
|
412 | props[eventName] = value.current.listeners
|
413 | ? value.current.listeners[eventName]
|
414 | : dummyListener;
|
415 | });
|
416 | } else {
|
417 | props[key] = dummyListener;
|
418 | }
|
419 | } else if (!(value instanceof AnimatedNode)) {
|
420 | props[key] = value;
|
421 | } else if (value instanceof AnimatedValue) {
|
422 |
|
423 |
|
424 | props[key] = value._startingValue;
|
425 | }
|
426 | }
|
427 | return props;
|
428 | }
|
429 |
|
430 | render() {
|
431 | const props = this._filterNonAnimatedProps(this.props);
|
432 | if (process.env.JEST_WORKER_ID) {
|
433 | props.animatedStyle = this.animatedStyle;
|
434 | }
|
435 |
|
436 | const platformProps = Platform.select({
|
437 | web: {},
|
438 | default: { collapsable: false },
|
439 | });
|
440 | return (
|
441 | <Component {...props} ref={this._setComponentRef} {...platformProps} />
|
442 | );
|
443 | }
|
444 | }
|
445 |
|
446 | AnimatedComponent.displayName = `AnimatedComponent(${
|
447 | Component.displayName || Component.name || 'Component'
|
448 | })`;
|
449 |
|
450 | return React.forwardRef(function AnimatedComponentWrapper(props, ref) {
|
451 | return (
|
452 | <AnimatedComponent
|
453 | {...props}
|
454 | {...(ref == null ? null : { forwardedRef: ref })}
|
455 | />
|
456 | );
|
457 | });
|
458 | }
|