You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

550 lines
19 KiB

  1. /* @flow */
  2. import mapToZero from './mapToZero';
  3. import stripStyle from './stripStyle';
  4. import stepper from './stepper';
  5. import mergeDiff from './mergeDiff';
  6. import defaultNow from 'performance-now';
  7. import defaultRaf from 'raf';
  8. import shouldStopAnimation from './shouldStopAnimation';
  9. import React from 'react';
  10. import PropTypes from 'prop-types';
  11. import type {
  12. ReactElement,
  13. PlainStyle,
  14. Velocity,
  15. TransitionStyle,
  16. TransitionPlainStyle,
  17. WillEnter,
  18. WillLeave,
  19. DidLeave,
  20. TransitionProps,
  21. } from './Types';
  22. const msPerFrame = 1000 / 60;
  23. // the children function & (potential) styles function asks as param an
  24. // Array<TransitionPlainStyle>, where each TransitionPlainStyle is of the format
  25. // {key: string, data?: any, style: PlainStyle}. However, the way we keep
  26. // internal states doesn't contain such a data structure (check the state and
  27. // TransitionMotionState). So when children function and others ask for such
  28. // data we need to generate them on the fly by combining mergedPropsStyles and
  29. // currentStyles/lastIdealStyles
  30. function rehydrateStyles(
  31. mergedPropsStyles: Array<TransitionStyle>,
  32. unreadPropStyles: ?Array<TransitionStyle>,
  33. plainStyles: Array<PlainStyle>,
  34. ): Array<TransitionPlainStyle> {
  35. // Copy the value to a `const` so that Flow understands that the const won't
  36. // change and will be non-nullable in the callback below.
  37. const cUnreadPropStyles = unreadPropStyles;
  38. if (cUnreadPropStyles == null) {
  39. return mergedPropsStyles.map((mergedPropsStyle, i) => ({
  40. key: mergedPropsStyle.key,
  41. data: mergedPropsStyle.data,
  42. style: plainStyles[i],
  43. }));
  44. }
  45. return mergedPropsStyles.map((mergedPropsStyle, i) => {
  46. for (let j = 0; j < cUnreadPropStyles.length; j++) {
  47. if (cUnreadPropStyles[j].key === mergedPropsStyle.key) {
  48. return {
  49. key: cUnreadPropStyles[j].key,
  50. data: cUnreadPropStyles[j].data,
  51. style: plainStyles[i],
  52. };
  53. }
  54. }
  55. return {key: mergedPropsStyle.key, data: mergedPropsStyle.data, style: plainStyles[i]};
  56. });
  57. }
  58. function shouldStopAnimationAll(
  59. currentStyles: Array<PlainStyle>,
  60. destStyles: Array<TransitionStyle>,
  61. currentVelocities: Array<Velocity>,
  62. mergedPropsStyles: Array<TransitionStyle>,
  63. ): boolean {
  64. if (mergedPropsStyles.length !== destStyles.length) {
  65. return false;
  66. }
  67. for (let i = 0; i < mergedPropsStyles.length; i++) {
  68. if (mergedPropsStyles[i].key !== destStyles[i].key) {
  69. return false;
  70. }
  71. }
  72. // we have the invariant that mergedPropsStyles and
  73. // currentStyles/currentVelocities/last* are synced in terms of cells, see
  74. // mergeAndSync comment for more info
  75. for (let i = 0; i < mergedPropsStyles.length; i++) {
  76. if (!shouldStopAnimation(
  77. currentStyles[i],
  78. destStyles[i].style,
  79. currentVelocities[i])) {
  80. return false;
  81. }
  82. }
  83. return true;
  84. }
  85. // core key merging logic
  86. // things to do: say previously merged style is {a, b}, dest style (prop) is {b,
  87. // c}, previous current (interpolating) style is {a, b}
  88. // **invariant**: current[i] corresponds to merged[i] in terms of key
  89. // steps:
  90. // turn merged style into {a?, b, c}
  91. // add c, value of c is destStyles.c
  92. // maybe remove a, aka call willLeave(a), then merged is either {b, c} or {a, b, c}
  93. // turn current (interpolating) style from {a, b} into {a?, b, c}
  94. // maybe remove a
  95. // certainly add c, value of c is willEnter(c)
  96. // loop over merged and construct new current
  97. // dest doesn't change, that's owner's
  98. function mergeAndSync(
  99. willEnter: WillEnter,
  100. willLeave: WillLeave,
  101. didLeave: DidLeave,
  102. oldMergedPropsStyles: Array<TransitionStyle>,
  103. destStyles: Array<TransitionStyle>,
  104. oldCurrentStyles: Array<PlainStyle>,
  105. oldCurrentVelocities: Array<Velocity>,
  106. oldLastIdealStyles: Array<PlainStyle>,
  107. oldLastIdealVelocities: Array<Velocity>,
  108. ): [Array<TransitionStyle>, Array<PlainStyle>, Array<Velocity>, Array<PlainStyle>, Array<Velocity>] {
  109. const newMergedPropsStyles = mergeDiff(
  110. oldMergedPropsStyles,
  111. destStyles,
  112. (oldIndex, oldMergedPropsStyle) => {
  113. const leavingStyle = willLeave(oldMergedPropsStyle);
  114. if (leavingStyle == null) {
  115. didLeave({ key: oldMergedPropsStyle.key, data: oldMergedPropsStyle.data });
  116. return null;
  117. }
  118. if (shouldStopAnimation(
  119. oldCurrentStyles[oldIndex],
  120. leavingStyle,
  121. oldCurrentVelocities[oldIndex])) {
  122. didLeave({ key: oldMergedPropsStyle.key, data: oldMergedPropsStyle.data });
  123. return null;
  124. }
  125. return {key: oldMergedPropsStyle.key, data: oldMergedPropsStyle.data, style: leavingStyle};
  126. },
  127. );
  128. let newCurrentStyles = [];
  129. let newCurrentVelocities = [];
  130. let newLastIdealStyles = [];
  131. let newLastIdealVelocities = [];
  132. for (let i = 0; i < newMergedPropsStyles.length; i++) {
  133. const newMergedPropsStyleCell = newMergedPropsStyles[i];
  134. let foundOldIndex = null;
  135. for (let j = 0; j < oldMergedPropsStyles.length; j++) {
  136. if (oldMergedPropsStyles[j].key === newMergedPropsStyleCell.key) {
  137. foundOldIndex = j;
  138. break;
  139. }
  140. }
  141. // TODO: key search code
  142. if (foundOldIndex == null) {
  143. const plainStyle = willEnter(newMergedPropsStyleCell);
  144. newCurrentStyles[i] = plainStyle;
  145. newLastIdealStyles[i] = plainStyle;
  146. const velocity = mapToZero(newMergedPropsStyleCell.style);
  147. newCurrentVelocities[i] = velocity;
  148. newLastIdealVelocities[i] = velocity;
  149. } else {
  150. newCurrentStyles[i] = oldCurrentStyles[foundOldIndex];
  151. newLastIdealStyles[i] = oldLastIdealStyles[foundOldIndex];
  152. newCurrentVelocities[i] = oldCurrentVelocities[foundOldIndex];
  153. newLastIdealVelocities[i] = oldLastIdealVelocities[foundOldIndex];
  154. }
  155. }
  156. return [newMergedPropsStyles, newCurrentStyles, newCurrentVelocities, newLastIdealStyles, newLastIdealVelocities];
  157. }
  158. type TransitionMotionDefaultProps = {
  159. willEnter: WillEnter,
  160. willLeave: WillLeave,
  161. didLeave: DidLeave
  162. }
  163. type TransitionMotionState = {
  164. // list of styles, each containing interpolating values. Part of what's passed
  165. // to children function. Notice that this is
  166. // Array<ActualInterpolatingStyleObject>, without the wrapper that is {key: ...,
  167. // data: ... style: ActualInterpolatingStyleObject}. Only mergedPropsStyles
  168. // contains the key & data info (so that we only have a single source of truth
  169. // for these, and to save space). Check the comment for `rehydrateStyles` to
  170. // see how we regenerate the entirety of what's passed to children function
  171. currentStyles: Array<PlainStyle>,
  172. currentVelocities: Array<Velocity>,
  173. lastIdealStyles: Array<PlainStyle>,
  174. lastIdealVelocities: Array<Velocity>,
  175. // the array that keeps track of currently rendered stuff! Including stuff
  176. // that you've unmounted but that's still animating. This is where it lives
  177. mergedPropsStyles: Array<TransitionStyle>,
  178. };
  179. export default class TransitionMotion extends React.Component<TransitionProps, TransitionMotionState> {
  180. static propTypes = {
  181. defaultStyles: PropTypes.arrayOf(PropTypes.shape({
  182. key: PropTypes.string.isRequired,
  183. data: PropTypes.any,
  184. style: PropTypes.objectOf(PropTypes.number).isRequired,
  185. })),
  186. styles: PropTypes.oneOfType([
  187. PropTypes.func,
  188. PropTypes.arrayOf(PropTypes.shape({
  189. key: PropTypes.string.isRequired,
  190. data: PropTypes.any,
  191. style: PropTypes.objectOf(PropTypes.oneOfType([
  192. PropTypes.number,
  193. PropTypes.object,
  194. ])).isRequired,
  195. }),
  196. )]).isRequired,
  197. children: PropTypes.func.isRequired,
  198. willEnter: PropTypes.func,
  199. willLeave: PropTypes.func,
  200. didLeave: PropTypes.func,
  201. };
  202. static defaultProps: TransitionMotionDefaultProps = {
  203. willEnter: styleThatEntered => stripStyle(styleThatEntered.style),
  204. // recall: returning null makes the current unmounting TransitionStyle
  205. // disappear immediately
  206. willLeave: () => null,
  207. didLeave: () => {},
  208. };
  209. unmounting: boolean = false;
  210. animationID: ?number = null;
  211. prevTime = 0;
  212. accumulatedTime = 0;
  213. // it's possible that currentStyle's value is stale: if props is immediately
  214. // changed from 0 to 400 to spring(0) again, the async currentStyle is still
  215. // at 0 (didn't have time to tick and interpolate even once). If we naively
  216. // compare currentStyle with destVal it'll be 0 === 0 (no animation, stop).
  217. // In reality currentStyle should be 400
  218. unreadPropStyles: ?Array<TransitionStyle> = null;
  219. constructor(props: TransitionProps) {
  220. super(props);
  221. this.state = this.defaultState();
  222. }
  223. defaultState(): TransitionMotionState {
  224. const {defaultStyles, styles, willEnter, willLeave, didLeave} = this.props;
  225. const destStyles: Array<TransitionStyle> = typeof styles === 'function' ? styles(defaultStyles) : styles;
  226. // this is special. for the first time around, we don't have a comparison
  227. // between last (no last) and current merged props. we'll compute last so:
  228. // say default is {a, b} and styles (dest style) is {b, c}, we'll
  229. // fabricate last as {a, b}
  230. let oldMergedPropsStyles: Array<TransitionStyle>;
  231. if (defaultStyles == null) {
  232. oldMergedPropsStyles = destStyles;
  233. } else {
  234. oldMergedPropsStyles = (defaultStyles: any).map(defaultStyleCell => {
  235. // TODO: key search code
  236. for (let i = 0; i < destStyles.length; i++) {
  237. if (destStyles[i].key === defaultStyleCell.key) {
  238. return destStyles[i];
  239. }
  240. }
  241. return defaultStyleCell;
  242. });
  243. }
  244. const oldCurrentStyles = defaultStyles == null
  245. ? destStyles.map(s => stripStyle(s.style))
  246. : (defaultStyles: any).map(s => stripStyle(s.style));
  247. const oldCurrentVelocities = defaultStyles == null
  248. ? destStyles.map(s => mapToZero(s.style))
  249. : defaultStyles.map(s => mapToZero(s.style));
  250. const [mergedPropsStyles, currentStyles, currentVelocities, lastIdealStyles, lastIdealVelocities] = mergeAndSync(
  251. // Because this is an old-style createReactClass component, Flow doesn't
  252. // understand that the willEnter and willLeave props have default values
  253. // and will always be present.
  254. (willEnter: any),
  255. (willLeave: any),
  256. (didLeave: any),
  257. oldMergedPropsStyles,
  258. destStyles,
  259. oldCurrentStyles,
  260. oldCurrentVelocities,
  261. oldCurrentStyles, // oldLastIdealStyles really
  262. oldCurrentVelocities, // oldLastIdealVelocities really
  263. );
  264. return {
  265. currentStyles,
  266. currentVelocities,
  267. lastIdealStyles,
  268. lastIdealVelocities,
  269. mergedPropsStyles,
  270. };
  271. }
  272. // after checking for unreadPropStyles != null, we manually go set the
  273. // non-interpolating values (those that are a number, without a spring
  274. // config)
  275. clearUnreadPropStyle = (unreadPropStyles: Array<TransitionStyle>): void => {
  276. let [mergedPropsStyles, currentStyles, currentVelocities, lastIdealStyles, lastIdealVelocities] = mergeAndSync(
  277. (this.props.willEnter: any),
  278. (this.props.willLeave: any),
  279. (this.props.didLeave: any),
  280. this.state.mergedPropsStyles,
  281. unreadPropStyles,
  282. this.state.currentStyles,
  283. this.state.currentVelocities,
  284. this.state.lastIdealStyles,
  285. this.state.lastIdealVelocities,
  286. );
  287. for (let i = 0; i < unreadPropStyles.length; i++) {
  288. const unreadPropStyle = unreadPropStyles[i].style;
  289. let dirty = false;
  290. for (let key in unreadPropStyle) {
  291. if (!Object.prototype.hasOwnProperty.call(unreadPropStyle, key)) {
  292. continue;
  293. }
  294. const styleValue = unreadPropStyle[key];
  295. if (typeof styleValue === 'number') {
  296. if (!dirty) {
  297. dirty = true;
  298. currentStyles[i] = {...currentStyles[i]};
  299. currentVelocities[i] = {...currentVelocities[i]};
  300. lastIdealStyles[i] = {...lastIdealStyles[i]};
  301. lastIdealVelocities[i] = {...lastIdealVelocities[i]};
  302. mergedPropsStyles[i] = {
  303. key: mergedPropsStyles[i].key,
  304. data: mergedPropsStyles[i].data,
  305. style: {...mergedPropsStyles[i].style},
  306. };
  307. }
  308. currentStyles[i][key] = styleValue;
  309. currentVelocities[i][key] = 0;
  310. lastIdealStyles[i][key] = styleValue;
  311. lastIdealVelocities[i][key] = 0;
  312. mergedPropsStyles[i].style[key] = styleValue;
  313. }
  314. }
  315. }
  316. // unlike the other 2 components, we can't detect staleness and optionally
  317. // opt out of setState here. each style object's data might contain new
  318. // stuff we're not/cannot compare
  319. this.setState({
  320. currentStyles,
  321. currentVelocities,
  322. mergedPropsStyles,
  323. lastIdealStyles,
  324. lastIdealVelocities,
  325. });
  326. }
  327. startAnimationIfNecessary = (): void => {
  328. if (this.unmounting) {
  329. return;
  330. }
  331. // TODO: when config is {a: 10} and dest is {a: 10} do we raf once and
  332. // call cb? No, otherwise accidental parent rerender causes cb trigger
  333. this.animationID = defaultRaf((timestamp) => {
  334. // https://github.com/chenglou/react-motion/pull/420
  335. // > if execution passes the conditional if (this.unmounting), then
  336. // executes async defaultRaf and after that component unmounts and after
  337. // that the callback of defaultRaf is called, then setState will be called
  338. // on unmounted component.
  339. if (this.unmounting) {
  340. return;
  341. }
  342. const propStyles = this.props.styles;
  343. let destStyles: Array<TransitionStyle> = typeof propStyles === 'function'
  344. ? propStyles(rehydrateStyles(
  345. this.state.mergedPropsStyles,
  346. this.unreadPropStyles,
  347. this.state.lastIdealStyles,
  348. ))
  349. : propStyles;
  350. // check if we need to animate in the first place
  351. if (shouldStopAnimationAll(
  352. this.state.currentStyles,
  353. destStyles,
  354. this.state.currentVelocities,
  355. this.state.mergedPropsStyles,
  356. )) {
  357. // no need to cancel animationID here; shouldn't have any in flight
  358. this.animationID = null;
  359. this.accumulatedTime = 0;
  360. return;
  361. }
  362. const currentTime = timestamp || defaultNow();
  363. const timeDelta = currentTime - this.prevTime;
  364. this.prevTime = currentTime;
  365. this.accumulatedTime = this.accumulatedTime + timeDelta;
  366. // more than 10 frames? prolly switched browser tab. Restart
  367. if (this.accumulatedTime > msPerFrame * 10) {
  368. this.accumulatedTime = 0;
  369. }
  370. if (this.accumulatedTime === 0) {
  371. // no need to cancel animationID here; shouldn't have any in flight
  372. this.animationID = null;
  373. this.startAnimationIfNecessary();
  374. return;
  375. }
  376. let currentFrameCompletion =
  377. (this.accumulatedTime - Math.floor(this.accumulatedTime / msPerFrame) * msPerFrame) / msPerFrame;
  378. const framesToCatchUp = Math.floor(this.accumulatedTime / msPerFrame);
  379. let [newMergedPropsStyles, newCurrentStyles, newCurrentVelocities, newLastIdealStyles, newLastIdealVelocities] = mergeAndSync(
  380. (this.props.willEnter: any),
  381. (this.props.willLeave: any),
  382. (this.props.didLeave: any),
  383. this.state.mergedPropsStyles,
  384. destStyles,
  385. this.state.currentStyles,
  386. this.state.currentVelocities,
  387. this.state.lastIdealStyles,
  388. this.state.lastIdealVelocities,
  389. );
  390. for (let i = 0; i < newMergedPropsStyles.length; i++) {
  391. const newMergedPropsStyle = newMergedPropsStyles[i].style;
  392. let newCurrentStyle: PlainStyle = {};
  393. let newCurrentVelocity: Velocity = {};
  394. let newLastIdealStyle: PlainStyle = {};
  395. let newLastIdealVelocity: Velocity = {};
  396. for (let key in newMergedPropsStyle) {
  397. if (!Object.prototype.hasOwnProperty.call(newMergedPropsStyle, key)) {
  398. continue;
  399. }
  400. const styleValue = newMergedPropsStyle[key];
  401. if (typeof styleValue === 'number') {
  402. newCurrentStyle[key] = styleValue;
  403. newCurrentVelocity[key] = 0;
  404. newLastIdealStyle[key] = styleValue;
  405. newLastIdealVelocity[key] = 0;
  406. } else {
  407. let newLastIdealStyleValue = newLastIdealStyles[i][key];
  408. let newLastIdealVelocityValue = newLastIdealVelocities[i][key];
  409. for (let j = 0; j < framesToCatchUp; j++) {
  410. [newLastIdealStyleValue, newLastIdealVelocityValue] = stepper(
  411. msPerFrame / 1000,
  412. newLastIdealStyleValue,
  413. newLastIdealVelocityValue,
  414. styleValue.val,
  415. styleValue.stiffness,
  416. styleValue.damping,
  417. styleValue.precision,
  418. );
  419. }
  420. const [nextIdealX, nextIdealV] = stepper(
  421. msPerFrame / 1000,
  422. newLastIdealStyleValue,
  423. newLastIdealVelocityValue,
  424. styleValue.val,
  425. styleValue.stiffness,
  426. styleValue.damping,
  427. styleValue.precision,
  428. );
  429. newCurrentStyle[key] =
  430. newLastIdealStyleValue +
  431. (nextIdealX - newLastIdealStyleValue) * currentFrameCompletion;
  432. newCurrentVelocity[key] =
  433. newLastIdealVelocityValue +
  434. (nextIdealV - newLastIdealVelocityValue) * currentFrameCompletion;
  435. newLastIdealStyle[key] = newLastIdealStyleValue;
  436. newLastIdealVelocity[key] = newLastIdealVelocityValue;
  437. }
  438. }
  439. newLastIdealStyles[i] = newLastIdealStyle;
  440. newLastIdealVelocities[i] = newLastIdealVelocity;
  441. newCurrentStyles[i] = newCurrentStyle;
  442. newCurrentVelocities[i] = newCurrentVelocity;
  443. }
  444. this.animationID = null;
  445. // the amount we're looped over above
  446. this.accumulatedTime -= framesToCatchUp * msPerFrame;
  447. this.setState({
  448. currentStyles: newCurrentStyles,
  449. currentVelocities: newCurrentVelocities,
  450. lastIdealStyles: newLastIdealStyles,
  451. lastIdealVelocities: newLastIdealVelocities,
  452. mergedPropsStyles: newMergedPropsStyles,
  453. });
  454. this.unreadPropStyles = null;
  455. this.startAnimationIfNecessary();
  456. });
  457. }
  458. componentDidMount() {
  459. this.prevTime = defaultNow();
  460. this.startAnimationIfNecessary();
  461. }
  462. componentWillReceiveProps(props: TransitionProps) {
  463. if (this.unreadPropStyles) {
  464. // previous props haven't had the chance to be set yet; set them here
  465. this.clearUnreadPropStyle(this.unreadPropStyles);
  466. }
  467. const styles = props.styles;
  468. if (typeof styles === 'function') {
  469. this.unreadPropStyles = styles(
  470. rehydrateStyles(
  471. this.state.mergedPropsStyles,
  472. this.unreadPropStyles,
  473. this.state.lastIdealStyles,
  474. )
  475. );
  476. } else {
  477. this.unreadPropStyles = styles;
  478. }
  479. if (this.animationID == null) {
  480. this.prevTime = defaultNow();
  481. this.startAnimationIfNecessary();
  482. }
  483. }
  484. componentWillUnmount() {
  485. this.unmounting = true;
  486. if (this.animationID != null) {
  487. defaultRaf.cancel(this.animationID);
  488. this.animationID = null;
  489. }
  490. }
  491. render(): ReactElement {
  492. const hydratedStyles = rehydrateStyles(
  493. this.state.mergedPropsStyles,
  494. this.unreadPropStyles,
  495. this.state.currentStyles,
  496. );
  497. const renderedChildren = this.props.children(hydratedStyles);
  498. return renderedChildren && React.Children.only(renderedChildren);
  499. }
  500. }