December 16, 2020
  • Engineering
  • Tutorials
  • Angular
  • Animations
  • Gestures
  • Tutorials

Building Interactive Ionic Apps with Gestures and Animations

Simon Grimm

This is a guest post from Simon Grimm, Ionic Developer Expert and educator at the Ionic Academy. Simon also created the Practical Ionic book, a guide to building real world Ionic applications with Capacitor and Firebase.

Since Ionic 5 we got access to a great Animations and Gestures utility API, which can help to add both simple interactive elements as well as complex gestures to your Ionic app.

In this tutorial we will go through different examples to spice up our Ionic app with animations and add gestures to elements.

Once we got the basics right, we will combine both approaches to build a custom slide to delete feature and finally use our own animations for Ionic page transitions!

Prerequisite

The coolest thing about all this upfront: You don’t need any additional package or library for everything we will implement in this tutorial!

To follow along, simply bring up a new Ionic application and you are good to go.

ionic start ionicInteractive blank --type=angular --capacitor

We will use Ionic Angular throughout the code snippets, but the same concepts and APIs exist for React and Vue as well.

If you also want a bit of “behind the scenes” on why those APIs were created in the first place, travel back in time to the introduction of Ionic Animations!

Simple Ionic Animations

Enough talk, let’s write some code. We start with a simple example of animating icons and buttons, and the only thing we need is a reference to our DOM elements.

Therefore, we create two buttons and a fab and give them a template reference so we can access them as a ViewChild later.

Open the home/home.page.html and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Interactive
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding-top">
  <ion-button expand="full" (click)="startLoad()">
    Load
    <ion-icon name="reload" slot="end" #loadingIcon></ion-icon>
  </ion-button>
  <ion-button expand="full" (click)="addToCart()" #cartBtn>
    Add to cart
  </ion-button>

  <ion-fab vertical="top" horizontal="end" slot="fixed" edge>
    <ion-fab-button #cartFabBtn color="secondary">
      <ion-icon name="cart"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

Now we can access our ViewChilds, and we will add a simple dummy animation to the loading icon of the button. Imagine a case where you don’t want to cover the whole view with a loading but still show some progress.

To create an animation, we need to handle a few things:

  • Create a new Animation with the AnimationController and give it a name
  • Add the native Element on which we want to perform the animation
  • Define what the animation should do
  • Call play() on the created Animation

That’s the core idea, and you can then define various elements of the animation like the duration (how long should the animation play?), iterations (how often should the animation run?) and the actual animation with keyframes or the simple fromTo() operator.

For now, let’s simply rotate the icon 3 times, where each iteration takes 1.5 seconds.

Therefore, open the home/home.page.ts and change it to:

import { Component, ElementRef, ViewChild } from '@angular/core';
import { AnimationController } from '@ionic/angular';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  @ViewChild('loadingIcon', { read: ElementRef }) loadingIcon: ElementRef;
  @ViewChild('cartBtn', { read: ElementRef }) cartBnt: ElementRef;
  @ViewChild('cartFabBtn', { read: ElementRef }) cartFabBnt: ElementRef;

  constructor(private animationCtrl: AnimationController) { }

  startLoad() {
    const loadingAnimation = this.animationCtrl.create('loading-animation')
      .addElement(this.loadingIcon.nativeElement)
      .duration(1500)
      .iterations(3)
      .fromTo('transform', 'rotate(0deg)', 'rotate(360deg)');

    // Don't forget to start the animation!
    loadingAnimation.play();
  }
}

Run your app, hit the button and you should see your first simple Ionic animation in action!

Chaining Ionic Animations

You are not limited to just animating one element or the specific operators, you could also use keyframes like in standard CSS animations or chain different animations together!

To create a chain, simply create two separate animations and group them together in one parent animation using addAnimation().

Go ahead and add a new function like this to your page:

addToCart() {
  const cartAnimation = this.animationCtrl.create('cart-animation')
    .addElement(this.cartBnt.nativeElement)
    .keyframes([
      { offset: 0, transform: 'scale(1)' },
      { offset: 0.5, transform: 'scale(1.2)' },
      { offset: 0.8, transform: 'scale(0.9)' },
      { offset: 1, transform: 'scale(1)' }
    ]);

  const cartColorAnimation = this.animationCtrl.create('cart-color-animation')
    .addElement(this.cartFabBnt.nativeElement)
    .fromTo('transform', 'rotate(0deg)', 'rotate(45deg)');


  const parent = this.animationCtrl.create('parent')
    .duration(300)
    .easing('ease-out')
    .iterations(2)
    .direction('alternate')
    .addAnimation([cartColorAnimation, cartAnimation]);

  // Playing the parent starts both animations
  parent.play();
}

You can now see both animations playing at the same time on different elements.

There’s a lot more you can do like removing certain classes with beforeRemoveClass() or beforeClearStyles and you can also do the opposite after an animation finishes to add a class back again.

Check out the whole Ionic animations API for all possibilities and to create your own epic animations!

Simple Ionic Gestures

Now that we’ve seen some animations, let’s take a look at gestures.

To get started, add a simple box to your view like this:

<ion-content class="ion-padding-top">
  <div #box
    [ngStyle]="{'width': '100px', 'height': '100px', 'background': 'var(--ion-color-secondary'}">
  </div>
</ion-content>

The definition of gestures requires certain values again:

  • The actual DOM element which reacts to the gesture
  • A threshold after which the gesture starts to prevent issues with scrolling or other functionality
  • A name for the gesture
  • Handling the gesture with onStart onMove and onEnd
  • Enable the gesture by calling enable(true) on the Gesture

Since you add the gesture to a ViewChild, you need to make sure you are running this inside the ngAfterViewInit or later as your ViewChild won’t be available before that time in the view lifecycle.

In our example, let’s say we want to simply move the box around inside the view (I know, not the most realistic example).

For this, we basically only need to get the coordinates of our touch gesture while dragging the item, which will be called whenever we move our finger on the screen.

So let’s move our element around with this gesture:

import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import { DomController, GestureController, IonHeader } from '@ionic/angular';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements AfterViewInit {
  @ViewChild('box', { read: ElementRef }) box: ElementRef;
  @ViewChild(IonHeader, { read: ElementRef }) header: ElementRef
  power = 0;
  longPressActive = false;

  constructor(private gestureCtrl: GestureController, private domCtrl: DomController) { }

  async ngAfterViewInit() {
    // Use DomCtrl to get the correct header height
    await this.domCtrl.read(() => {
      const headerHeight = this.header.nativeElement.offsetHeight;
      this.setupGesture(headerHeight)
    });
  }

  setupGesture(headerHeight) {
    const moveGesture = this.gestureCtrl.create({
      el: this.box.nativeElement,
      threshold: 0,
      gestureName: 'move',
      onStart: ev => {
        console.log('move start!');
      },
      onMove: ev => {
        console.log(ev);

        const currentX = ev.currentX;
        const currentY = ev.currentY;

        this.box.nativeElement.style.transform = `translate(${currentX}px, ${currentY-headerHeight}px)`;
      },
      onEnd: ev => {
        console.log('move end!');
      }
    });

    // Don't forget to enable!
    moveGesture.enable(true);
  }

}

There’s also an additional block before setting up the gesture to get the correct height of the header, since otherwise the gesture would respoisiton the element incorrectly.

This was a pretty basic example, let’s do a bit more!

Ionic Long Press Gesture

Over the years the question for a long press gesture came up over and over again, and with gestures you can easily create this on your own now.

First of all, let’s create a button that we can use for our gesture:

<ion-content class="ion-padding-top">
  <ion-button expand="full" #powerBtn>
    Increase Power: {{ power }}
    <ion-icon name="flame" slot="end"></ion-icon>
  </ion-button>
</ion-content>

Now we can go ahead and create another gesture almost like before, but this time we especially use onStart to set a longPressActive to true, and we will revert this inside the onEnd

Inside the start callback we also trigger our own increasePower() function that will continue to run until the long press is not active anymore. While it runs, it will increase the power variable a bit faster with every run.

Go ahead and give this second gesture a try by changing your class to:

import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import { GestureController } from '@ionic/angular';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements AfterViewInit {
  @ViewChild('powerBtn', { read: ElementRef }) powerBtn: ElementRef;
  power = 0;
  longPressActive = false;

  constructor(private gestureCtrl: GestureController) { }

  ngAfterViewInit() {
    const longPress = this.gestureCtrl.create({
      el: this.powerBtn.nativeElement,
      threshold: 0,
      gestureName: 'long-press',
      onStart: ev => {
        this.longPressActive = true;
        this.increasePower();
      },
      onEnd: ev => {
        this.longPressActive = false;
      }
    }, true); // Passing true will run the gesture callback inside of NgZone!

    // Don't forget to enable!
    longPress.enable(true);
  }

  increasePower(timeout = 200) {
    setTimeout(() => {
      if (this.longPressActive) {
          this.power++;
          this.increasePower(timeout/1.2);
      }
    }, timeout);
  }
}

This time there’s also a special case, since usually the callback functions of the gestures don’t run within Angular’s NgZone Normally, this means the view wouldn’t be updated although we changed a variable inside our function.

But you can easily change this by adding true at the end of the gesture definition (like we did in the above snippet) to fix this problem.

Of course this is not the only way to implement a long press – you can also see an implementation of this concept by Liam from Ionic here.

More examples of helpful gesture are parallax image scrolling or a custom bottom drawer component!

Combining Gestures and Animations

Now that we understand the basics of animations and gestures, the next logical step is to combine them to create even better interactive elements inside your Ionic app.

One thing you can quite easily create with a combination is a custom slide to delete functionality for your items.

Although we already got the ion-item-sliding, it never felt 100% correct to me since usually you can completely swipe out an item (like inside a mail application) which removes that row.

Now let’s create this by first of all setting up a few dummy items inside our view:

<ion-content>
  <div class="item-container" *ngFor="let i of myArray" #container>
    <ion-item>
      <ion-label>
        Dummy item {{ i }}
      </ion-label>
    </ion-item>
    <div class="icon-row">
      <div class="icon-container">
        <ion-icon name="trash"></ion-icon>
      </div>
    </div>
  </div>
</ion-content>

If you plan to use this gesture, I recommend to put the whole div into a shared component instead of implementing it right inside one page.

But for now, we don’t want to change too many files.

To make it look like the ion-item covers the icon-row div in the beginning, we can use a bit of CSS and use the grid layout for our elements:

.item-container {
  background-color: var(--ion-color-danger);
  display: grid;
}

ion-item {
  grid-column: 1;
  grid-row: 1;
}

.icon-row {
  grid-column: 1;
  grid-row: 1;
  display: grid;
  align-items: center;
  text-align: right;
  color: #fff;
  margin-right: 20px;
}

The list looks still the same, but now the container has a red background color (not visible at the moment) and the row with our icon is behind the actual ion-item Cool.

So how do you go about tackling a complex gesture and animation like this?

  • Since we got a list of items, we can access each of them inside a QueryList using the @ViewChildren decorator this time
  • We need to add a gesture to each item in that list and allow moving it to the left and right along the x axis, therefore we set the direction only to x
  • When an item is moved, we will transform the X position like in the example before

That’s basically the gesture part of this example, since this is already enough to move an item to the left and right to reveal the underlying background and delete icon.

We have actually also already defined our deleteAnimation above the gesture but not used it yet, since we want to add it inside the onEnd callback now!

If the deltaX (the change along the x axis) crosses a certain value, we will play our animation which simply changes the height of the container element to zero. If that’s not the case (the item was moved just a bit perhaps), we will simply slide it back into its original position.

Enough said, here’s the code for our custom swipe to delete gesture & animation:

import { AfterViewInit, Component, ElementRef, QueryList, ViewChildren } from '@angular/core';
import { AnimationController, DomController, GestureController, ToastController } from '@ionic/angular';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements AfterViewInit {
  @ViewChildren('container', { read: ElementRef }) itemContainer: QueryList<ElementRef>;
  myArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];

  constructor(private gestureCtrl: GestureController, private animationCtrl: AnimationController, private domCtrl: DomController, private toastCtrl: ToastController) { }

  ngAfterViewInit() {
    const windowWidth = window.innerWidth;
    const containerArray = this.itemContainer.toArray();

    for (let i = 0; i < containerArray.length; i++) {
      const containerElement = containerArray[i].nativeElement;

      // We know the ion-item is the first child of teachhe container element
      const itemElement = containerElement.childNodes[0];

      const deleteAnimation = this.animationCtrl.create()
        .addElement(containerElement)
        .duration(200)
        .easing('ease-out')
        .fromTo('height', '48px', '0');

      const swipeGesture = this.gestureCtrl.create({
        el: itemElement,
        threshold: 15,
        direction: 'x',
        gestureName: 'swipe-delete',
        onMove: ev => {
          const currentX = ev.deltaX;

          this.domCtrl.write(() => {
            // Make sure the item is above the other elements
            itemElement.style.zIndex = 2;
            // Reposition the item
            itemElement.style.transform = `translateX(${currentX}px)`;
          });
        },
        onEnd: ev => {
          itemElement.style.transition = '0.2s ease-out';

          // Fly out the element if we cross the threshold of 150px
          if (ev.deltaX < -150) {
            this.domCtrl.write(() => {
              itemElement.style.transform = `translate3d(-${windowWidth}px, 0, 0)`;
            });
            deleteAnimation.play();

            deleteAnimation.onFinish(async () => {
              this.myArray = this.myArray.filter(number => number != i + 1);

              const toast = await this.toastCtrl.create({
                message: `Item ${i + 1} removed.`,
                duration: 2000
              });
              toast.present();
            });
          } else {
            // Fly the item back into the original position
            this.domCtrl.write(() => {
              itemElement.style.transform = '';
            });
          }
        }
      }, true);

      // Don't forget to enable!
      swipeGesture.enable(true);
    }
  }

}

The animation alone only changes the height, but we also need to fly out the element, so we use the DomController again to shift the item out of the view – if you read carefully you could also chain the animations together without even using this additional block and create one animation that first decreases the height and then moves the item.

Finally, this is all just DOM operation, so we add another function to the onFinish callback of the gesture (yes, that exists!) so we can safely remove the item from our local array, which completes the whole slide to remove gesture!

It’s actually kinda fun to create these kinds of gesture and animation combinations, and if you want another example check out my popular Tinder swipe gesture tutorial!

Using Animations for Modals

After seeing the different use cases for animations and gestures in our Ionic app, let’s wrap this up by taking a look at another option: Defining your custom animations for page or modal transitions!

You heard right, you can define your own transitions and use them instead of the e.g. standard modal appear animation.

As a starting point, I highly recommend you check out the current implementation which you can easily find within the Ionic Github repository.

If you use this file as the base, you can change all parts quite easily to your need and build a (more or less) helpful other transitions like the one below.

To use it, simply create a new file inside your app at src/app/modal-animation.ts and insert:

import { Animation, createAnimation } from '@ionic/angular';

export const modalEnterAnimation = (
    baseEl: HTMLElement,
    presentingEl?: HTMLElement,
  ): Animation => {


  const backdropAnimation = createAnimation()
    .addElement(baseEl.querySelector('ion-backdrop')!)
    .fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
    .beforeStyles({
      'pointer-events': 'none'
    })
    .afterClearStyles(['pointer-events']);

  const wrapperAnimation = createAnimation()
    .addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
    .beforeStyles({ 'opacity': 1 })
    .keyframes([
        { offset: 0, transform: 'translateY(-100vh)' },
        { offset: 0.4, transform: 'translateY(20vh)'},
        { offset: 0.7, transform: 'translateY(-10vh)'},
        { offset: 1, transform: 'translateY(0vh)'},
      ]);

  const baseAnimation = createAnimation()
    .addElement(baseEl)
    .easing('ease-out')
    .duration(900)
    .addAnimation([wrapperAnimation, backdropAnimation]);

  return baseAnimation;
};

This animation will show the backdrop like the regular modal appear, but then somehow bounce in the next page. I’m sure you will come up with better animations than I did for your own app.

There are two ways to use your custom animations:

  • Define them while opening a modal
  • Define them as the standard inside the Ionic config at the App module

For the modal let’s simply use it directly when presenting a modal. This is really just passing in your exported modalEnterAnimation to the enterAnimation of the modal like this:

