UNPKG

16 kBJavaScriptView Raw
1import React from 'react';
2import { findNodeHandle, Platform, StyleSheet } from 'react-native';
3import ReanimatedEventEmitter from './ReanimatedEventEmitter';
4
5import AnimatedEvent from './core/AnimatedEvent';
6import AnimatedNode from './core/AnimatedNode';
7import AnimatedValue from './core/AnimatedValue';
8import { createOrReusePropsNode } from './core/AnimatedProps';
9import WorkletEventHandler from './reanimated2/WorkletEventHandler';
10import setAndForwardRef from './setAndForwardRef';
11
12import invariant from 'fbjs/lib/invariant';
13import { adaptViewConfig } from './ConfigHelper';
14import { RNRenderer } from './reanimated2/platform-specific/RNRenderer';
15
16const NODE_MAPPING = new Map();
17
18function listener(data) {
19 const component = NODE_MAPPING.get(data.viewTag);
20 component && component._updateFromNative(data.props);
21}
22
23function dummyListener() {
24 // empty listener we use to assign to listener properties for which animated
25 // event is used.
26}
27
28function 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
41function 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
60export 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 // Make sure to get the scrollable node for components that implement
99 // `ScrollResponder.Mixin`.
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 // event was in prev props but not in current props, we detach
162 prop.detachEvent(node, key);
163 } else {
164 // event was in prev and is still in current props
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 // not yet attached
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 // The system is best designed when setNativeProps is implemented. It is
193 // able to avoid re-rendering and directly set the attributes that changed.
194 // However, setNativeProps can only be implemented on native components
195 // If you want to animate a composite component, you need to re-render it.
196 // In this case, we have a fallback that uses forceUpdate.
197 _animatedPropsCallback = () => {
198 if (this._component == null) {
199 // AnimatedProps is created in will-mount because it's used in render.
200 // But this callback may be invoked before mount in async mode,
201 // In which case we should defer the setNativeProps() call.
202 // React may throw away uncommitted work in async mode,
203 // So a deferred call won't always be invoked.
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 // If prop node has been reused we don't need to call into "__detach"
221 if (oldPropsAnimated !== this._propsAnimated) {
222 // When you call detach, it removes the element from the parent list
223 // of children. If it goes to 0, then the parent also detaches itself
224 // and so on.
225 // An optimization is to attach the new elements and THEN detach the old
226 // ones instead of detaching and THEN attaching.
227 // This way the intermediate state isn't to go to 0 and trigger
228 // this expensive recursive detaching to then re-attach everything on
229 // the very next operation.
230 oldPropsAnimated && oldPropsAnimated.__detach();
231 }
232 }
233
234 _updateFromNative(props) {
235 // eslint-disable-next-line no-unused-expressions
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 // hostInstance can be null for a component that doesn't render anything (render function returns null). Example: svg Stop: https://github.com/react-native-svg/react-native-svg/blob/develop/src/elements/Stop.tsx
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 // we can access view tag in the same way it's accessed here https://github.com/facebook/react/blob/e3f4eb7272d4ca0ee49f27577156b57eeb07cf73/packages/react-native-renderer/src/ReactFabric.js#L146
265 viewTag = hostInstance?._nativeTag;
266 /**
267 * RN uses viewConfig for components for storing different properties of the component(example: https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/ScrollViewViewConfig.js#L16).
268 * The name we're looking for is in the field named uiViewClassName.
269 */
270 viewName = hostInstance?.viewConfig?.uiViewClassName;
271 // update UI props whitelist for this view
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 * We need to connect Jest's TestObject instance whose contains just props object
287 * with the updateProps() function where we update the properties of the component.
288 * We can't update props object directly because TestObject contains a copy of props - look at render function:
289 * const props = this._filterNonAnimatedProps(this.props);
290 */
291 this.animatedStyle.value = {
292 ...this.animatedStyle.value,
293 ...style.initial,
294 };
295 style.animatedStyle.current = this.animatedStyle;
296 }
297 }
298 });
299 // attach animatedProps property
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 // eslint-disable-next-line no-prototype-builtins
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 // TODO: Delete this after React Native also deletes this deprecation helper.
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 // if any style in animated component is set directly to the `Value` we set those styles to the first value of `Value` node in order
369 // to avoid flash of default styles when `Value` is being asynchrounously sent via bridge and initialized in the native side.
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 // this is how we recognize styles returned by useAnimatedStyle
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 // we cannot filter out event listeners completely as some components
402 // rely on having a callback registered in order to generate events
403 // alltogether. Therefore we provide a dummy callback here to allow
404 // native event dispatcher to hijack events.
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 // if any prop in animated component is set directly to the `Value` we set those props to the first value of `Value` node in order
423 // to avoid default values for a short moment when `Value` is being asynchrounously sent via bridge and initialized in the native side.
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}