UNPKG

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