Achievement Animation

Creating an Achievement Unlocked Animation with Angular Animations in Ionic



·

I’ve written a few articles in the past that covered using the Angular Animations API in an Ionic application. You can find some of those articles below:

Most of the tutorials that I have created so far focus on reasonably simple animations, but the Angular Animations API is powerful and can handle much more complicated animations.

In this tutorial, we will be focusing on implementing an animation sequence to create an “achievement unlocked” overlay in an Ionic application. The end result will look like this:

Achievement Animation in Ionic

Unlike previous tutorials, this animation will involve animating child elements of the parent animation. This has some implications for the code we write.

We are going to focus specifically on the animation in this tutorial. We will just be adding everything to the Home page of the application rather than creating any custom components/services.

I think it would be interesting to develop a more fully fleshed out achievements/gamification system that could be dropped into an application, though. If this is something you would like to see me take further, let me know in the comments.

Before We Get Started

Last updated for Ionic 4, beta.15

This is an advanced tutorial that assumes you already have a decent working knowledge of the Ionic framework. If you require more introductory level content on Ionic I would recommend checking out my book or the Ionic tutorials on my website.

This tutorial will also require some level of familiarity with the Angular Animations API.

Set Up the Angular Animations API

Before we jump into the code, we will need to install the Angular Animations API in our Ionic application. To do that, you will just need to install the following package:

npm install @angular/animations --save

and you will also need to add the BrowserAnimationsModule to your app.module.ts file:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, BrowserAnimationsModule, IonicModule.forRoot(), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

The Template

Before we work on the animation, we are going to set up the basic template both for our home page and the overlay that we will be animating.

Modify src/app/home/home.page.html to reflect the following:

<ion-header>
  <ion-toolbar color="danger">
    <ion-title>
      Achievements
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>

  <div *ngIf="displayAchievement" class="achievement-container">

      <div class="medal">
          <img src="assets/medal.png" />
      </div>

      <div class="message">
        <h5>Tutorial Completed</h5>
      </div>

  </div>

  <p>
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent lacus neque, imperdiet ac maximus at, sagittis vel turpis. Nullam ac enim vel ante tincidunt iaculis. Suspendisse ultrices luctus orci, a bibendum nisi consectetur consequat. Nam rhoncus nec dolor pulvinar porta. Aliquam erat volutpat. Nunc pretium mauris ipsum, quis pulvinar tellus tincidunt sit amet. Integer auctor ultrices rutrum. Suspendisse faucibus, urna in consectetur rutrum, orci turpis vestibulum nibh, eu fermentum nisi elit dignissim libero. Donec vestibulum, diam aliquet aliquet dapibus, quam nisi mattis neque, nec elementum mauris justo at tortor. Sed tempus sed nisi id tincidunt. In enim nisi, consectetur eget euismod semper, gravida ac ligula.
  </p>

  <ion-button (click)="test()" color="light" fill="outline">Test Achievement</ion-button>

</ion-content>

Aside from the basic layout, we have added a <div> that contains our achievement overlay and its child elements. Eventually, we will be animating this. I am using a badge from Kenney’s assets as the achievement icon, but you can use whatever you like. We have also added a button to trigger the animation.

We will need to define the corresponding event binding for that button, along with the displayAchievement class member:

Modify src/app/home/home.page.ts to reflect the following:

import { Component } from '@angular/core';
import { ToastController } from '@ionic/angular';
import { trigger, style, animate, transition, group, query, animateChild } from '@angular/animations';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  public displayAchievement: boolean = false;

  constructor(private toastCtrl: ToastController){

  }

  test(){

    this.toastCtrl.create({
      message: 'Achievement unlocked!',
      duration: 2000
    }).then((toast) => {
      toast.present();
    });

    this.displayAchievement = true;

    setTimeout(() => {
      this.displayAchievement = false;
    }, 3000);

  }

}

Our test method handles toggling the displayAchievement class member on an off. We also trigger a toast to display the “Achievement unlocked!” message at the bottom of the screen.

We will also need to add a few styles to our overlay:

Modify src/app/home/home.page.scss to reflect the following:

.achievement-container {
    z-index: 100;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.8);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    position: absolute;
    top: 0;
    left: 0;
}

h5 {
    color: #fff;
    text-transform: uppercase;
    font-weight: lighter;
    font-size: 2em;
    text-shadow: -1px 2px 15px rgba(0, 0, 0, 0.7);
}

