Ionic-framework: Use Android Transitions on iOS (Ionic 4)

Created on 20 Dec 2018  路  27Comments  路  Source: ionic-team/ionic-framework

Feature Request

Ionic version:


[x] 4.x

Describe the Feature Request

I want to use Android transitions on iOS because they are faster and look better with my current design. Like you can see in the GIF below, the iOS transitions doesn't work quite well with my app. The header/toolbar is visible quite to long.

Describe Preferred Solution

A simple solution to use Android transitions on iOS without creating a custom Animation with AnimationBuilder. Would be nice if you could add a configuration like in Ionic 3 to easy change the transitions of the platform: https://ionicframework.com/docs/api/config/Config/

Describe Alternatives

Currently I'm using the ModalController to fix this issue. The problem is that the page can't be lazy loaded by now.

Related Code

Ionic 4 iOS Transition Issue

Additional Context

I'm using CLI in Version 4.6.0 and Framework in Version RC-0.

core feature request

Most helpful comment

Hi @mariusbolik,
you can try my custom animation for your app
in app.module.ts add:

import { Animation, NavOptions } from '@ionic/core';
@NgModule({
    declarations: [
    AppComponent,
    ],
    entryComponents: [],
    imports: [
        ...
    IonicModule.forRoot({
        navAnimation: myTransitionAnimation,
    }),
    ],
    providers: [
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
    ],
    bootstrap: [AppComponent]
})
export function myTransitionAnimation(AnimationC: Animation, _: HTMLElement, opts: TransitionOptions): Promise<Animation> {

    const TRANSLATE_DIRECTION = 'translateX';
    const OFF_BOTTOM = '100%';
    const CENTER = '0px';
    const enteringEl = opts.enteringEl;
    const leavingEl = opts.leavingEl;
    const ionPageElement = getIonPageElement(enteringEl);
    const rootTransition = new AnimationC();

    rootTransition
    .addElement(ionPageElement)
    .beforeRemoveClass('ion-page-invisible');

    const backDirection = (opts.direction === 'back');

   // animate the component itself
   if (backDirection) {
    rootTransition
    .duration(opts.duration || 350)
    .easing('cubic-bezier(0.3,0,0.66,1)');

   } else {
    rootTransition
    .duration(opts.duration || 350)
    .easing('cubic-bezier(0.3,0,0.66,1)')
    .fromTo(TRANSLATE_DIRECTION, OFF_BOTTOM, CENTER, true)
    .fromTo('opacity', 1, 1, true);
   }

   // Animate toolbar if it's there
   const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar');
   if (enteringToolbarEle) {
    const enteringToolBar = new AnimationC();
    enteringToolBar.addElement(enteringToolbarEle);
    rootTransition.add(enteringToolBar);
   }

   // setup leaving view
   if (leavingEl && backDirection) {
    rootTransition
    .duration(opts.duration || 350)
    .easing('cubic-bezier(0.3,0,0.66,1)');

    const leavingPage = new AnimationC();
    leavingPage
    .addElement(getIonPageElement(leavingEl))
    .fromTo(TRANSLATE_DIRECTION, CENTER, OFF_BOTTOM)
    .fromTo('opacity', 1, 1);

    rootTransition.add(leavingPage);
   }

   return Promise.resolve(rootTransition);
}

export interface TransitionOptions extends NavOptions {
    animationCtrl: HTMLIonAnimationControllerElement;
    progressCallback?: ((ani: Animation | undefined) => void);
    window: Window;
    baseEl: any;
    enteringEl: HTMLElement;
    leavingEl: HTMLElement | undefined;
}

function getIonPageElement(element: HTMLElement) {
    if (element.classList.contains('ion-page')) {
        return element;
    }
    const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');
    if (ionPage) {
        return ionPage;
    }
    return element;
}

All 27 comments

ionic3

There are similar problems

Hi @mariusbolik,
you can try my custom animation for your app
in app.module.ts add:

