@bankify/react-native-animate-number
Version:
Animated number component
190 lines (159 loc) • 4.42 kB
JavaScript
/**
* Component that will animate number value changes
* @author wkh237
* @maintainer Bankify Ltd
* @version 0.1.1
*/
// @flow
import * as React from 'react';
import {
Text,
View
} from 'react-native';
const HALF_RAD = Math.PI/2
type Props = {
countBy? : ?number,
interval? : ?number,
steps? : ?number,
value : number,
initial : ?number,
timing : 'linear' | 'easeOut' | 'easeIn' | () => number,
renderContent: (value: number) => React.Node,
formatter : () => {},
onProgress : () => {},
onFinish : () => {}
}
type State = {
value? : ?number,
displayValue? : ?number
}
export default class AnimateNumber extends React.Component<Props, State> {
static defaultProps = {
interval : 14,
timing : 'linear',
steps : 45,
value : 0,
formatter : (val) => val,
renderContent: (value: number) => (<Text>
{value}
</Text>),
onFinish : () => {}
};
static TimingFunctions = {
linear : (interval:number, progress:number):number => {
return interval
},
easeOut : (interval:number, progress:number):number => {
return interval * Math.sin(HALF_RAD*progress) * 5
},
easeIn : (interval:number, progress:number):number => {
return interval * Math.sin((HALF_RAD - HALF_RAD*progress)) * 5
},
};
/**
* Animation direction, true means positive, false means negative.
* @type {bool}
*/
direction : bool;
/**
* Start value of last animation.
* @type {number}
*/
startFrom : number;
/**
* End value of last animation.
* @type {number}
*/
endWith : number;
/**
* Mounted status of the component
* @type {boolean}
*/
mounted: boolean;
constructor(props:any) {
super(props);
// default values of state and non-state variables
this.state = {
value : this.props.initial ? this.props.initial : 0,
displayValue : 0
}
this.dirty = false;
this.startFrom = 0;
this.endWith = 0;
}
componentDidMount() {
this.startFrom = this.state.value
this.endWith = this.props.value
this.dirty = true
this.mounted = true;
this.startAnimate()
}
componentDidUpdate(prevProps) {
// check if start an animation
if(this.props.value !== prevProps.value) {
this.startFrom = prevProps.value;
this.endWith = this.props.value;
this.dirty = true;
this.startAnimate();
return
}
// Check if iterate animation frame
if(!this.dirty) {
this.props.onFinish();
return
}
if (this.direction === true) {
if(parseFloat(this.state.value) <= parseFloat(prevProps.value)) {
this.startAnimate();
}
}
else if(this.direction === false){
if (parseFloat(this.state.value) >= parseFloat(prevProps.value)) {
this.startAnimate();
}
}
}
componentWillUnmount() {
this.mounted = false;
clearTimeout(this.timer);
}
render() {
return this.props.renderContent(this.state.displayValue)
}
startAnimate() {
let progress = this.getAnimationProgress()
this.timer = setTimeout(() => {
if (this.mounted) {
let value = (this.endWith - this.startFrom)/this.props.steps
let sign = value >= 0 ? 1 : -1
if(this.props.countBy)
value = sign*Math.abs(this.props.countBy)
let total = parseFloat(this.state.value) + parseFloat(value)
this.direction = (value > 0)
// animation terminate conditions
if (((this.direction) ^ (total <= this.endWith)) === 1) {
this.dirty = false
total = this.endWith
}
if(this.props.onProgress)
this.props.onProgress(this.state.value, total)
this.setState({
value : total,
displayValue : this.props.formatter(total)
})
}
}, this.getTimingFunction(this.props.interval, progress))
}
getAnimationProgress():number {
return (this.state.value - this.startFrom) / (this.endWith - this.startFrom)
}
getTimingFunction(interval:number, progress:number) {
if(typeof this.props.timing === 'string') {
let fn = AnimateNumber.TimingFunctions[this.props.timing]
return fn(interval, progress)
} else if(typeof this.props.timing === 'function')
return this.props.timing(interval, progress)
else
return AnimateNumber.TimingFunctions['linear'](interval, progress)
}
}