The original address: https://github.com/SmallStoneSK/Blog/issues/1

1. Introduction

Recently, I was assigned the task of making an opening animation. Although RN provides Animated custom animations, there are a lot of elements in this animation, and the interaction is annoying… While completing the task, I found that many steps were actually repeated, so I encapsulated a small piece to record and share it with everyone.

2. Experiment

Analysis: although this animation needs a lot of steps, but the animation of each step is broken down into step1, step2, step3, step4… Is it possible to be reasonable? Well, Animated.Value() creates the Value and then Animated. Timing should be fine.

With that in mind, the backhand is to create a demo.js and make a balloon that floats up.

export class Demo1 extends PureComponent {

  constructor(props) {
    super(props);
  }

  componentWillMount() {
    this._initAnimation();
  }

  componentDidMount() {
    this._playAnimation();
  }

  _initAnimation() {
    this.topAnimatedValue = new Animated.Value(400);
    this.balloonStyle = {
      position: 'absolute'.left: 137.5.top: this.topAnimatedValue.interpolate({
        inputRange: [- 999999..999999].outputRange: [- 999999..999999]})}; } _playAnimation() { Animated.timing(this.topAnimatedValue, {
      toValue: 200.duration: 1500
    }).start();
  }

  render() {
    return (
      <View style={styles.demoContainer}>
        <Animated.Image
          style={[styles.balloonImage, this.balloonStyle]}
          source={require('././pic/demo1/balloon.png')} / >
      </View>); }}Copy the code

Of course, this is the simplest basic animation… What if we had a balloon here that preferably started at a point at the bottom and had a tap-in effect, and then floated up? So the code looks like this:

export class Demo1 extends PureComponent {... _interpolateAnimation(animatedValue, inputRange, outputRange) {return animatedValue.interpolate({inputRange, outputRange});
  }

  _initAnimation() {

    this.opacityAnimatedValue = new Animated.Value(0);
    this.scaleAnimatedValue = new Animated.Value(0);
    this.topAnimatedValue = new Animated.Value(400);

    this.balloonStyle = {
      position: 'absolute'.left: 137.5.opacity: this._interpolateAnimation(this.opacityAnimatedValue, [0.1], [0.1]),
      top: this._interpolateAnimation(this.topAnimatedValue, [- 999999..999999], [- 999999..999999]),
      transform: [{scale: this._interpolateAnimation(this.scaleAnimatedValue, [0.1], [0.1]])}}; } _playAnimation() { Animated.sequence([this.step1(),
      this.step2()
    ]).start();
  }

  step1() {
    return Animated.parallel([
      Animated.timing(this.opacityAnimatedValue, {
        toValue: 1.duration: 500
      }),
      Animated.timing(this.scaleAnimatedValue, {
        toValue: 1.duration: 500})]); } step2() {return Animated.timing(this.topAnimatedValue, {
      toValue: 200.duration: 1500}); }... }Copy the code

In a word: when the animation is connected, I still struggle a little. Because Animated has a lot of options, sequence and parallel are used for sequential and parallel animation, respectively. In addition, the Animtaion start method supports passing in a callback that will be triggered at the end of the current animation. So we can also write:

  _playAnimation() { this.step1(() => this.step2()); // Difference 1: Step2 as step1 animation after the callback incoming} step1 (the callback) {Animated. The parallel ([Animated. Timing (enclosing opacityAnimatedValue, {toValue: 1, duration: 500 }), Animated.timing(this.scaleAnimatedValue, { toValue: 1, duration: 500 }) ]).start(() => { callback && callback(); // difference 2: call the incoming callback}); }step2() {
    Animated.timing(this.topAnimatedValue, {
      toValue: 200,
      duration: 1500
    }).start();
  }
Copy the code

Although it can also achieve the effect, but still feel that this way is not very comfortable, so abandoned…

