|  | @@ -0,0 +1,174 @@
 | 
	
		
			
			|  | 1 | +/**
 | 
	
		
			
			|  | 2 | + * @author wkh237
 | 
	
		
			
			|  | 3 | + * @version 0.1.1
 | 
	
		
			
			|  | 4 | + */
 | 
	
		
			
			|  | 5 | +
 | 
	
		
			
			|  | 6 | +// @flow
 | 
	
		
			
			|  | 7 | +
 | 
	
		
			
			|  | 8 | +import React, { Component } from 'react';
 | 
	
		
			
			|  | 9 | +import {
 | 
	
		
			
			|  | 10 | +  Text,
 | 
	
		
			
			|  | 11 | +  View
 | 
	
		
			
			|  | 12 | +} from 'react-native';
 | 
	
		
			
			|  | 13 | +import Timer from 'react-timer-mixin';
 | 
	
		
			
			|  | 14 | +
 | 
	
		
			
			|  | 15 | +const HALF_RAD = Math.PI/2
 | 
	
		
			
			|  | 16 | +
 | 
	
		
			
			|  | 17 | +export default class AnimateNumber extends Component {
 | 
	
		
			
			|  | 18 | +
 | 
	
		
			
			|  | 19 | +  props : {
 | 
	
		
			
			|  | 20 | +    countBy? : ?number,
 | 
	
		
			
			|  | 21 | +    interval? : ?number,
 | 
	
		
			
			|  | 22 | +    steps? : ?number,
 | 
	
		
			
			|  | 23 | +    value : number,
 | 
	
		
			
			|  | 24 | +    timing : 'linear' | 'easeOut' | 'easeIn' | () => number,
 | 
	
		
			
			|  | 25 | +    formatter : () => {},
 | 
	
		
			
			|  | 26 | +    onProgress : () => {},
 | 
	
		
			
			|  | 27 | +    onFinish : () => {}
 | 
	
		
			
			|  | 28 | +  };
 | 
	
		
			
			|  | 29 | +
 | 
	
		
			
			|  | 30 | +  static defaultProps = {
 | 
	
		
			
			|  | 31 | +    interval : 14,
 | 
	
		
			
			|  | 32 | +    timing : 'linear',
 | 
	
		
			
			|  | 33 | +    steps : 45,
 | 
	
		
			
			|  | 34 | +    value : 0,
 | 
	
		
			
			|  | 35 | +    formatter : (val) => val,
 | 
	
		
			
			|  | 36 | +    onFinish : () => {}
 | 
	
		
			
			|  | 37 | +  };
 | 
	
		
			
			|  | 38 | +
 | 
	
		
			
			|  | 39 | +  static TimingFunctions = {
 | 
	
		
			
			|  | 40 | +
 | 
	
		
			
			|  | 41 | +    linear : (interval:number, progress:number):number => {
 | 
	
		
			
			|  | 42 | +      return interval
 | 
	
		
			
			|  | 43 | +    },
 | 
	
		
			
			|  | 44 | +
 | 
	
		
			
			|  | 45 | +    easeOut : (interval:number, progress:number):number => {
 | 
	
		
			
			|  | 46 | +      return interval * Math.sin(HALF_RAD*progress) * 5
 | 
	
		
			
			|  | 47 | +    },
 | 
	
		
			
			|  | 48 | +
 | 
	
		
			
			|  | 49 | +    easeIn : (interval:number, progress:number):number => {
 | 
	
		
			
			|  | 50 | +      return interval * Math.sin((HALF_RAD - HALF_RAD*progress)) * 5
 | 
	
		
			
			|  | 51 | +    },
 | 
	
		
			
			|  | 52 | +
 | 
	
		
			
			|  | 53 | +  };
 | 
	
		
			
			|  | 54 | +
 | 
	
		
			
			|  | 55 | +  state : {
 | 
	
		
			
			|  | 56 | +    value? : ?number,
 | 
	
		
			
			|  | 57 | +    displayValue? : ?number
 | 
	
		
			
			|  | 58 | +  };
 | 
	
		
			
			|  | 59 | +
 | 
	
		
			
			|  | 60 | +  /**
 | 
	
		
			
			|  | 61 | +   * Animation direction, true means positive, false means negative.
 | 
	
		
			
			|  | 62 | +   * @type {bool}
 | 
	
		
			
			|  | 63 | +   */
 | 
	
		
			
			|  | 64 | +  direction : bool;
 | 
	
		
			
			|  | 65 | +  /**
 | 
	
		
			
			|  | 66 | +   * Start value of last animation.
 | 
	
		
			
			|  | 67 | +   * @type {number}
 | 
	
		
			
			|  | 68 | +   */
 | 
	
		
			
			|  | 69 | +  startFrom : number;
 | 
	
		
			
			|  | 70 | +  /**
 | 
	
		
			
			|  | 71 | +  * End value of last animation.
 | 
	
		
			
			|  | 72 | +  * @type {number}
 | 
	
		
			
			|  | 73 | +   */
 | 
	
		
			
			|  | 74 | +  endWith : number;
 | 
	
		
			
			|  | 75 | +
 | 
	
		
			
			|  | 76 | +  constructor(props:any) {
 | 
	
		
			
			|  | 77 | +    super(props);
 | 
	
		
			
			|  | 78 | +    // default values of state and non-state variables
 | 
	
		
			
			|  | 79 | +    this.state = {
 | 
	
		
			
			|  | 80 | +      value : 0,
 | 
	
		
			
			|  | 81 | +      displayValue : 0
 | 
	
		
			
			|  | 82 | +    }
 | 
	
		
			
			|  | 83 | +    this.dirty = false;
 | 
	
		
			
			|  | 84 | +    this.startFrom = 0;
 | 
	
		
			
			|  | 85 | +    this.endWith = 0;
 | 
	
		
			
			|  | 86 | +  }
 | 
	
		
			
			|  | 87 | +
 | 
	
		
			
			|  | 88 | +  componentDidMount() {
 | 
	
		
			
			|  | 89 | +    this.startFrom = this.state.value
 | 
	
		
			
			|  | 90 | +    this.endWith = this.props.value
 | 
	
		
			
			|  | 91 | +    this.dirty = true
 | 
	
		
			
			|  | 92 | +    this.startAnimate()
 | 
	
		
			
			|  | 93 | +  }
 | 
	
		
			
			|  | 94 | +
 | 
	
		
			
			|  | 95 | +  componentWillUpdate(nextProps, nextState) {
 | 
	
		
			
			|  | 96 | +
 | 
	
		
			
			|  | 97 | +    // check if start an animation
 | 
	
		
			
			|  | 98 | +    if(this.props.value !== nextProps.value) {
 | 
	
		
			
			|  | 99 | +      this.startFrom = this.props.value
 | 
	
		
			
			|  | 100 | +      this.endWith = nextProps.value
 | 
	
		
			
			|  | 101 | +      this.dirty = true
 | 
	
		
			
			|  | 102 | +      this.startAnimate()
 | 
	
		
			
			|  | 103 | +      return
 | 
	
		
			
			|  | 104 | +    }
 | 
	
		
			
			|  | 105 | +    // Check if iterate animation frame
 | 
	
		
			
			|  | 106 | +    if(!this.dirty) {
 | 
	
		
			
			|  | 107 | +      return
 | 
	
		
			
			|  | 108 | +    }
 | 
	
		
			
			|  | 109 | +    if (this.direction === true) {
 | 
	
		
			
			|  | 110 | +      if(parseFloat(this.state.value) <= parseFloat(this.props.value)) {
 | 
	
		
			
			|  | 111 | +        this.startAnimate();
 | 
	
		
			
			|  | 112 | +      }
 | 
	
		
			
			|  | 113 | +    }
 | 
	
		
			
			|  | 114 | +    else if(this.direction === false){
 | 
	
		
			
			|  | 115 | +      if (parseFloat(this.state.value) >= parseFloat(this.props.value)) {
 | 
	
		
			
			|  | 116 | +        this.startAnimate();
 | 
	
		
			
			|  | 117 | +      }
 | 
	
		
			
			|  | 118 | +    }
 | 
	
		
			
			|  | 119 | +
 | 
	
		
			
			|  | 120 | +  }
 | 
	
		
			
			|  | 121 | +
 | 
	
		
			
			|  | 122 | +  render() {
 | 
	
		
			
			|  | 123 | +    return (
 | 
	
		
			
			|  | 124 | +      <Text {...this.props}>
 | 
	
		
			
			|  | 125 | +        {this.state.displayValue}
 | 
	
		
			
			|  | 126 | +      </Text>)
 | 
	
		
			
			|  | 127 | +  }
 | 
	
		
			
			|  | 128 | +
 | 
	
		
			
			|  | 129 | +  startAnimate() {
 | 
	
		
			
			|  | 130 | +
 | 
	
		
			
			|  | 131 | +    let progress = this.getAnimationProgress()
 | 
	
		
			
			|  | 132 | +
 | 
	
		
			
			|  | 133 | +    Timer.setTimeout(() => {
 | 
	
		
			
			|  | 134 | +
 | 
	
		
			
			|  | 135 | +      let value = (this.endWith - this.startFrom)/this.props.steps
 | 
	
		
			
			|  | 136 | +      if(this.props.countBy)
 | 
	
		
			
			|  | 137 | +        value = Math.sign(value)*Math.abs(this.props.countBy)
 | 
	
		
			
			|  | 138 | +      let total = parseFloat(this.state.value) + parseFloat(value)
 | 
	
		
			
			|  | 139 | +
 | 
	
		
			
			|  | 140 | +      this.direction = (value > 0)
 | 
	
		
			
			|  | 141 | +      // animation terminate conditions
 | 
	
		
			
			|  | 142 | +      if (((this.direction) ^ (total <= this.endWith)) === 1) {
 | 
	
		
			
			|  | 143 | +        this.dirty = false
 | 
	
		
			
			|  | 144 | +        total = this.endWith
 | 
	
		
			
			|  | 145 | +        this.props.onFinish(total, this.props.formatter(total))
 | 
	
		
			
			|  | 146 | +      }
 | 
	
		
			
			|  | 147 | +
 | 
	
		
			
			|  | 148 | +      if(this.props.onProgress)
 | 
	
		
			
			|  | 149 | +        this.props.onProgress(this.state.value, total)
 | 
	
		
			
			|  | 150 | +
 | 
	
		
			
			|  | 151 | +      this.setState({
 | 
	
		
			
			|  | 152 | +        value : total,
 | 
	
		
			
			|  | 153 | +        displayValue : this.props.formatter(total)
 | 
	
		
			
			|  | 154 | +      })
 | 
	
		
			
			|  | 155 | +
 | 
	
		
			
			|  | 156 | +    }, this.getTimingFunction(this.props.interval, progress))
 | 
	
		
			
			|  | 157 | +
 | 
	
		
			
			|  | 158 | +  }
 | 
	
		
			
			|  | 159 | +
 | 
	
		
			
			|  | 160 | +  getAnimationProgress():number {
 | 
	
		
			
			|  | 161 | +    return (this.state.value - this.startFrom) / (this.endWith - this.startFrom)
 | 
	
		
			
			|  | 162 | +  }
 | 
	
		
			
			|  | 163 | +
 | 
	
		
			
			|  | 164 | +  getTimingFunction(interval:number, progress:number) {
 | 
	
		
			
			|  | 165 | +    if(typeof this.props.timing === 'string') {
 | 
	
		
			
			|  | 166 | +      let fn = AnimateNumber.TimingFunctions[this.props.timing]
 | 
	
		
			
			|  | 167 | +      return fn(interval, progress)
 | 
	
		
			
			|  | 168 | +    } else if(typeof this.props.timing === 'function')
 | 
	
		
			
			|  | 169 | +      return this.props.timing(interval, progress)
 | 
	
		
			
			|  | 170 | +    else
 | 
	
		
			
			|  | 171 | +      return AnimateNumber.TimingFunctions['linear'](interval, progress)
 | 
	
		
			
			|  | 172 | +  }
 | 
	
		
			
			|  | 173 | +
 | 
	
		
			
			|  | 174 | +}
 |