import { Component } from '@angular/core';
import { ModalController } from '@ionic/angular';
import { modalEnterAnimation } from '../modal-animation';
import { MyModalPage } from '../my-modal/my-modal.page';

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

  constructor(private modalCtrl: ModalController) { }

  async presentModal() {
    const modal = await this.modalCtrl.create({
      component: MyModalPage,
      showBackdrop: true,
      backdropDismiss: true,
      enterAnimation: modalEnterAnimation,
      cssClass: 'custom-modal'
    });

    await modal.present();
  }
}

To make the modal stand out a bit more and see the animation more clearly I also used a custom class for the modal which you need to define inside the src/global.scss:

.custom-modal {
    .modal-wrapper {
        width: 80%;
        height: 80%;
    }
}

This just changes the size of the modal a bit so it looks cooler above our current page!

You can basically define your own transition for any kind of overlay component that Ionic offers.

Changing the Ionic Page Transition Animation

As said before, you can also change the way your page transition looks like when using the Angular router.

And just like with the modal animation, you can either define them directly when navigating on a per-component basis or within your glocal Ionic config.

Defining a transition for pages is a bit more difficult, as you can see from the standard Ionic definition for page transitions on Github.

In this case, you can animate both the entering element and the leaving element to compose your own unique transition.

Let’s say we want to simply fade our pages instead of the regular transition. In that case, we can create a new file at src/app/nav-animation.ts and check for the direction property of the options to create the right animation order for our elements:

import { Animation, createAnimation } from '@ionic/angular';

export const enterAnimation = (baseEl: HTMLElement, opts?: any): Animation => {
    const DURATION = 1000;

    if (opts.direction === 'forward') {
        // Fade in the next page
        return createAnimation()
        .addElement(opts.enteringEl)
        .duration(DURATION)
        .easing('ease-in')
        .fromTo('opacity', 0, 1);
    } else if (opts.direction === 'back') {
        // Fade in the previous page
        const rootAnimation = createAnimation()
        .addElement(opts.enteringEl)
        .duration(DURATION)
        .easing('ease-out')
        .fromTo('opacity', 0, 1);

        // Fade out the current top page
        const leavingAnim = createAnimation()
        .addElement(opts.leavingEl)
        .duration(DURATION)
        .easing('ease-out')
        .fromTo('opacity', 1, 0);

        // Chain both animations
        return createAnimation().addAnimation([rootAnimation, leavingAnim]);
    }
};

But as you can see after a few examples – the basic syntax and creation of animations is always the same, so pick up the API and functions once and you are free to create any kind of powerful animation!

To use our custom animation, you can pass it right to the config object of your Ionic app like this:

import { enterAnimation } from './nav-animation';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot({
    navAnimation: enterAnimation // Add your animations!
  }), AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now all page transitions inside your app would use this new animation, you can simply use any kind of standard routing through the Angular router like:

constructor(private router: Router) { }

navigateToPage() {
  this.router.navigateByUrl('my-modal');
}

If that’s too much for you and you just need a decent animation in some places of your app, you can also use the Ionic NavController to define the animation for the next transition!

That means, calling setDirection() won’t directly change your page, but when the next transition happens, your defined transition and custom animation will be consumed:

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { NavController } from '@ionic/angular';
import { enterAnimation } from '../nav-animation';

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

  constructor(private router: Router, private navCtrl: NavController) { }

  navigateToPage() {
    // Define the animation for the transition!
    this.navCtrl.setDirection('forward', true, 'forward', enterAnimation);
    this.router.navigateByUrl('my-modal');
  }
}

Now we have completely changed the transition between two pages:

It’s actually quite amazing to see how easy these core parts of your Ionic app can be changed, given that we only defined one additional file and used it in the right places.

Conclusion

Improving your Ionic app with small animations and additional gestures can have a big impact on the overall user experience, and using the built in Ionic APIs makes working with them a breeze.

These were just a handful of examples, but you can basically do anything: Add a double tap gesture, create powerful directives or use advanced custom page transitions!

If you want to learn even more about Ionic with a library of 60+ video courses, templates and a supportive community, you can join the Ionic Academy and get access to a ton of learning material to boost your Ionic development skills.

And don’t forget to subscribe to my YouTube channel for fresh Ionic tutorials coming every week!


Simon Grimm