Container Animation

With the basic layout set up, let’s focus on creating the animations. We will start by just animating the container itself. This is going to be a simple opacity animation that transitions from 0 opacity to 1 opacity.

Add the following animation to src/app/home/home.page.ts:

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
  animations: [

    trigger('container', [
      transition(':enter', [
          style({opacity: '0'}),
          group([
            animate('500ms ease-out', style({opacity: '1'}))
          ])

      ]),
      transition(':leave', [
          group([
            animate('500ms ease-out', style({opacity: '0'}))
          ])
      ])
    ])

  ]
})

We are defing transitions on both :enter and :leave (which is the equivalent of void => * and * => void respectively, it is just easier to write).

For the :enter animation we start with an opacity of 0 and then we animate to an opacity of 1 over 500ms with an ease-out easing. The :leave animation is mostly the same, just in reverse.

Notice that we are using an animation group here. This isn’t required for a single animation, but eventually, we will be triggering the animations for our child elements within those groups.

We will need to add the container trigger to the container in our template.

Add the @container trigger to the container in home.page.html:

<div *ngIf="displayAchievement" class="achievement-container" @container>

If you were to run the animation now, you would see the achievement overlay fade in and then fade back out again. It doesn’t actually look too bad with just this, but we are going to take it a step further by animating the badge and text elements as well.

Child Element Animations

We are animating the element that contains both of the other elements we also want to animate. There is a bit of a trick to triggering these animations as they won’t run by default.

We are going to add triggers for both the badge and the message, but we will also need to add them to the animation group and trigger their animations manually.

Modify the animations in home.page.ts to reflect the following:

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
  animations: [

    trigger('container', [
      transition(':enter', [
          style({opacity: '0'}),
          group([
            animate('500ms ease-out', style({opacity: '1'})),
            query('@badge, @message', [
              animateChild()
            ])
          ])

      ]),
      transition(':leave', [
          group([
            animate('500ms ease-out', style({opacity: '0'})),
            query('@badge, @message', [
              animateChild()
            ])
          ])
      ])
    ]),

    trigger('badge', [
        transition(':enter', [
            style({transform: 'translateY(400%)'}),
            animate('500ms ease-out', style({transform: 'translateY(0)'}))
        ]),
        transition(':leave', [
            animate('500ms ease-in', style({transform: 'translateY(400%)'}))   
        ])
    ]),

    trigger('message', [
      transition(':enter', [
          style({opacity: '0'}),
          animate('500ms 1000ms ease-out', style({opacity: '1'}))
      ]),
      transition(':leave', [
          animate('500ms ease-in', style({opacity: '0'}))   
      ])
    ])

  ]
})

Let’s discuss the animations themselves first. We have added triggers for badge and message – both with corresponding enter and leave animations.

The badge animation will cause the badge to fly in from the bottom, as we are animating translateY. When the enter animation is triggered the badge will start off the screen and fly to its normal position. On leaving, the badge will begin to move off screen again.

The message animation is just another simple opacity animation. However, we add a delay on this animation so that it triggers after the other elements have already animated in. I think this adds a little more intrigue to the animation.

We will need to add these triggers to our template, but that in itself isn’t enough. We have also added these animations to the animation group:

    trigger('container', [
      transition(':enter', [
          style({opacity: '0'}),
          group([
            animate('500ms ease-out', style({opacity: '1'})),
            query('@badge, @message', [
              animateChild()
            ])
          ])

      ]),
      transition(':leave', [
          group([
            animate('500ms ease-out', style({opacity: '0'})),
            query('@badge, @message', [
              animateChild()
            ])
          ])
      ])
    ]),

We query those elements by their trigger names, and then we manually run the animation by calling animateChild, which will trigger the animations we have already defined.

Now we just need to add those triggers to the template.

Modify the badge and message elements in home.page.html to reflect the following:

      <div class="medal" @badge>
          <img src="assets/medal.png" />
      </div>

      <div class="message" @message>
        <h5>Tutorial Completed</h5>
      </div>

If you run the animation now, you should have something that looks like this:

Achievement Animation in Ionic

Summary

Although the focus of this tutorial has just been on the animations, there is room to improve this functionality to be useful in a more general sense. Right now, it is hard coded and tied to the home page, but there is the potential to develop this into something that could be triggered dynamically.

If this would be of interest to you, rather than just the animations themselves, let me know in the comments.

What to watch next...