Using React Refs and Animating between Modals — Part 2
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
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
Here’s an example of our final API for our
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:
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
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:
So there you can see that we take our starting
currentScreen from our props and push that into our queue array. We also then take the dimensions from the
screenDimensions prop that matches the current screen. Then we set the initial state for the component. Note that the
animating to false as we’re just showing the current screen to start with.
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
Here you can see that we check if the
currentScreen prop is changing. If so, we add the
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:
Written by Jean-Paul Gorman, Software Engineer at Moneyhub Enterprise