So far, we have done gradient, zoom in, and pan on the balloon. But what about five balloons and all the other elements? This is only a balloon we have used opacityAnimatedValue, scaleAnimatedValue, topAnimatedValue three variables to control, more animation elements that straight gg, need not work…

3. Implement the upgrade

Honestly, how can it be so much like making a powerpoint presentation…

“The screen is like a powerpoint background; Each balloon is an element of the PPT; You can drag the mouse to place each balloon, I can use absolute positioning to determine the position of each balloon; As far as animation is concerned, the demo proved that it’s not too difficult to implement, just control transparency, XY coordinates, zoom.”

When I thought of this, I could not help but feel a burst of joy in my heart. Ha-ha, there is a way to encapsulate these elements in PPT into a common component, and then provide some common animation methods. All that is left is to call these animation methods to assemble more complex animations. Create a new slide: “Appear, leap, fade, float in, shutter, checkerboard…” Looking at this dazzling variety of animation, I thought: well, I still start from the simplest…

First, we can divide animation into two types: disposable animation and looping animation. Secondly, as an element, it can be used as animation properties mainly including: opacity, x, y, scale, Angle, etc. (only the 2d plane is considered here, but it can also be extended to 3d). Finally, basic animations can be broken down into these behaviors: appear/disappear, move, zoom, rotate.

3.1 One-time Animation

With that in mind, the backhand is to create a new file as follows:

// Comstants.js
export const INF = 999999999;

// Helper.js
export const Helper = {
  sleep(millSeconds) {
    return new Promise(resolve= > {
      setTimeout((a)= > resolve(), millSeconds);
    });
  },
  animateInterpolate(animatedValue, inputRange, outputRange) {
    if(animatedValue && animatedValue.interpolate) {
      returnanimatedValue.interpolate({inputRange, outputRange}); }}};// AnimatedContainer.js
import {INF} from "./Constants";
import {Helper} from "./Helper";

export class AnimatedContainer extends PureComponent {

  constructor(props) {
    super(props);
  }

  componentWillMount() {
    this._initAnimationConfig();
  }

  _initAnimationConfig() {

    const {initialConfig} = this.props;
    const {opacity = 1, scale = 1, x = 0, y = 0, rotate = 0} = initialConfig;

    // create animated values: opacity, scale, x, y, rotate
    this.opacityAnimatedValue = new Animated.Value(opacity);
    this.scaleAnimatedValue = new Animated.Value(scale);
    this.rotateAnimatedValue = new Animated.Value(rotate);
    this.xAnimatedValue = new Animated.Value(x);
    this.yAnimatedValue = new Animated.Value(y);

    this.style = {
      position: 'absolute'.left: this.xAnimatedValue,
      top: this.yAnimatedValue,
      opacity: Helper.animateInterpolate(this.opacityAnimatedValue, [0.1], [0.1]),
      transform: [{scale: this.scaleAnimatedValue},
        {rotate: Helper.animateInterpolate(this.rotateAnimatedValue, [-INF, INF], [` -${INF}rad`.`${INF}rad`]])}}; } show() {} hide() {} scaleTo() {} rotateTo() {} moveTo() {} render() {return (
      <Animated.View style={[this.style, this.props.style]} >
        {this.props.children}
      </Animated.View>
    );
  }
}

AnimatedContainer.defaultProps = {
  initialConfig: {
    opacity: 1,
    scale: 1,
    x: 0,
    y: 0,
    rotate: 0
  }
};
Copy the code

The skeleton of the first step was built, and it was incredibly simple… Here’s how to implement each animation: show/hide.

show(config = {opacity: 1, duration: 500}) {
  Animated.timing(this.opacityAnimatedValue, {
    toValue: config.opacity,
    duration: config.duration
  }).start();
}

hide(config = {opacity: 0, duration: 500}) {
  Animated.timing(this.opacityAnimatedValue, {
    toValue: config.opacity,
    duration: config.duration
  }).start();
}
Copy the code

I tried it. It was beautiful

