Adding fly-over animation from list to details

After I made list resorting animate I got excited about how much ember-animated can do with how little code. I was hooked and signed up to EmberMap's animation workshop at EmberConf. Having finished the workshop, my enthusiasm rose to new levels.

I decided to do another animation that you see in lots of apps. When an item is clicked in a list to go to its details page, it "flies over" to its new position on the details page (I wrote about how that's done with liquid-fire a long time ago on the AirPair blog).

Mark the pieces that animate

The first step is to tell ember-animated which value needs to be watched. If that value changes, the animation will be triggered. Here, the two components that render the player's image that I wanted to animate are player-card and detailed-player-card.

{{!-- app/templates/components/player-card.hbs --}}
<AnimatedContainer class="flex-1 w-full h-full">
  {{#animated-value @player}}
    <img
      src={{@player.imageURL}}
      alt={{@player.name}}
      title={{@player.name}}
    >
  {{/animated-value}}
</AnimatedContainer>
{{!-- app/templates/components/detailed-player-card.hbs --}}
<AnimatedContainer>
  {{#animated-value @player use=this.transition duration=500}}
    <img
      src={{@player.imageURL}}
      alt={{@player.name}}
      title={{@player.name}}
    >
  {{/animated-value}}
</AnimatedContainer>

The first parameter to animated-value establishes identity. That's how ember-animated knows it should animate between animated-value instances whose first params have the same value.

The use parameter defines how the animation should be carried out, so let's take a look at that:

// app/components/detailed-player-card.js

// (...)
import move from 'ember-animated/motions/move';
import scale from 'ember-animated/motions/scale';

export default class DetailedPlayerCard extends Component {
// (...)
  * transition(context) {
    let { receivedSprites, sentSprites } = context;
    for (let sprite of receivedSprites) {
      move(sprite);
      scale(sprite);
    }
    for (let sprite of sentSprites) {
      scale(sprite);
      move(sprite);
    }
  }
}  

context has three main properties on it that helps us, developers, implement the way we want objects to animate. sentSprites are the objects that are present in the sender but not the receiver. receivedSprites are the inverse – sprites appearing in the destination. The third one, keptSprites , is not present in this example. It's the array of objects that are present both at the sender and the receiver. (The analogy to D3 joins can help comprehension.)

The transition method is defined in the detailed-player-card component so when we go from the list to the details page (from player-card to detailed-player-card) we'll have a receivedSprite. When we go the other way, from details to list, we'll have a sentSprite.

To be able to animate the image (wrapped in animated-value) back to the list even though its containing container (the detailed-player-card component) have been destroyed, we also need to render an animated-orphans component in a template that doesn't get destroyed between the transitions.  This way, the animated-orphans can adopt the parentless child.

I chose the application template:

{{!-- app/templates/application.hbs --}}
{{animated-orphans}}
{{!-- (...) --}}

With that I had a working fly-over animation:

The astute observer among you might notice two things:

  1. I fixed the jank from last time that was produced when the list is re-ordered. It wasn't a spectacular fix, I need to amend some flex-related CSS.
  2. When the image travels back from the details to the list page, it goes beyond the cards. I think that's not very bad but not ideal either and I might attempt a fix later.

If you have any questions, comments or observations, please write me at balint@balinterdi.com.