import { Animation, NavOptions } from '@ionic/core';
@NgModule({
    declarations: [
    AppComponent,
    ],
    entryComponents: [],
    imports: [
        ...
    IonicModule.forRoot({
        navAnimation: myTransitionAnimation,
    }),
    ],
    providers: [
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
    ],
    bootstrap: [AppComponent]
})
export function myTransitionAnimation(AnimationC: Animation, _: HTMLElement, opts: TransitionOptions): Promise<Animation> {

    const TRANSLATE_DIRECTION = 'translateX';
    const OFF_BOTTOM = '100%';
    const CENTER = '0px';
    const enteringEl = opts.enteringEl;
    const leavingEl = opts.leavingEl;
    const ionPageElement = getIonPageElement(enteringEl);
    const rootTransition = new AnimationC();

    rootTransition
    .addElement(ionPageElement)
    .beforeRemoveClass('ion-page-invisible');

    const backDirection = (opts.direction === 'back');

   // animate the component itself
   if (backDirection) {
    rootTransition
    .duration(opts.duration || 350)
    .easing('cubic-bezier(0.3,0,0.66,1)');

   } else {
    rootTransition
    .duration(opts.duration || 350)
    .easing('cubic-bezier(0.3,0,0.66,1)')
    .fromTo(TRANSLATE_DIRECTION, OFF_BOTTOM, CENTER, true)
    .fromTo('opacity', 1, 1, true);
   }

   // Animate toolbar if it's there
   const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar');
   if (enteringToolbarEle) {
    const enteringToolBar = new AnimationC();
    enteringToolBar.addElement(enteringToolbarEle);
    rootTransition.add(enteringToolBar);
   }

   // setup leaving view
   if (leavingEl && backDirection) {
    rootTransition
    .duration(opts.duration || 350)
    .easing('cubic-bezier(0.3,0,0.66,1)');

    const leavingPage = new AnimationC();
    leavingPage
    .addElement(getIonPageElement(leavingEl))
    .fromTo(TRANSLATE_DIRECTION, CENTER, OFF_BOTTOM)
    .fromTo('opacity', 1, 1);

    rootTransition.add(leavingPage);
   }

   return Promise.resolve(rootTransition);
}

export interface TransitionOptions extends NavOptions {
    animationCtrl: HTMLIonAnimationControllerElement;
    progressCallback?: ((ani: Animation | undefined) => void);
    window: Window;
    baseEl: any;
    enteringEl: HTMLElement;
    leavingEl: HTMLElement | undefined;
}

function getIonPageElement(element: HTMLElement) {
    if (element.classList.contains('ion-page')) {
        return element;
    }
    const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');
    if (ionPage) {
        return ionPage;
    }
    return element;
}

Thank you so much @duyetnk,

this really helps a lot!

thank you @duyetnk, you solved my problem too

glad to help you guy

Great solution. Is there a way to do this only for platform ios?

update, found out a way to do this only for iOS:

let ionic = [
    IonicModule.forRoot()
];

const platform = new Platform();

if (platform.is('ios')) {
    ionic = [
        IonicModule.forRoot({
            navAnimation: myTransitionAnimation,
        })
    ]
}

and in module imports:

imports: [ ...ionic, ]

@duyetnk hi! I am dealing with animation issues with iOS so I was looking in to checking your custom animation. It appears that HTMLIonAnimationControllerElement interface for animationCtrl no longer exists in 4.0.0-rc.2? I copied your code as is, what am I missing? Thanks!

hi uncvrd,
you can copy transitionoptions from here
https://github.com/ionic-team/ionic/blob/master/core/src/utils/transition/index.ts

or import whole file, i didnt test it yet

UPDATE: the ionic team has changed source code,
you guys can go there to update your transition
https://github.com/ionic-team/ionic/blob/master/core/src/utils/transition/

Hi @mariusbolik,
you can try my custom animation for your app
in app.module.ts add:

import { Animation, NavOptions } from '@ionic/core';
@NgModule({
  declarations: [
  AppComponent,
  ],
  entryComponents: [],
  imports: [
        ...
  IonicModule.forRoot({
      navAnimation: myTransitionAnimation,
  }),
  ],
  providers: [
  { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export function myTransitionAnimation(AnimationC: Animation, _: HTMLElement, opts: TransitionOptions): Promise<Animation> {

  const TRANSLATE_DIRECTION = 'translateX';
  const OFF_BOTTOM = '100%';
  const CENTER = '0px';
  const enteringEl = opts.enteringEl;
  const leavingEl = opts.leavingEl;
  const ionPageElement = getIonPageElement(enteringEl);
  const rootTransition = new AnimationC();

  rootTransition
  .addElement(ionPageElement)
  .beforeRemoveClass('ion-page-invisible');

  const backDirection = (opts.direction === 'back');

   // animate the component itself
   if (backDirection) {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)');

   } else {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)')
      .fromTo(TRANSLATE_DIRECTION, OFF_BOTTOM, CENTER, true)
      .fromTo('opacity', 1, 1, true);
   }

   // Animate toolbar if it's there
   const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar');
   if (enteringToolbarEle) {
      const enteringToolBar = new AnimationC();
      enteringToolBar.addElement(enteringToolbarEle);
      rootTransition.add(enteringToolBar);
   }

   // setup leaving view
   if (leavingEl && backDirection) {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)');

      const leavingPage = new AnimationC();
      leavingPage
      .addElement(getIonPageElement(leavingEl))
      .fromTo(TRANSLATE_DIRECTION, CENTER, OFF_BOTTOM)
      .fromTo('opacity', 1, 1);

      rootTransition.add(leavingPage);
   }

   return Promise.resolve(rootTransition);
}

export interface TransitionOptions extends NavOptions {
  animationCtrl: HTMLIonAnimationControllerElement;
  progressCallback?: ((ani: Animation | undefined) => void);
  window: Window;
  baseEl: any;
  enteringEl: HTMLElement;
  leavingEl: HTMLElement | undefined;
}

function getIonPageElement(element: HTMLElement) {
  if (element.classList.contains('ion-page')) {
      return element;
  }
  const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');
  if (ionPage) {
      return ionPage;
  }
  return element;
}

where is HTMLIonAnimationControllerElement ?

just remove the folowing line:
animationCtrl: HTMLIonAnimationControllerElement;

update, found out a way to do this only for iOS:

let ionic = [
    IonicModule.forRoot()
];

const platform = new Platform();

if (platform.is('ios')) {
    ionic = [
        IonicModule.forRoot({
            navAnimation: myTransitionAnimation,
        })
    ]
}

and in module imports:

imports: [ ...ionic, ]

I am facing the same issue. while navigating one screen to other screen in iOS.
but in Android it is working as expected.

@elmartino could you please give more elaborate on your answer. it could help full.
Thanks in Advance.

thanks!!! . a lot of thanks!. it's working. ;)

I think the simplest way to use Android Transitions on iOS is the following:

import { mdTransitionAnimation } from '@ionic/core/dist/collection/utils/transition/md.transition';

...

IonicModule.forRoot({
   navAnimation: mdTransitionAnimation
}),

This also works for Ionic 4.8+

Hi @mariusbolik do you know how to do this on specific page only? Not the entire page as you did.

Hi @mirzailhami, did you find a way to change the page transition animation for a specific page?

And also, I'm not able to build my app with --prod when I try to use the solution proposed by @mariusbolik

I get the following error

ERROR in app/app.module.ts(113,21): Error during template compile of 'AppModule'
  Function expressions are not supported in decorators in 'mdTransitionAnimation'
    'mdTransitionAnimation' contains the error at ../@ionic/core/dist/collection/utils/transition/md.transition.js.ts(1,38)
      Consider changing the function expression into an exported function.

Is there a workaround that makes it possible? Thank you guys.

@mateusduraes, you need to wrap the exported function expression into your own function:

import { mdTransitionAnimation } from '@ionic/core/dist/collection/utils/transition/md.transition';
import { Animation } from '@ionic/core';

export function transitionAnimation(foo: Animation, el: HTMLElement, opts: any): Promise<Animation> {
  return Promise.resolve(mdTransitionAnimation(el, opts));
}

...and use that one in the decorator:

IonicModule.forRoot({
   navAnimation: transitionAnimation
}),

Hi @damirarh, thank you, it works.
I just made a small fix in your code because it was giving me some errors.

return Promise.resolve(mdTransitionAnimation(foo, el, opts))

Is this feature considered to be available in the next releases? In Ionic 3 we
was able to change the page animation based on the active platform (android | ios)

@mateusduraes, I managed to use a different animation based on the platform using the following code:

import { Animation, isPlatform } from '@ionic/core';

export function transitionAnimation(AnimationC: Animation, el: HTMLElement, opts: TransitionOptions): Promise<Animation> {
  if (isPlatform('ios')) {
    // invoke iOS animation
  } else {
    // invoke Android animation
  }
}

Still, it would be great to have a built-in way to do this in Ionic.

Hi @mariusbolik,
you can try my custom animation for your app
in app.module.ts add:

import { Animation, NavOptions } from '@ionic/core';
@NgModule({
  declarations: [
  AppComponent,
  ],
  entryComponents: [],
  imports: [
        ...
  IonicModule.forRoot({
      navAnimation: myTransitionAnimation,
  }),
  ],
  providers: [
  { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export function myTransitionAnimation(AnimationC: Animation, _: HTMLElement, opts: TransitionOptions): Promise<Animation> {

  const TRANSLATE_DIRECTION = 'translateX';
  const OFF_BOTTOM = '100%';
  const CENTER = '0px';
  const enteringEl = opts.enteringEl;
  const leavingEl = opts.leavingEl;
  const ionPageElement = getIonPageElement(enteringEl);
  const rootTransition = new AnimationC();

  rootTransition
  .addElement(ionPageElement)
  .beforeRemoveClass('ion-page-invisible');

  const backDirection = (opts.direction === 'back');

   // animate the component itself
   if (backDirection) {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)');

   } else {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)')
      .fromTo(TRANSLATE_DIRECTION, OFF_BOTTOM, CENTER, true)
      .fromTo('opacity', 1, 1, true);
   }

   // Animate toolbar if it's there
   const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar');
   if (enteringToolbarEle) {
      const enteringToolBar = new AnimationC();
      enteringToolBar.addElement(enteringToolbarEle);
      rootTransition.add(enteringToolBar);
   }

   // setup leaving view
   if (leavingEl && backDirection) {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)');

      const leavingPage = new AnimationC();
      leavingPage
      .addElement(getIonPageElement(leavingEl))
      .fromTo(TRANSLATE_DIRECTION, CENTER, OFF_BOTTOM)
      .fromTo('opacity', 1, 1);

      rootTransition.add(leavingPage);
   }

   return Promise.resolve(rootTransition);
}

export interface TransitionOptions extends NavOptions {
  animationCtrl: HTMLIonAnimationControllerElement;
  progressCallback?: ((ani: Animation | undefined) => void);
  window: Window;
  baseEl: any;
  enteringEl: HTMLElement;
  leavingEl: HTMLElement | undefined;
}

function getIonPageElement(element: HTMLElement) {
  if (element.classList.contains('ion-page')) {
      return element;
  }
  const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');
  if (ionPage) {
      return ionPage;
  }
  return element;
}

Thanks, i am using this code, but i have audio tag to play audios on my pages, Animation is causing it width to reduce. How can i set it up so that it does not reduce audio's width?

Hi @mariusbolik,
you can try my custom animation for your app
in app.module.ts add:

import { Animation, NavOptions } from '@ionic/core';
@NgModule({
  declarations: [
  AppComponent,
  ],
  entryComponents: [],
  imports: [
        ...
  IonicModule.forRoot({
      navAnimation: myTransitionAnimation,
  }),
  ],
  providers: [
  { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export function myTransitionAnimation(AnimationC: Animation, _: HTMLElement, opts: TransitionOptions): Promise<Animation> {

  const TRANSLATE_DIRECTION = 'translateX';
  const OFF_BOTTOM = '100%';
  const CENTER = '0px';
  const enteringEl = opts.enteringEl;
  const leavingEl = opts.leavingEl;
  const ionPageElement = getIonPageElement(enteringEl);
  const rootTransition = new AnimationC();

  rootTransition
  .addElement(ionPageElement)
  .beforeRemoveClass('ion-page-invisible');

  const backDirection = (opts.direction === 'back');

   // animate the component itself
   if (backDirection) {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)');

   } else {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)')
      .fromTo(TRANSLATE_DIRECTION, OFF_BOTTOM, CENTER, true)
      .fromTo('opacity', 1, 1, true);
   }

   // Animate toolbar if it's there
   const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar');
   if (enteringToolbarEle) {
      const enteringToolBar = new AnimationC();
      enteringToolBar.addElement(enteringToolbarEle);
      rootTransition.add(enteringToolBar);
   }

   // setup leaving view
   if (leavingEl && backDirection) {
      rootTransition
      .duration(opts.duration || 350)
      .easing('cubic-bezier(0.3,0,0.66,1)');

      const leavingPage = new AnimationC();
      leavingPage
      .addElement(getIonPageElement(leavingEl))
      .fromTo(TRANSLATE_DIRECTION, CENTER, OFF_BOTTOM)
      .fromTo('opacity', 1, 1);

      rootTransition.add(leavingPage);
   }

   return Promise.resolve(rootTransition);
}

export interface TransitionOptions extends NavOptions {
  animationCtrl: HTMLIonAnimationControllerElement;
  progressCallback?: ((ani: Animation | undefined) => void);
  window: Window;
  baseEl: any;
  enteringEl: HTMLElement;
  leavingEl: HTMLElement | undefined;
}

function getIonPageElement(element: HTMLElement) {
  if (element.classList.contains('ion-page')) {
      return element;
  }
  const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');
  if (ionPage) {
      return ionPage;
  }
  return element;
}

Updated to work with Ionic 5 Beta 2:

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

export const myTransitionAnimation = (_: HTMLElement, opts: TransitionOptions): Animation => {

  const TRANSLATE_DIRECTION = 'translateX';
  const OFF_BOTTOM = '100%';
  const CENTER = '0px';
  const enteringEl = opts.enteringEl;
  const leavingEl = opts.leavingEl;
  // tslint:disable-next-line: no-use-before-declare
  const ionPageElement = getIonPageElement(enteringEl);
  const rootTransition = createAnimation();

  rootTransition
    .addElement(ionPageElement)
    .fill('both')
    .beforeRemoveClass('ion-page-invisible');

  const backDirection = (opts.direction === 'back');

  // animate the component itself
  if (backDirection) {
    rootTransition
      .duration(opts.duration || 100)
      .easing('cubic-bezier(0.3,0,0.66,1)');

  } else {
    rootTransition
      .duration(opts.duration || 100)
      .easing('cubic-bezier(0.3,0,0.66,1)')
      .fromTo('transform', `translateX(${OFF_BOTTOM})`, `translateX(${CENTER})`)
      .fromTo('opacity', 1, 1);
  }

  // Animate toolbar if it's there
  const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar');
  if (enteringToolbarEle) {
    const enteringToolBar = createAnimation();
    enteringToolBar.addElement(enteringToolbarEle);
    rootTransition.addAnimation(enteringToolBar);
  }

  // setup leaving view
  if (leavingEl && backDirection) {
    // leaving content
    rootTransition
      .duration(opts.duration || 100)
      .easing('cubic-bezier(0.3,0,0.66,1)');

    const leavingPage = createAnimation();
    leavingPage
      // tslint:disable-next-line: no-use-before-declare
      .addElement(getIonPageElement(leavingEl))
      .afterStyles({ 'display': 'none' })
      .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_BOTTOM})`)
      .fromTo('opacity', 1, 1);

    rootTransition.addAnimation(leavingPage);
  }

  return rootTransition;
};

export interface TransitionOptions extends NavOptions {
  progressCallback?: ((ani: Animation | undefined) => void);
  baseEl: any;
  enteringEl: HTMLElement;
  leavingEl: HTMLElement | undefined;
}

export const getIonPageElement = (element: HTMLElement) => {
  if (element.classList.contains('ion-page')) {
    return element;
  }

  const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');
  if (ionPage) {
    return ionPage;
  }
  // idk, return the original element so at least something animates and we don't have a null pointer
  return element;
};

@mateusduraes, you need to wrap the exported function expression into your own function:

import { mdTransitionAnimation } from '@ionic/core/dist/collection/utils/transition/md.transition';
import { Animation } from '@ionic/core';

export function transitionAnimation(foo: Animation, el: HTMLElement, opts: any): Promise<Animation> {
  return Promise.resolve(mdTransitionAnimation(el, opts));
}

...and use that one in the decorator:

IonicModule.forRoot({
   navAnimation: transitionAnimation
}),

I get error as
Module not found: Error: Can't resolve '@stencil/core'

Use this for Ionic 5:

import { mdTransitionAnimation } from '@ionic/core';
IonicModule.forRoot({
  mode: 'ios',
  navAnimation: mdTransitionAnimation
})

Use this for Ionic 5:

import { mdTransitionAnimation } from '@ionic/core';
IonicModule.forRoot({
  mode: 'ios',
  navAnimation: mdTransitionAnimation
})

thank you, now it says:

ERROR in src/app/app.module.ts(42,21): Error during template compile of 'AppModule' Only initialized variables and constants can be referenced in decorators because the value of this variable is needed by the template compiler in 'mdTransitionAnimation' 'mdTransitionAnimation' references 'mdTransitionAnimation' 'mdTransitionAnimation' references 'mdTransitionAnimation' 'mdTransitionAnimation' is not initialized at @ionic/core/dist/types/utils/transition/md.transition.ts(3,22).

I can't make it works =(

Thanks for the issue. I am going to close this as this is now possible Ionic Framework.

Ionic Angular example:

import { mdTransitionAnimation } from '@ionic/angular';

IonicModule.forRoot({
  navAnimation: mdTransitionAnimation
});

Note: This requires Ionic Framework v5.1.0 or newer.

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

manucorporat picture manucorporat  路  3Comments

brandyscarney picture brandyscarney  路  3Comments

vswarte picture vswarte  路  3Comments

Nick-The-Uncharted picture Nick-The-Uncharted  路  3Comments

gio82 picture gio82  路  3Comments