react-spring
Version:
Animate React with ease
289 lines (256 loc) • 9.54 kB
JavaScript
import React from 'react'
import PropTypes from 'prop-types'
import controller from './animated/AnimatedController'
import AnimatedValue from './animated/AnimatedValue'
import AnimatedArray from './animated/AnimatedArray'
import AnimatedProps from './animated/AnimatedProps'
import SpringAnimation from './animated/SpringAnimation'
import * as Globals from './animated/Globals'
import { config } from './targets/shared/constants'
import {
renderChildren,
convertValues,
callProp,
shallowEqual,
getValues,
} from './targets/shared/helpers'
export default class Spring extends React.Component {
static propTypes = {
to: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
from: PropTypes.object,
native: PropTypes.bool,
onStart: PropTypes.func,
onRest: PropTypes.func,
onFrame: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.func,
PropTypes.arrayOf(PropTypes.func),
PropTypes.node,
]),
render: PropTypes.func,
reset: PropTypes.bool,
force: PropTypes.bool,
config: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
immediate: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
impl: PropTypes.func,
inject: PropTypes.func,
delay: PropTypes.number,
}
static defaultProps = {
from: {},
to: {},
config: config.default,
native: false,
immediate: false,
reset: false,
force: false,
impl: SpringAnimation,
inject: Globals.bugfixes,
}
state = {
lastProps: { from: {}, to: {} },
changed: false,
dry: false,
}
didUpdate = false
didInject = false
animations = {}
interpolators = {}
componentDidMount() {
// componentDidUpdate isn't called on mount, we call it here to start animating
this.componentDidUpdate()
this.mounted = true
}
componentWillUnmount() {
// Stop all ongoing animtions
this.mounted = false
this.stop()
}
static getDerivedStateFromProps(props, { changed, dry, lastProps }) {
// The following is a test against props that could alter the animation
const { from, to, reset, force } = props
changed =
!shallowEqual(to, lastProps.to) ||
!shallowEqual(from, lastProps.from) ||
reset ||
(force && !dry)
return { changed, lastProps: props, dry: false }
}
render() {
const propsChanged = this.state.changed
// Handle injected frames, for instance targets/web/fix-auto
// An inject will return an intermediary React node which measures itself out
// .. and returns a callback when the values sought after are ready, usually "auto".
if (this.props.inject && propsChanged && !this.injectProps) {
const frame = this.props.inject(this.props, injectProps => {
// The inject frame has rendered, now let's update animations...
this.injectProps = injectProps
this.setState({ dry: true })
})
// Render out injected frame
if (frame) return frame
}
// Update animations, this turns from/to props into AnimatedValues
// An update can occur on injected props, or when own-props have changed.
if (this.injectProps) {
this.updateAnimations(this.injectProps)
this.injectProps = undefined
// didInject is needed, because there will be a 3rd stage, where the original values
// .. will be restored after the animation is finished. When someone animates towards
// .. "auto", the end-result should be "auto", not "1999px", which would block nested
// .. height/width changes.
this.didInject = true
} else if (propsChanged) this.updateAnimations(this.props)
// Render out raw values or AnimatedValues depending on "native"
const values = this.getAnimatedValues()
return values && Object.keys(values).length
? renderChildren(this.props, {
...values,
...this.afterInject,
})
: null
}
componentDidUpdate() {
// The animation has to start *after* render, since at that point the scene
// .. graph should be established, so we do it here. Unfortunatelly, non-native
// .. animations as well as "auto" injects call forceUpdate, so it's causing a loop.
// .. didUpdate prevents that as it gets set only on prop changes.
if (this.didUpdate) {
if (this.props.delay) {
if (this.timeout) clearTimeout(this.timeout)
this.timeout = setTimeout(this.start, this.props.delay)
} else this.start()
}
this.didUpdate = false
}
updateAnimations({ from, to, attach, reset, immediate, onFrame, native }) {
// This function will turn own-props into AnimatedValues, it tries to re-use
// .. exsting animations as best as it can by detecting the changes made
// Attachment handling, trailed springs can "attach" themselves to a previous spring
let target = attach && attach(this)
let animationsChanged = false
let allProps = Object.entries({ ...from, ...to })
this.animations = allProps.reduce((acc, [name, value], i) => {
const entry = (reset === false && acc[name]) || { stopped: true }
const isNumber = typeof value === 'number'
const isString =
typeof value === 'string' &&
!value.startsWith('#') &&
!/\d/.test(value) &&
!Globals.colorNames[value]
const isArray = !isNumber && !isString && Array.isArray(value)
const fromValue = from[name] !== undefined ? from[name] : value
const fromAnimated = fromValue instanceof AnimatedValue
let toValue = isNumber || isArray ? value : 1
if (target) {
// Attach value to target animation
const attachedAnimation = target.animations[name]
if (attachedAnimation) toValue = attachedAnimation.animation
}
let old = entry.animation
let animation, interpolation
if (fromAnimated) {
// Use provided animated value
animation = interpolation = fromValue
} else if (isNumber || isString) {
// Create animated value
animation = interpolation =
entry.animation || new AnimatedValue(fromValue)
} else if (isArray) {
// Create animated array
animation = interpolation =
entry.animation || new AnimatedArray(fromValue)
} else {
// Deal with interpolations
const previous =
entry.interpolation &&
entry.interpolation._interpolation(entry.animation._value)
if (entry.animation) {
animation = entry.animation
animation.setValue(0)
} else animation = new AnimatedValue(0)
const config = {
range: [0, 1],
output: [previous !== undefined ? previous : fromValue, value],
}
if (entry.interpolation)
interpolation = entry.interpolation.__update(config)
else interpolation = animation.interpolate(config)
}
if (old !== animation) animationsChanged = true
// Set immediate values
if (callProp(immediate, name)) animation.setValue(toValue)
// Save interpolators
this.interpolators = { ...this.interpolators, [name]: interpolation }
return {
...acc,
[name]: {
...entry,
name,
animation,
interpolation,
toValue,
stopped: false,
},
}
}, this.animations)
// Update animated props (which from now on will take care of the animation)
if (animationsChanged) {
const oldAnimatedProps = this.animatedProps
this.animatedProps = new AnimatedProps(this.interpolators, () => {
// This gets called on every animation frame ...
if (onFrame) onFrame(this.animatedProps.__getValue())
if (!native) this.setState({ dry: true })
})
oldAnimatedProps && oldAnimatedProps.__detach()
}
// Flag an update that occured, componentDidUpdate will start the animation later on
this.didUpdate = true
this.afterInject = undefined
this.didInject = false
}
start = () => {
const { config, impl } = this.props
if (this.props.onStart) this.props.onStart()
Object.keys(this.animations).forEach(name => {
const { animation, toValue: to } = this.animations[name]
// TODO: figure out why this is needed ...
if (!to.__getValue && animation.__getValue() === to)
return this.finishAnimation(name)
controller(animation, { to, ...callProp(config, name) }, impl).start(
!to.__getValue &&
(props => props.finished && this.finishAnimation(name))
)
})
}
stop = () =>
getValues(this.animations).forEach(({ animation }) =>
animation.stopAnimation()
)
finishAnimation = name => {
if (!this.mounted) return
const { animation, toValue: to } = this.animations[name]
this.animations[name].stopped = true
if (getValues(this.animations).every(a => a.stopped)) {
const current = { ...this.props.from, ...this.props.to }
if (this.props.onRest) this.props.onRest(current)
// Restore end-state
if (this.didInject) {
this.afterInject = convertValues(this.props)
this.didInject = false
this.setState({ dry: true })
}
}
}
flush() {
getValues(this.animations).forEach(
({ animation }) => animation._update && animation._update()
)
}
getValues() {
return this.animatedProps ? this.animatedProps.__getValue() : {}
}
getAnimatedValues() {
return this.props.native ? this.interpolators : this.getValues()
}
}