But! On second thought, there is a very serious problem, how to deal with the animation connection here? How to do a show and then hide animation 1s later? Looks like we’re back to our first thought. But this time, I used Promise to solve the problem. So the code looks like this again:

sleep(millSeconds) {
  return new Promise(resolve= > setTimeout((a)= > resolve(), millSeconds));
}

show(config = {opacity: 1.duration: 500{})return new Promise(resolve= > {
    Animated.timing(this.opacityAnimatedValue, {
      toValue: config.opacity,
      duration: config.duration
    }).start((a)= > resolve());
  });
}

hide(config = {opacity: 0.duration: 500{})return new Promise(resolve= > {
    Animated.timing(this.opacityAnimatedValue, {
      toValue: config.opacity,
      duration: config.duration
    }).start((a)= > resolve());
  });
}
Copy the code

Now let’s look at the animation again. This is all we need to do:

playAnimation() {
  this.animationRef
    .show()                                 / / to appear first
    .sleep(1000)                            / / wait for 1 s
    .then((a)= > this.animationRef.hide());  / / disappear
}
Copy the code

You can even encapsulate another wave of the createPromise process:

_createAnimation(animationConfig = []) {
  const len = animationConfig.length;
  if (len === 1) {
    const {animatedValue, toValue, duration} = animationConfig[0];
    return Animated.timing(animatedValue, {toValue, duration});
  } else if (len >= 2) {
    return Animated.parallel(animationConfig.map(config= > {
      return this._createAnimation([config]);
    }));
  }
}

_createAnimationPromise(animationConfig = []) {
  return new Promise(resolve= > {
    const len = animationConfig.length;
    if(len <= 0) {
      resolve();
    } else {
      this._createAnimation(animationConfig).start((a)= >resolve()); }}); } opacityTo(config = {opacity: . 5.duration: 500{})return this._createAnimationPromise([{
    toValue: config.opacity,
    duration: config.duration,
    animatedValue: this.opacityAnimatedValue
  }]);
}

show(config = {opacity: 1.duration: 500{})this.opacityTo(config);
}

hide(config = {opacity: 0.duration: 500{})this.opacityTo(config);
}
Copy the code

We then add the other basic animation implementations (scale, rotate, move) :

scaleTo(config = {scale: 1.duration: 1000{})return this._createAnimationPromise([{
    toValue: config.scale,
    duration: config.duration,
    animatedValue: this.scaleAnimatedValue
  }]);
}

rotateTo(config = {rotate: 0.duration: 500{})return this._createAnimationPromise([{
    toValue: config.rotate,
    duration: config.duration,
    animatedValue: this.rotateAnimatedValue
  }]);
}

moveTo(config = {x: 0.y: 0.duration: 1000{})return this._createAnimationPromise([{
    toValue: config.x,
    duration: config.duration,
    animatedValue: this.xAnimatedValue
  }, {
    toValue: config.y,
    duration: config.duration,
    animatedValue: this.yAnimatedValue
  }]);
}
Copy the code

3.2 Looping Animation

Now that the one-time animation problem is solved, let’s look at looping animations. As a rule of thumb, a looping animation would look something like this:

roll() {

  this.rollAnimation = Animated.timing(this.rotateAnimatedValue, {
  	toValue: Math.PI * 2.duration: 2000
  });

  this.rollAnimation.start((a)= > {
  	this.rotateAnimatedValue.setValue(0);
  	this.roll();
  });
}

play() {
  this.roll();
}

stop() {
  this.rollAnimation.stop();
}
Copy the code

That’s right, passing a callback to the start of an animation that recursively calls the function that plays the animation itself. So how does that correspond to the component we want to encapsulate?

In order to maintain consistency with the disposable animation API, we can add the following functions to the animatedContainer:

export class AnimatedContainer extends PureComponent {... constructor(props) {super(props);
    this.cyclicAnimations = {};
  }

  _createCyclicAnimation(name, animations) {
    this.cyclicAnimations[name] = Animated.sequence(animations);
  }
  
