forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcreateAnimatedComponent.js
More file actions
223 lines (199 loc) · 8.17 KB
/
createAnimatedComponent.js
File metadata and controls
223 lines (199 loc) · 8.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const {AnimatedEvent} = require('./AnimatedEvent');
const AnimatedProps = require('./nodes/AnimatedProps');
const React = require('react');
const invariant = require('invariant');
const setAndForwardRef = require('../../Utilities/setAndForwardRef');
export type AnimatedComponentType<
Props: {+[string]: mixed, ...},
Instance,
> = React.AbstractComponent<$ObjMap<Props, () => any>, Instance>;
function createAnimatedComponent<Props: {+[string]: mixed, ...}, Instance>(
Component: React.AbstractComponent<Props, Instance>,
): AnimatedComponentType<Props, Instance> {
invariant(
typeof Component !== 'function' ||
(Component.prototype && Component.prototype.isReactComponent),
'`createAnimatedComponent` does not support stateless functional components; ' +
'use a class component instead.',
);
class AnimatedComponent extends React.Component<Object> {
_component: any; // TODO T53738161: flow type this, and the whole file
_invokeAnimatedPropsCallbackOnMount: boolean = false;
_prevComponent: any;
_propsAnimated: AnimatedProps;
_eventDetachers: Array<Function> = [];
_attachNativeEvents() {
// Make sure to get the scrollable node for components that implement
// `ScrollResponder.Mixin`.
const scrollableNode = this._component?.getScrollableNode
? this._component.getScrollableNode()
: this._component;
for (const key in this.props) {
const prop = this.props[key];
if (prop instanceof AnimatedEvent && prop.__isNative) {
prop.__attach(scrollableNode, key);
this._eventDetachers.push(() => prop.__detach(scrollableNode, key));
}
}
}
_detachNativeEvents() {
this._eventDetachers.forEach(remove => remove());
this._eventDetachers = [];
}
// The system is best designed when setNativeProps is implemented. It is
// able to avoid re-rendering and directly set the attributes that changed.
// However, setNativeProps can only be implemented on leaf native
// components. If you want to animate a composite component, you need to
// re-render it. In this case, we have a fallback that uses forceUpdate.
_animatedPropsCallback = () => {
if (this._component == null) {
// AnimatedProps is created in will-mount because it's used in render.
// But this callback may be invoked before mount in async mode,
// In which case we should defer the setNativeProps() call.
// React may throw away uncommitted work in async mode,
// So a deferred call won't always be invoked.
this._invokeAnimatedPropsCallbackOnMount = true;
} else if (
process.env.NODE_ENV === 'test' ||
// For animating properties of non-leaf/non-native components
typeof this._component.setNativeProps !== 'function' ||
// In Fabric, force animations to go through forceUpdate and skip setNativeProps
// eslint-disable-next-line dot-notation
this._component['_internalInstanceHandle']?.stateNode?.canonical !=
null ||
// Some components have a setNativeProps function but aren't a host component
// such as lists like FlatList and SectionList. These should also use
// forceUpdate in Fabric since setNativeProps doesn't exist on the underlying
// host component. This crazy hack is essentially special casing those lists and
// ScrollView itself to use forceUpdate in Fabric.
// If these components end up using forwardRef then these hacks can go away
// as this._component would actually be the underlying host component and the above check
// would be sufficient.
(this._component.getNativeScrollRef != null &&
this._component.getNativeScrollRef() != null &&
// eslint-disable-next-line dot-notation
this._component.getNativeScrollRef()['_internalInstanceHandle']
?.stateNode?.canonical != null) ||
(this._component.getScrollResponder != null &&
this._component.getScrollResponder().getNativeScrollRef != null &&
this._component.getScrollResponder().getNativeScrollRef() != null &&
this._component.getScrollResponder().getNativeScrollRef()[
// eslint-disable-next-line dot-notation
'_internalInstanceHandle'
]?.stateNode?.canonical != null)
) {
this.forceUpdate();
} else if (!this._propsAnimated.__isNative) {
this._component.setNativeProps(
this._propsAnimated.__getAnimatedValue(),
);
} else {
throw new Error(
'Attempting to run JS driven animation on animated ' +
'node that has been moved to "native" earlier by starting an ' +
'animation with `useNativeDriver: true`',
);
}
};
_attachProps(nextProps) {
const oldPropsAnimated = this._propsAnimated;
this._propsAnimated = new AnimatedProps(
nextProps,
this._animatedPropsCallback,
);
// When you call detach, it removes the element from the parent list
// of children. If it goes to 0, then the parent also detaches itself
// and so on.
// An optimization is to attach the new elements and THEN detach the old
// ones instead of detaching and THEN attaching.
// This way the intermediate state isn't to go to 0 and trigger
// this expensive recursive detaching to then re-attach everything on
// the very next operation.
if (oldPropsAnimated) {
oldPropsAnimated.__restoreDefaultValues();
oldPropsAnimated.__detach();
}
}
_setComponentRef = setAndForwardRef({
getForwardedRef: () => this.props.forwardedRef,
setLocalRef: ref => {
this._prevComponent = this._component;
this._component = ref;
// TODO: Delete this in a future release.
if (ref != null && ref.getNode == null) {
ref.getNode = () => {
console.warn(
'%s: Calling `getNode()` on the ref of an Animated component ' +
'is no longer necessary. You can now directly use the ref ' +
'instead. This method will be removed in a future release.',
ref.constructor.name ?? '<<anonymous>>',
);
return ref;
};
}
},
});
render() {
const props = this._propsAnimated.__getValue();
return (
<Component
{...props}
ref={this._setComponentRef}
// The native driver updates views directly through the UI thread so we
// have to make sure the view doesn't get optimized away because it cannot
// go through the NativeViewHierarchyManager since it operates on the shadow
// thread.
collapsable={
this._propsAnimated.__isNative ? false : props.collapsable
}
/>
);
}
UNSAFE_componentWillMount() {
this._attachProps(this.props);
}
componentDidMount() {
if (this._invokeAnimatedPropsCallbackOnMount) {
this._invokeAnimatedPropsCallbackOnMount = false;
this._animatedPropsCallback();
}
this._propsAnimated.setNativeView(this._component);
this._attachNativeEvents();
}
UNSAFE_componentWillReceiveProps(newProps) {
this._attachProps(newProps);
}
componentDidUpdate(prevProps) {
if (this._component !== this._prevComponent) {
this._propsAnimated.setNativeView(this._component);
}
if (this._component !== this._prevComponent || prevProps !== this.props) {
this._detachNativeEvents();
this._attachNativeEvents();
}
}
componentWillUnmount() {
this._propsAnimated && this._propsAnimated.__detach();
this._detachNativeEvents();
}
}
return React.forwardRef(function AnimatedComponentWrapper(props, ref) {
return (
<AnimatedComponent
{...props}
{...(ref == null ? null : {forwardedRef: ref})}
/>
);
});
}
module.exports = createAnimatedComponent;