UNPKG

8.02 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 { createOrReusePropsNode } from './core/AnimatedProps';
8
9import invariant from 'fbjs/lib/invariant';
10
11const NODE_MAPPING = new Map();
12
13function listener(data) {
14 const component = NODE_MAPPING.get(data.viewTag);
15 component && component._updateFromNative(data.props);
16}
17
18function dummyListener() {
19 // empty listener we use to assign to listener properties for which animated
20 // event is used.
21}
22
23export default function createAnimatedComponent(Component) {
24 invariant(
25 typeof Component !== 'function' ||
26 (Component.prototype && Component.prototype.isReactComponent),
27 '`createAnimatedComponent` does not support stateless functional components; ' +
28 'use a class component instead.'
29 );
30
31 class AnimatedComponent extends React.Component {
32 _invokeAnimatedPropsCallbackOnMount = false;
33
34 constructor(props) {
35 super(props);
36 this._attachProps(this.props);
37 }
38
39 componentWillUnmount() {
40 this._detachPropUpdater();
41 this._propsAnimated && this._propsAnimated.__detach();
42 this._detachNativeEvents();
43 }
44
45 setNativeProps(props) {
46 this._component.setNativeProps(props);
47 }
48
49 componentDidMount() {
50 if (this._invokeAnimatedPropsCallbackOnMount) {
51 this._invokeAnimatedPropsCallbackOnMount = false;
52 this._animatedPropsCallback();
53 }
54
55 this._propsAnimated.setNativeView(this._component);
56 this._attachNativeEvents();
57 this._attachPropUpdater();
58 }
59
60 _getEventViewRef() {
61 // Make sure to get the scrollable node for components that implement
62 // `ScrollResponder.Mixin`.
63 return this._component.getScrollableNode
64 ? this._component.getScrollableNode()
65 : this._component;
66 }
67
68 _attachNativeEvents() {
69 const node = this._getEventViewRef();
70
71 for (const key in this.props) {
72 const prop = this.props[key];
73 if (prop instanceof AnimatedEvent) {
74 prop.attachEvent(node, key);
75 }
76 }
77 }
78
79 _detachNativeEvents() {
80 const node = this._getEventViewRef();
81
82 for (const key in this.props) {
83 const prop = this.props[key];
84 if (prop instanceof AnimatedEvent) {
85 prop.detachEvent(node, key);
86 }
87 }
88 }
89
90 _reattachNativeEvents(prevProps) {
91 const node = this._getEventViewRef();
92 const attached = new Set();
93 const nextEvts = new Set();
94 for (const key in this.props) {
95 const prop = this.props[key];
96 if (prop instanceof AnimatedEvent) {
97 nextEvts.add(prop.__nodeID);
98 }
99 }
100 for (const key in prevProps) {
101 const prop = this.props[key];
102 if (prop instanceof AnimatedEvent) {
103 if (!nextEvts.has(prop.__nodeID)) {
104 // event was in prev props but not in current props, we detach
105 prop.detachEvent(node, key);
106 } else {
107 // event was in prev and is still in current props
108 attached.add(prop.__nodeID);
109 }
110 }
111 }
112 for (const key in this.props) {
113 const prop = this.props[key];
114 if (prop instanceof AnimatedEvent && !attached.has(prop.__nodeID)) {
115 // not yet attached
116 prop.attachEvent(node, key);
117 }
118 }
119 }
120
121 // The system is best designed when setNativeProps is implemented. It is
122 // able to avoid re-rendering and directly set the attributes that changed.
123 // However, setNativeProps can only be implemented on native components
124 // If you want to animate a composite component, you need to re-render it.
125 // In this case, we have a fallback that uses forceUpdate.
126 _animatedPropsCallback = () => {
127 if (this._component == null) {
128 // AnimatedProps is created in will-mount because it's used in render.
129 // But this callback may be invoked before mount in async mode,
130 // In which case we should defer the setNativeProps() call.
131 // React may throw away uncommitted work in async mode,
132 // So a deferred call won't always be invoked.
133 this._invokeAnimatedPropsCallbackOnMount = true;
134 } else if (typeof this._component.setNativeProps !== 'function') {
135 this.forceUpdate();
136 } else {
137 this._component.setNativeProps(this._propsAnimated.__getValue());
138 }
139 };
140
141 _attachProps(nextProps) {
142 const oldPropsAnimated = this._propsAnimated;
143
144 this._propsAnimated = createOrReusePropsNode(
145 nextProps,
146 this._animatedPropsCallback,
147 oldPropsAnimated
148 );
149 // If prop node has been reused we don't need to call into "__detach"
150 if (oldPropsAnimated !== this._propsAnimated) {
151 // When you call detach, it removes the element from the parent list
152 // of children. If it goes to 0, then the parent also detaches itself
153 // and so on.
154 // An optimization is to attach the new elements and THEN detach the old
155 // ones instead of detaching and THEN attaching.
156 // This way the intermediate state isn't to go to 0 and trigger
157 // this expensive recursive detaching to then re-attach everything on
158 // the very next operation.
159 oldPropsAnimated && oldPropsAnimated.__detach();
160 }
161 }
162
163 _updateFromNative(props) {
164 this._component.setNativeProps(props);
165 }
166
167 _attachPropUpdater() {
168 const viewTag = findNodeHandle(this);
169 NODE_MAPPING.set(viewTag, this);
170 if (NODE_MAPPING.size === 1) {
171 ReanimatedEventEmitter.addListener('onReanimatedPropsChange', listener);
172 }
173 }
174
175 _detachPropUpdater() {
176 const viewTag = findNodeHandle(this);
177 NODE_MAPPING.delete(viewTag);
178 if (NODE_MAPPING.size === 0) {
179 ReanimatedEventEmitter.removeAllListeners('onReanimatedPropsChange');
180 }
181 }
182
183 componentDidUpdate(prevProps) {
184 this._attachProps(this.props);
185 this._reattachNativeEvents(prevProps);
186
187 this._propsAnimated.setNativeView(this._component);
188 }
189
190 _setComponentRef = c => {
191 if (c !== this._component) {
192 this._component = c;
193 }
194 };
195
196 _filterNonAnimatedStyle(inputStyle) {
197 const style = {};
198 for (const key in inputStyle) {
199 const value = inputStyle[key];
200 if (!(value instanceof AnimatedNode) && key !== 'transform') {
201 style[key] = value;
202 }
203 }
204 return style;
205 }
206
207 _filterNonAnimatedProps(inputProps) {
208 const props = {};
209 for (const key in inputProps) {
210 const value = inputProps[key];
211 if (key === 'style') {
212 props[key] = this._filterNonAnimatedStyle(StyleSheet.flatten(value));
213 } else if (value instanceof AnimatedEvent) {
214 // we cannot filter out event listeners completely as some components
215 // rely on having a callback registered in order to generate events
216 // alltogether. Therefore we provide a dummy callback here to allow
217 // native event dispatcher to hijack events.
218 props[key] = dummyListener;
219 } else if (!(value instanceof AnimatedNode)) {
220 props[key] = value;
221 }
222 }
223 return props;
224 }
225
226 render() {
227 const props = this._filterNonAnimatedProps(this.props);
228 const platformProps = Platform.select({
229 web: {},
230 default: { collapsable: false },
231 });
232 return (
233 <Component {...props} ref={this._setComponentRef} {...platformProps} />
234 );
235 }
236
237 // A third party library can use getNode()
238 // to get the node reference of the decorated component
239 getNode() {
240 return this._component;
241 }
242 }
243
244 AnimatedComponent.displayName = `AnimatedComponent(${Component.displayName || Component.name || 'Component'})`
245
246 return AnimatedComponent;
247}