  _createCyclicAnimationPromise(name, animations) {
    return new Promise(resolve= > {
      this._createCyclicAnimation(name, animations);
      this._playCyclicAnimation(name);
      resolve();
    });
  }  

  _playCyclicAnimation(name) {
    const animation = this.cyclicAnimations[name];
    animation.start((a)= > {
      animation.reset();
      this._playCyclicAnimation(name);
    });
  }

  _stopCyclicAnimation(name) {
    this.cyclicAnimations[name].stop(); }... }Copy the code

Among them, _createCyclicAnimation, _createCyclicAnimationPromise is corresponding to a one-time animation API. The difference, however, is that the parameters passed in have changed a lot: animationConfg -> (name, animations)

  1. Name is an identifier that cannot be duplicated between loop animations. Both _playCyclicAnimation and _stopCyclicAnimation are called by name to match the animation.
  2. Animations are a group of animations in which each animation is generated by calling _createAnimation. Because looping animations can be made up of a set of one-off animations, Animated. Sequence is called directly in _createCyclicAnimation, and the looping is implemented by recursive calls in _playCyclicAnimation.

At this point, the looping animation is almost wrapped. To encapsulate two looping animations roll and blink try:

blink(config = {period: 2000{})return this._createCyclicAnimationPromise('blink'[this._createAnimation([{
      toValue: 1.duration: config.period / 2.animatedValue: this.opacityAnimatedValue
    }]),
    this._createAnimation([{
      toValue: 0.duration: config.period / 2.animatedValue: this.opacityAnimatedValue
    }])
  ]);
}

stopBlink() {
  this._stopCyclicAnimation('blink');
}

roll(config = {period: 1000{})return this._createCyclicAnimationPromise('roll'[this._createAnimation([{
      toValue: Math.PI * 2.duration: config.period,
      animatedValue: this.rotateAnimatedValue
    }])
  ]);
}

stopRoll() {
  this._stopCyclicAnimation('roll');
}
Copy the code

4. The real

After much work, the AnimatedContainer was finally packaged. First find a material to practice hands ~ but, find what? “Ding”, I saw a mobile phone to dig up a reminder to light up. Hey hey, you, dig the wealth of the check-in page is really suitable (no advertising…) The renderings are as follows:

I won’t post the render code for the render element, but let’s look at the code for the animation:

startOpeningAnimation() {

  // Check in (one-time animation)
  Promise
    .all([
      this._header.show(),
      this._header.scaleTo({scale: 1}),
      this._header.rotateTo({rotate: Math.PI * 2})
    ])
    .then((a)= > this._header.sleep(100))
    .then((a)= > this._header.moveTo({x: 64.y: 150}))
    .then((a)= > Promise.all([
      this._tips.show(),
      this._ladder.sleep(150).then((a)= > this._ladder.show())
    ]))
    .then((a)= > Promise.all([
      this._today.show(),
      this._today.moveTo({x: 105.y: 365})));// Stars twinkle (loop animation)
  this._stars.forEach(item= > item
    .sleep(Math.random() * 2000)
    .then((a)= > item.blink({period: 1000}))); }Copy the code

Just look at the code, is it already imagined the entire animation ~ fat bowel at a glance, really is delicious.

5. Follow up

  1. To be fair, the animations that the AnimatedContainer can create are still a little thin and contain only the most basic operations. However, this also shows that there is a great extension of space, according to _createCyclicAnimationPromise and _createAnimationPromise these two functions, free to encapsulate we want various complex animation effects. The caller simply controls the animation order through the Promise’s all and then methods. Personally, I even use jQuery a little bit…

  2. In addition, there is another question: since these elements are absolutely positioned, what about the x and y coordinate values of these elements? That feels ok with visual annotations. But once the number of elements goes up, it’s a little tricky to use… So, if there was a tool that could drag and drop elements and get their coordinates in real time like powerpoint, it would be wamei…

Same rules, this article code address: github.com/SmallStoneS…