Animating between screens
Following on from my previous article which dealt with how we would measure a set of screens that we want to animate between, let’s now continue on and deal with the animation.
So firstly it will help if we decide on the API for the component that will deal with the transitions between our screens. For the sake of argument, let’s call it ScreenTransitions. We know that we want it to accept an object that will consist of named screens and then dimensions for each, e.g.:
In order for our ‘ScreenTransitions’ component to transition from screen A to B to C, it will need to know which screen it is currently on — let’s call this currentScreen.
Next we just need to give it the screens themselves that were used to derive the screen dimensions. Here is the key reason for designing the MeasureScreenDimensions component so that it passes its props onto its children. You can see now that the screen’s props, created by MeasureScreenDimensions would then be passed to our ScreenTransition component. This is a good example of using React components in Composition, allowing the parent component, in this case MeasureScreenDimensions to inform and pass props to its children, in this case ScreenTransition.
Here’s an example of our final API for our ScreenTransition component:
So now that we have an API to work to, next we need to put together our actual component.
First though, we need to decide on how our animation will work. We know that we have to animate from one screen of content to another. We also know that our screens can consist of anything — text, images, etc. So the simplest way to transition, would first be to fade out our current screens content, then morph our screen size into the next screen, finally fading into our next screen. Here’s a working example: https://codepen.io/jeanpaulgorman/pen/RgbOXR?editors=0010
Now remember at the start of my previous article, we had a list of things that we wanted our animation to achieve, let’s review them again:
- Animation: We want to only animate the css attributes that don’t cause expensive re-draws of our layout.
- Delta: We need to work out the change in sizes between each screen.
- Orchestration: We need to martial the animation from one screen to another.
As with the previous example, let’s start with our final component and then break down each section:
This is a much more complex component than our previous example, so let’s go through it and see how we address each of the animation problems that we want to solve, as listed above.
We know that we want to animate cheaply, so let’s set up the areas that we want to animate first.
We’ll have two sections that we’ll want to animate. The current screen, which will simply fade-in and out and a ContentWrapper, which will fade in on top of the current screen, resize and then fade out again to reveal our new current screen.
So we know that we want our contentWrapper to animate the transform property after a given delay. Let’s look at how these styles are applied in the render method of our component:
The render method looks a little complex at first but really, its two main tasks are to; Firstly render our DOM elements in the centre of the screen. Secondly we apply a CSS transition to show or hide our currentScreen as well as our animated view or contentWrapper.
At the top of the render method you can see the following:
You can see that we take the width, height and scale (this is the change in size of our contentWrapper) from the component’s state. We then apply that to our contentWrapper. The key point here though, is that the width and height of the currently shown screen that will be used to measure the change in scale between it and the next screen to be shown. Once we know the scale value it can be used to drive our animation.
We then show and hide either the contentWrapper if we’re loading in the next screen i.e. animating, or the currentScreen if the animation is complete.
The other key point to note is that the currentScreen is taken by plucking the first item from the this.queue array and then grabbing the corresponding screen from the screens prop.
So the next question to ask is, what is this.queue and how is it being populated?
Let’s start by thinking of our animation again. We start with some screen with a width and a height, we then hide that screen and show our contentWrapper which then scales to fit our next screen. So in order for our animation to work we need to know the dimensions of our current screen and the next screen that we will render. From those two entities we can calculate the change in the dimensions and work out the differences between the two screens on both the X and Y axis, in other words our delta. So our queue is just a list of screens that we want to render between.
Let’s start by looking at the component constructor:
Okay, so what triggers an animation then?
Remember that our component receives a prop of currentScreen, it’s this property that tells the ScreenTransition component that it’s getting a new currentScreen, and it’s then up to the ScreenTransition to transition between the previous screen and the next.
So the obvious place to kick this off is in the componentWillReceiveProps method:
Here you can see that we check if the currentScreen prop is changing. If so, we add the currentScreen from nextProps onto the queue and then call the startTimer method to begin our animation.
This method first checks that we’re not in the middle of an animation. It then sets animating to true, called the this.animateBetweenScreens method, which performs the animation calculations and then calls setTimeout. This registers a function which will tidy up our state, ready for the next animation between screens, and then start the next animation.
This last point is key as it will chain a series of animations between screen together.
Good, so let’s look at how the change in scale between each screen is calculated:
Firstly we check to see that we actually have another screen in our queue to animate to. We then retrieve our list of screen dimensions and assign a reference to our currentScreen and the nextScreen. We then remove the current screen from the start of the queue as we won’t need it anymore.
Calculating the difference in scale between our two screens is surprisingly straightforward:
We then simply need to apply this new scale to our state, which will cause our screens to re-render and our animation to begin:
Now remember the startTimer method from earlier. Let’s go back and look at those tidy-up methods that we registered to be called once the animation has completed.
As our animation is now complete, we need to update our state so that the our new current screen’s dimensions are the ones that are to be used when calculating the scale for the next screen, whenever that is added to our queue.
We simply take the screen that we just animated to, which is now at the start of the queue and update our state with its dimensions and crucially we omit the scale argument, when calling the setDimensions method, as this now changes the scale back to 1. Finally we set the animating state to false.
Next we trigger a new call to the startTimer method, if we still have any screens to animate between, making sure to wait for any current animations to complete first.
Here’s a final working DEMO for you to try out: https://codepen.io/jeanpaulgorman/pen/RgbOXR
Written by Jean-Paul Gorman, Software Engineer at Moneyhub Enterprise