Recently, someone asked me if it is possible to create a shared element transition in an Ionic application. I wasn’t actually familiar with that specific term, but it is something I’ve wanted to try building in Ionic for a while and it’s actually a little bit easier than I thought it would be. The basic idea is that rather than just transitioning directly to a new page, we take one of the items on the existing page and animate that into its position on the new page.
This seems like it would be rather complicated to implement, but after seeing this video demonstrating the effect for an Android application, it highlighted that it’s really just a bit of an animation trick rather than having to have some kind of complicated navigation structure.
If you watch the video (and I recommend that you do), you will see that the basic idea behind creating this effect is:
- Fade in the new page on top of the current page
- Have the “shared” element be positioned in the same spot on the new page as it was on the current page
- As the new page is fading in, animate the position of the shared element to its normal position for the new page
There is no actual sharing of elements between the pages, we just make it look like there is by having the starting position of the “shared” element on the new page be the same as its position on the previous page.
We are going to create this process in an Ionic & Angular application using the ModalController
and some simple animations. Ours won’t be quite as impressive as the one in the video as we will just be focusing solely on this concept of “sharing” the element between the two pages and animating it. The example in the video also has some additional animations playing on the initial page which we won’t be implementing (e.g. the items that weren’t chosen fly off the page). That’s not to say it can’t be done, in fact I imagine it wouldn’t require that much more effort, but perhaps that’s a tutorial for another time.
Here is what we will be creating by the end of this tutorial:
Keep in mind that whilst we will be building the example above, you should treat this blog post more as research or a proof of concept rather than a fully fleshed out method for creating shared element transitions in Ionic. The method I am using to achieve this is rather manual. You could still implement this in your own applications with a bit of tweaking, probably in a variety of different situations. However, I’d like to investigate a more reusable/adaptable way to go about this in future.
Before We Get Started
Last updated for Ionic 4.0.0
This is a somewhat advanced tutorial and will assume that you already have a reasonably strong understanding of Ionic and Angular. If you require more introductory level content on Ionic I would recommend checking out my book or the Ionic tutorials on my website.
1. Create the Basic Layout
We are going to implement a basic master/detail pattern for this tutorial. We will have a list of cards that display images on the master page, and then when one of these are clicked the image for that card will become the “shared” element that is animated into position on the detail page.
A lot of this is just going to be basic layout stuff, so let’s get that out of the way first. I’ll assume you already have a “home” and “detail” page set up.
Modify src/app/home/home.page.html to reflect the following:
<ion-header><ion-toolbarcolor="danger"><ion-title>
Transition
</ion-title></ion-toolbar></ion-header><ion-content><ion-card(click)="launchDetail($event)"*ngFor="let card of cards"><imgsrc="http://placehold.it/600x300"/></ion-card></ion-content>
We are just looping over an array of cards
here which we will set up later, but notice that we are also sending the event information along with the (click)
binding. This is important as it will allow us to determine the position of the element that was clicked.
Modify src/app/detail/detail.page.html to reflect the following:
<ion-header><ion-toolbarcolor="danger"><ion-title>Detail</ion-title><ion-buttonsslot="end"><ion-button(click)="close()"><ion-iconslot="icon-only"name="close"></ion-icon></ion-button></ion-buttons></ion-toolbar></ion-header><ion-content><img#headersrc="http://placehold.it/600x300"/><divclass="container"><h2>Really cool...</h2><p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p><p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.</p></div></ion-content>
Here we have set up a template that has a header image at the top (which will be the shared element we are animating). Since we don’t want padding
for the header but we do want it for the content, we will set up a container
class with its own padding.
Modify src/app/detail/detail.page.scss to reflect the following:
.container{padding: 20px;}
2. Create a Custom Modal Animation
A key part of the shared element transition is having the detail page fade in on top of the master page. As I mentioned, we will be using a modal to overlay our detail page over the master page, but the default modal animation doesn’t use a “fade in” effect where the opacity is gradually animated from 0
to 1
. Therefore, we are going to create our own custom modal animation so that we can make it do whatever we like.
If you are not already familiar with creating a custom modal animation in Ionic, I have a separate tutorial that you can view here: Create a Custom Modal Page Transition Animation in Ionic. It is not important for the sake of this tutorial that you understand the details of how this custom animation works if you are not interested.
Create a file at src/app/animations/fade-in.ts and add the following:
import{ Animation }from'@ionic/core';exportfunctionmyFadeInAnimation(AnimationC: Animation, baseEl: HTMLElement): Promise<Animation>{const baseAnimation =newAnimationC();const backdropAnimation =newAnimationC();
backdropAnimation.addElement(baseEl.querySelector('ion-backdrop'));const wrapperAnimation =newAnimationC();
wrapperAnimation.addElement(baseEl.querySelector('.modal-wrapper'));
wrapperAnimation.beforeStyles({'opacity':1}).fromTo('translateX','0%','0%');
backdropAnimation.fromTo('opacity',0.01,0.4);return Promise.resolve(baseAnimation
.addElement(baseEl).easing('cubic-bezier(0.36,0.66,0.04,1)').duration(1000).beforeAddClass('show-modal').add(backdropAnimation).add(wrapperAnimation));}
We have created an animation that will animate to full opacity over one second.
3. Pass Position Information to the Modal
Now we need to define the launchDetail
function so that we can launch the modal, but we also need to pass the information about the clicked element’s position to the modal page.
Modify src/app/home/home.page.ts to reflect the following:
import{ Component }from'@angular/core';import{ ModalController }from'@ionic/angular';import{ DetailPage }from'../detail/detail.page';import{ myFadeInAnimation }from'../animations/fade-in';
@Component({
selector:'app-home',
templateUrl:'home.page.html',
styleUrls:['home.page.scss'],})exportclassHomePage{public cards =newArray(10);constructor(private modalCtrl: ModalController){}launchDetail(ev){this.modalCtrl.create({
component: DetailPage,
enterAnimation: myFadeInAnimation,
componentProps:{'coords':{
x: ev.target.x,
y: ev.target.y
}}}).then((modal)=>{
modal.present();});}}
This is mostly just the normal method for launching a modal, but we are also suppying our custom enterAnimation
and we pass in the coords
of the clicked element using the componentProps
of the modal.
4. Animate the Shared Elements Position
Now we just need to grab that passed in information and use it to animate the image element on the detail page from the position the element was in on the master page, to its normal position on the detail page.
Modify src/app/detail/detail.page.ts to reflect the following:
import{ Component, OnInit, ElementRef, Renderer2, ViewChild }from'@angular/core';import{ ModalController, NavParams }from'@ionic/angular';
@Component({
selector:'app-detail',
templateUrl:'./detail.page.html',
styleUrls:['./detail.page.scss'],})exportclassDetailPageimplementsOnInit{
@ViewChild('header') headerImage: ElementRef;constructor(private modalCtrl: ModalController,private navParams: NavParams,private renderer: Renderer2
){}ngOnInit(){let coords =this.navParams.get('coords');this.renderer.setStyle(this.headerImage.nativeElement,'transform',`translate3d(0, ${coords.y -56}px, 0) scale3d(0.9, 0.9, 1)`);this.renderer.setStyle(this.headerImage.nativeElement,'transition','0.5s ease-in-out');setTimeout(()=>{this.renderer.removeStyle(this.headerImage.nativeElement,'transform');},50);}close(){this.modalCtrl.dismiss();}}
We use @ViewChild
to grab a reference to our image element so that we can animate it. In the ngOnInit
function we grab the coords
that were passed in to the modal using NavParams
. We then set some initial styles on the image element. Using the coords
information, we move the element to a position that matches the master page by using translate3d
and we also scale it to be slightly smaller as it is on the master page.
When we change these styles back to their original styles, we want that transition to animate. So, we also modify the transition
property of the element and give it a property of 0.5s ease-in-out
which will animate the transition over half a second and use the ease-in-out
animation easing.
After a slight delay, we then remove the transform
style we applied which will trigger the animation. We should now have something like this:
Summary
I think there is more work to do here still, but we have a rather cool effect with relatively little effort. This isn’t just a “cool” animation either, it actually adds something to the user experience as it maintains the context better between the pages as it very clearly indicates that the new page contains information about the specific item that was clicked.