Another way to get a wow experience from users is to have great-looking introduction slides. A typical app would have three to five slides to describe what the app does and how it will benefit the users. Today, many apps even add videos or interactive screens so that the users can get a feel for how the app may work. Such an interactive animation will require some internal development to bind touch gestures to the animation state. Animating based on a specific state is very difficult because you really have to get granular gesture data. On the other hand, it's a lot easier to just animate at the beginning or ending of a state. For example, you could animate an object inside a slide when the slide completely shows up on the screen after a left swipe. However, this animation effect is not as interesting or attractive as binding the animation during the touch movement.
The app you will build in this section will have three slides that will animate when you swipe left or right:
You will see fade in and fade out animation effects between slides. The following Angular logo also moves up when you swipe left from the second slide:
There is no need to test the app in a physical device because the animation is done via HTML and JavaScript. However, it's recommended to test the app in your device to evaluate to animation performance.
Here are the instructions:
SliderAnimation
app using the blank
template, as follows, and go to the SliderAnimation
folder:$ ionic start SliderAnimation blank --v2 $ cd SliderAnimation
./src/pages/home/home.html
file and modify its content with the following code:<ion-content class="home"> <div class="slides-float"> <div class="slide-float" #slidefloat1> <ion-icon name="ios-ionic"></ion-icon> <h1>Ionic 2</h1> </div> <div class="slide-float" #slidefloat2> <ion-icon name="logo-angular"></ion-icon> <h1>Angular 2</h1> </div> <div class="slide-float" #slidefloat3> <ion-icon name="logo-javascript"></ion-icon> <h1>Both</h1> </div> </div> <ion-slides #myslides pager (ionDrag)="onMove()"> <ion-slide> <h2>is Beautiful</h2> </ion-slide> <ion-slide> <h2>is Fast</h2> </ion-slide> <ion-slide> <h2>are Awesome</h2> </ion-slide> </ion-slides> </ion-content>
This template mainly uses the <ion-slides>
tag. However, there are some layers to float on top of the <ion-slide>
tag in order to animate them separately.
./src/pages/home/home.ts
with the following:import { Component, ViewChild } from '@angular/core'; import { NavController } from 'ionic-angular'; @Component({ selector: 'page-home', templateUrl: 'home.html' }) export class HomePage { @ViewChild('myslides') myslides; @ViewChild('slidefloat1') slidefloat1; @ViewChild('slidefloat2') slidefloat2; @ViewChild('slidefloat3') slidefloat3; private rAf: any; private bindOnProgress: boolean = false; constructor(public navCtrl: NavController) { this.rAf = (function(){ return (window as any).requestAnimationFrame || (window as any).webkitRequestAnimationFrame || (window as any).mozRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })(); } onMove() { if (!this.bindOnProgress) { this.bindOnProgress = true; this.myslides.slider.on('onProgress', (swiper, progress) => { // (0, 1) - (0.25, 0) ==> (0-1)/(0.25-0) => -1/0.25 * x + 1 let firstQuarter = () => { let slidefloat1Opacity = -1/0.25 * progress + 1; console.log('slidefloat1Opacity: ' + slidefloat1Opacity); this.slidefloat1.nativeElement.style.opacity = slidefloat1Opacity; this.slidefloat2.nativeElement.style.opacity = 0; } // (0.25, 0) - (0.5, 1) ==> (1-0)/(0.5-0.25) => 1/0.25 * x - 1 = 4*x - 1 let secondQuarter = () => { let slidefloat2Opacity = 4 * progress - 1; console.log('slidefloat2Opacity: ' + slidefloat2Opacity); this.slidefloat2.nativeElement.style.opacity = slidefloat2Opacity; this.slidefloat2.nativeElement.style.transform = 'translateY(0px)'; this.slidefloat1.nativeElement.style.opacity = 0; } // (0.5, 0) - (0.75, -250) ==> (-250-0)/(0.75-0.5) = -250/0.25 => -1000*x + 500 let thirdQuarter = () => { let slidefloat2transform = -1000 * progress + 500; console.log('slidefloat2transform: ' + slidefloat2transform); this.slidefloat2.nativeElement.style.transform = 'translateY(' + slidefloat2transform + 'px)'; this.slidefloat3.nativeElement.style.opacity = 0; } // (0.75, 0) - (1, 1) ==> (1-0)/(1-0.75) => 1/0.25 * x - 0.75*4 = 4*x - 3 let fourthQuarter = () => { let slidefloat3Opacity = 4 * progress - 3; console.log('slidefloat3Opacity: ' + slidefloat3Opacity); this.slidefloat3.nativeElement.style.opacity = slidefloat3Opacity; this.slidefloat2.nativeElement.style.transform = 'translateY(-250px)'; } // Animate per quarter of the total 3 slides if (progress <= 0.25) { this.rAf(firstQuarter); } else if ((progress > 0.25) && (progress <= 0.5 )) { this.rAf(secondQuarter); } else if ((progress > 0.5) && (progress <= 0.75 )) { this.rAf(thirdQuarter); } else if ((progress > 0.75) && (progress <= 1 )) { this.rAf(fourthQuarter); } }); } } }
Note that the comments are useful to calculate an animation formula for each object.
./app/pages/home/home.scss
with the following:.slides-float { .slide-float { top: 0; position: fixed; width: 100%; margin-top: 20px; } } .home { background-color: DarkSlateBlue; h2 { font-size: 3rem; } ion-slide { color: white; background-color: transparent; } .slides-float { color: white; text-align: center; > .slide-float:nth-child(2), > .slide-float:nth-child(3) { opacity: 0; } } .slide-float { ion-icon { font-size: 150px; } h1 { font-weight: lighter; font-size: 60px; margin-top: 0; } } }
$ ionic serve
This is the general process for animation:
You probably noted that the amount of fade or movement will depend on the progress percentage. Thus, if you swipe left and right a little bit, you can see the animation responding to the gesture right away. There are two layers in the template. The floating static layer, as illustrated, must be on top and it must stay at the same position regardless of which slide is current:
<div class="slides-float"> <div class="slide-float" #slidefloat1> <ion-icon name="ios-ionic"></ion-icon> <h1>Ionic 2</h1> </div> <div class="slide-float" #slidefloat2> <ion-icon name="logo-angular"></ion-icon> <h1>Angular 2</h1> </div> <div class="slide-float" #slidefloat3> <ion-icon name="logo-javascript"></ion-icon> <h1>Both</h1> </div> </div>
The bottom layer is your typical <ion-slides>
, as shown:
<ion-slides #myslides pager (ionDrag)="onMove()"> <ion-slide> <h2>is Beautiful</h2> </ion-slide> <ion-slide> <h2>is Fast</h2> </ion-slide> <ion-slide> <h2>are Awesome</h2> </ion-slide> </ion-slides>
When you swipe, it's actually moving <ion-slide>
. However, it also triggers the onMove()
method because you bind it with the move
event. The onMove()
method will access #slidefloat1
, #slidefloat2
, and #slidefloat3
from the floating <div>
layer. The home.ts
file is where you have to animate these individual floating slides.
There are several variables that you need to declare in the home.ts
file. You need to be able to access the <ion-slides>
object in order to call the native Swiper methods:
@ViewChild('myslides') myslides;
According to the Ionic documentation, the <ion-slides>
object is written based on the Swiper library at http://ionicframework.com/docs/v2/api/components/slides/Slides/.
You need to bind it with the swiping event natively in order to get the correct progress data.
The following three variables are necessary to access each floating slide:
@ViewChild('slidefloat1') slidefloat1; @ViewChild('slidefloat2') slidefloat2; @ViewChild('slidefloat3') slidefloat3;
You need to leverage requestAnimationFrame
, as follows, for the best animation performance:
private rAf: any;
Otherwise, users will sense a jerky movement during a swipe because your animation is not at 60 FPS.
Lastly, you need to bind the swipe event only once; thus, it's necessary to have a Boolean toggle to detect the binding event:
private bindOnProgress: boolean = false;
The following code shows how to create a requestAnimationFrame
object to call whichever function is to be rendered later:
this.rAf = (function(){ return (window as any).requestAnimationFrame || (window as any).webkitRequestAnimationFrame || (window as any).mozRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })();
The onMove()
method is where you put all the animation logic, which must bind with the onProgress
event, as follows:
this.myslides.slider.on('onProgress', {})
First, let's take a look at the code at the bottom of onMove()
, as shown:
if (progress <= 0.25) { this.rAf(firstQuarter); } else if ((progress > 0.25) && (progress <= 0.5 )) { this.rAf(secondQuarter); } else if ((progress > 0.5) && (progress <= 0.75 )) { this.rAf(thirdQuarter); } else if ((progress > 0.75) && (progress <= 1 )) { this.rAf(fourthQuarter); }
Basically, you want to have four quarters (or segments) of animation. When you swipe from slide 1 to slide 2, it will trigger the firstQuarter
and secondQuarter
methods. That is, you want to fade out the first floating slide and fade in the second floating slide at the end of the process. The concept is similar for the thirdQuarter
and fourthQuarter
methods. Note that you don't want to call the method directly but just pass the function reference inside this.rAf
to have the rendering engine manage the frame rate. Otherwise, the rendered function may end up blocking other processes in the UI, which causes jerky movement.
For each of the quarters, you only have to change the style
property, given a known progress value, as shown:
let firstQuarter = () => { let slidefloat1Opacity = -1/0.25 * progress + 1; console.log('slidefloat1Opacity: ' + slidefloat1Opacity); this.slidefloat1.nativeElement.style.opacity = slidefloat1Opacity; this.slidefloat2.nativeElement.style.opacity = 0; }
It's important to use the arrow function here so that you can access the this
context. You have to call this.slidefloat2.nativeElement
to get to the <div>
DOM object. It's really up to you to write your own math function to calculate the position or opacity during the slide movement with the progress value. In this example, the slidefloat1Opacity
variable is just a linear function based on the progress
input value.
The secondQuarter
follows the same approach. However, the thirdQuarter
uses the transform
property instead of opacity
, as illustrated:
let thirdQuarter = () => { let slidefloat2transform = -1000 * progress + 500; console.log('slidefloat2transform: ' + slidefloat2transform); this.slidefloat2.nativeElement.style.transform = 'translateY(' + slidefloat2transform + 'px)'; this.slidefloat3.nativeElement.style.opacity = 0; }
There are many ways to make a DOM object change its position. However, it's best to leverage the transform
property instead of using the left
and top
properties. You want to achieve the highest Frame Per Second. In the thirdQuarter
method, your slidefloat2transform
will be calculated and it will update a new Y position using translateY()
.
requestAnimationFrame
, you can check out the official documentation at https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame3.145.153.251