improve iOS transitions/reduce dom updates

This commit is contained in:
Adam Bradley
2015-10-11 12:47:39 -05:00
parent 6c44ae6246
commit 61bd51367a
6 changed files with 274 additions and 210 deletions

View File

@@ -13,7 +13,7 @@ ion-navbar.toolbar {
}
.back-button {
order: map-get($toolbar-order, backButton);;
order: map-get($toolbar-order, backButton);
transform: translateZ(0px);
display: none;

View File

@@ -8,6 +8,7 @@ import {ViewController} from './view-controller';
import {Transition} from '../../transitions/transition';
import {SwipeBackGesture} from './swipe-back';
import * as util from 'ionic/util';
import {raf} from '../../util/dom';
/**
* _For examples on the basic usage of NavController, check out the [Navigation section](../../../../components/#navigation)
@@ -350,11 +351,11 @@ export class NavController extends Ion {
let viewToRemove = this._views[index];
if (this.isActive(viewToRemove)){
return this.pop();
} else {
this._remove(index);
viewToRemove.destroy();
return Promise.resolve();
}
viewToRemove.shouldDestroy = true;
this._cleanup();
return Promise.resolve();
}
/**
@@ -677,7 +678,7 @@ export class NavController extends Ion {
* @private
* TODO
*/
_runSwipeBack() {
_sbComplete() {
if (this.canSwipeBack()) {
// it is possible to swipe back
@@ -744,12 +745,10 @@ export class NavController extends Ion {
* @private
*/
_transComplete() {
let destroys = [];
this._views.forEach(view => {
if (view) {
if (view.shouldDestroy) {
destroys.push(view);
view.didUnload();
} else if (view.state === CACHED_STATE && view.shouldCache) {
view.shouldCache = false;
@@ -757,100 +756,44 @@ export class NavController extends Ion {
}
});
destroys.forEach(view => {
this._remove(view);
view.destroy();
});
// allow clicks again, but still set an enable time
// meaning nothing with this view controller can happen for XXms
this.app.setEnabled(true);
this.app.setTransitioning(false);
if (this._views.length === 1) {
this.elementRef.nativeElement.classList.add('has-views');
}
this._sbComplete();
this._runSwipeBack();
raf(() => {
this._cleanup();
});
}
/**
* TODO
* @returns {TODO} TODO
*/
getActive() {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].state === ACTIVE_STATE) {
return this._views[i];
}
}
return null;
}
_cleanup() {
// the active view, and the previous view, should be rendered in dom and ready to go
// all others, like a cached page 2 back, should be display: none and not rendered
let destroys = [];
let activeView = this.getActive();
let previousView = this.getPrevious(activeView);
/**
* TODO
* @param {TODO} instance TODO
* @returns {TODO} TODO
*/
getByInstance(instance) {
if (instance) {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].instance === instance) {
return this._views[i];
this._views.forEach(view => {
if (view) {
if (view.shouldDestroy) {
destroys.push(view);
} else {
let isActiveView = (view === activeView);
let isPreviousView = (view === previousView);
view.domCache(isActiveView, isPreviousView);
}
}
}
return null;
}
});
/**
* TODO
* @param {TODO} index TODO
* @returns {TODO} TODO
*/
getByIndex(index) {
if (index < this._views.length && index > -1) {
return this._views[index];
}
return null;
}
/**
* TODO
* @param {TODO} view TODO
* @returns {TODO} TODO
*/
getPrevious(view) {
if (view) {
return this._views[ this._views.indexOf(view) - 1 ];
}
return null;
}
/**
* TODO
* @returns {TODO} TODO
*/
getStagedEnteringView() {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].state === STAGED_ENTERING_STATE) {
return this._views[i];
}
}
return null;
}
/**
* TODO
* @returns {TODO} TODO
*/
getStagedLeavingView() {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].state === STAGED_LEAVING_STATE) {
return this._views[i];
}
}
return null;
// all views being destroyed should be removed from the list of views
// and completely removed from the dom
destroys.forEach(view => {
this._remove(view);
view.destroy();
});
}
/**
@@ -906,6 +849,91 @@ export class NavController extends Ion {
util.array.remove(this._views, viewOrIndex);
}
/**
* TODO
* @returns {TODO} TODO
*/
getActive() {
for (let i = this._views.length - 1; i >= 0; i--) {
if (this._views[i].state === ACTIVE_STATE && !this._views[i].shouldDestroy) {
return this._views[i];
}
}
return null;
}
/**
* TODO
* @param {TODO} instance TODO
* @returns {TODO} TODO
*/
getByInstance(instance) {
if (instance) {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].instance === instance) {
return this._views[i];
}
}
}
return null;
}
/**
* TODO
* @param {TODO} index TODO
* @returns {TODO} TODO
*/
getByIndex(index) {
if (index < this._views.length && index > -1) {
return this._views[index];
}
return null;
}
/**
* TODO
* @param {TODO} view TODO
* @returns {TODO} TODO
*/
getPrevious(view) {
if (view) {
let viewIndex = this._views.indexOf(view);
for (let i = viewIndex - 1; i >= 0; i--) {
if (!this._views[i].shouldDestroy) {
return this._views[i];
}
}
}
return null;
}
/**
* TODO
* @returns {TODO} TODO
*/
getStagedEnteringView() {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].state === STAGED_ENTERING_STATE) {
return this._views[i];
}
}
return null;
}
/**
* TODO
* @returns {TODO} TODO
*/
getStagedLeavingView() {
for (let i = 0, ii = this._views.length; i < ii; i++) {
if (this._views[i].state === STAGED_LEAVING_STATE) {
return this._views[i];
}
}
return null;
}
/**
* First view in this nav controller's stack. This would
* not return an view which is about to be destroyed.

View File

@@ -130,6 +130,9 @@ import {NavController} from './nav-controller';
defaultInputs: {
'swipeBackEnabled': true
},
host: {
'[class.has-views]': '_views.length > 0'
},
template: '<template pane-anchor></template>',
directives: [forwardRef(() => NavPaneAnchor)]
})

View File

@@ -71,22 +71,31 @@ export function run() {
expect(active).toBe(null);
});
it('should return the first active page', () => {
it('should return the last active page', () => {
let activeView = new ViewController();
activeView.state = 1; // ACTIVE_STATE
nav._add(activeView);
var active = nav.getActive();
expect(active).toBe(activeView);
expect(nav.getActive()).toBe(activeView);
let secondActiveView = new ViewController();
secondActiveView.state = 1; // ACTIVE_STATE
nav._add(secondActiveView);
active = nav.getActive();
expect(active).toBe(activeView);
expect(nav.getActive()).toBe(secondActiveView);
});
it('should return the last active page thats not shouldDestroy', () => {
let view1 = new ViewController();
view1.state = 1; // ACTIVE_STATE
nav._add(view1);
expect(nav.getActive()).toBe(view1);
let view2 = new ViewController();
view2.state = 1; // ACTIVE_STATE
view2.shouldDestroy = true;
nav._add(view2);
expect(nav.getActive()).toBe(view1);
});
});

View File

@@ -94,8 +94,6 @@ export class ViewController {
for (let i = 0; i < this.disposals.length; i++) {
this.disposals[i]();
}
this.didUnload();
}
/**
@@ -140,6 +138,14 @@ export class ViewController {
return this._nbVw;
}
/**
* TODO
* @returns {TODO} TODO
*/
hasNavbar() {
return !!this.getNavbar();
}
/**
* TODO
* @returns {TODO} TODO
@@ -256,4 +262,20 @@ export class ViewController {
this.instance && this.instance.onPageDidUnload && this.instance.onPageDidUnload();
}
domCache(isActiveView, isPreviousView) {
let renderInDom = (isActiveView || isPreviousView);
let contentRef = this.contentRef();
if (contentRef) {
// the active view, and the previous view should have the 'show-view' css class
// all others, like a cached page 2 back, should now have 'show-view' so it's not rendered
contentRef.nativeElement.classList[renderInDom ? 'add' : 'remove' ]('show-view');
}
let navbarRef = this.getNavbar();
if (navbarRef) {
navbarRef.elementRef.nativeElement.classList[renderInDom ? 'add' : 'remove' ]('show-navbar');
}
}
}

View File

@@ -3,156 +3,158 @@ import {Animation} from '../animations/animation';
const DURATION = 550;
const EASING = 'cubic-bezier(0.36,0.66,0.04,1)';
const OPACITY = 'opacity';
const TRANSLATEX = 'translateX';
const OFF_RIGHT = '99.5%';
const OFF_LEFT = '-33%';
const CENTER = '0%'
const OFF_OPACITY = 0.8;
const SHOW_NAVBAR_CSS = 'show-navbar';
const SHOW_VIEW_CSS = 'show-view';
const SHOW_BACK_BTN_CSS = 'show-back-button';
class IOSTransition extends Transition {
class IOSTransition extends Animation {
constructor(navCtrl, opts) {
super(navCtrl, opts, IOSEnteringTransition, IOSLeavingTransition);
super(null, opts);
this.duration(DURATION);
this.easing(EASING);
}
}
// what direction is the transition going
let backDirection = (opts.direction === 'back');
class IOSEnteringTransition extends ViewTransition {
// get entering/leaving views
let enteringView = navCtrl.getStagedEnteringView();
let leavingView = navCtrl.getStagedLeavingView();
constructor(navCtrl, viewCtrl, opts) {
super(navCtrl, viewCtrl, opts);
// do they have navbars?
let enteringHasNavbar = enteringView.hasNavbar();
let leavingHasNavbar = leavingView && leavingView.hasNavbar();
this.content
.to(TRANSLATEX, CENTER)
.to(OPACITY, 1)
.before.setStyles({ zIndex: viewCtrl.index });
this.navbar.before.addClass('show-navbar');
// entering content
let enteringContent = new Animation(enteringView.contentRef());
enteringContent
.before.addClass(SHOW_VIEW_CSS)
.before.setStyles({ zIndex: enteringView.index });
this.add(enteringContent);
this.title
.fadeIn()
.to(TRANSLATEX, CENTER);
this.navbarBg
.to(TRANSLATEX, CENTER);
this.navbarItems
.fadeIn();
if (viewCtrl.enableBack()) {
this.backButton
.before.addClass('show-back-button')
.fadeIn();
}
// set properties depending on direction
if (opts.direction === 'back') {
if (backDirection) {
// back direction
this.content
.from(TRANSLATEX, OFF_LEFT)
.from(OPACITY, OFF_OPACITY)
.to(OPACITY, 1);
this.title
.from(TRANSLATEX, OFF_LEFT);
this.navbarBg
.from(TRANSLATEX, OFF_LEFT);
enteringContent
.fromTo(TRANSLATEX, OFF_LEFT, CENTER)
.fromTo(OPACITY, OFF_OPACITY, 1);
} else {
// forward direction
this.content
.from(TRANSLATEX, '99.5%')
.from(OPACITY, 1);
enteringContent
.fromTo(TRANSLATEX, OFF_RIGHT, CENTER)
.fromTo(OPACITY, 1, 1);
}
this.title
.from(TRANSLATEX, '99.5%');
this.navbarBg
.from(TRANSLATEX, '99.5%')
.fadeIn();
// entering navbar
if (enteringHasNavbar) {
let enteringNavBar = new Animation(enteringView.navbarRef());
enteringNavBar.before.addClass(SHOW_NAVBAR_CSS);
this.add(enteringNavBar);
if (viewCtrl.enableBack()) {
let backBtnText = new Animation(viewCtrl.backBtnTextRef());
backBtnText.fromTo(TRANSLATEX, (300) + 'px', CENTER);
this.navbar.add(backBtnText);
let enteringTitle = new Animation(enteringView.titleRef());
let enteringNavbarItems = new Animation(enteringView.navbarItemRefs());
let enteringNavbarBg = new Animation(enteringView.navbarBgRef());
let enteringBackButton = new Animation(enteringView.backBtnRef());
enteringNavBar
.add(enteringTitle)
.add(enteringNavbarItems)
.add(enteringNavbarBg)
.add(enteringBackButton);
enteringTitle.fadeIn();
enteringNavbarItems.fadeIn();
// set properties depending on direction
if (backDirection) {
// back direction
enteringTitle.fromTo(TRANSLATEX, OFF_LEFT, CENTER);
enteringNavbarBg.fromTo(TRANSLATEX, OFF_LEFT, CENTER);
} else {
// forward direction
enteringTitle.fromTo(TRANSLATEX, OFF_RIGHT, CENTER);
enteringNavbarBg.fromTo(TRANSLATEX, OFF_RIGHT, CENTER);
if (enteringView.enableBack()) {
enteringBackButton.before.addClass(SHOW_BACK_BTN_CSS);
enteringBackButton.fadeIn();
let enteringBackBtnText = new Animation(enteringView.backBtnTextRef());
enteringBackBtnText.fromTo(TRANSLATEX, '150px', '0px');
enteringNavBar.add(enteringBackBtnText);
}
}
}
}
}
// setup leaving view
if (leavingView) {
// leaving content
let leavingContent = new Animation(leavingView.contentRef());
this.add(leavingContent);
leavingContent
.before.addClass(SHOW_VIEW_CSS)
.before.setStyles({ zIndex: leavingView.index })
class IOSLeavingTransition extends ViewTransition {
if (backDirection) {
leavingContent
.fromTo(TRANSLATEX, CENTER, '100%')
.fromTo(OPACITY, 1, 1);
constructor(navCtrl, viewCtrl, opts) {
super(navCtrl, viewCtrl, opts);
} else {
leavingContent
.fromTo(TRANSLATEX, CENTER, OFF_LEFT)
.fromTo(OPACITY, 1, OFF_OPACITY);
}
// leaving viewCtrl moves off screen
this.content
.from(TRANSLATEX, CENTER)
.from(OPACITY, 1)
.before.setStyles({ zIndex: viewCtrl.index })
.after.removeClass('show-view');
if (leavingHasNavbar) {
let leavingNavBar = new Animation(leavingView.navbarRef());
let leavingBackButton = new Animation(leavingView.backBtnRef());
let leavingTitle = new Animation(leavingView.titleRef());
let leavingNavbarItems = new Animation(leavingView.navbarItemRefs());
let leavingNavbarBg = new Animation(leavingView.navbarBgRef());
this.title
.from(TRANSLATEX, CENTER)
.from(OPACITY, 1);
leavingNavBar
.add(leavingBackButton)
.add(leavingTitle)
.add(leavingNavbarItems)
.add(leavingNavbarBg);
this.add(leavingNavBar);
this.navbarBg
.from(TRANSLATEX, CENTER);
leavingBackButton
.after.removeClass(SHOW_BACK_BTN_CSS)
.fadeOut();
this.navbarItems
.fadeOut();
leavingTitle.fadeOut();
leavingNavbarItems.fadeOut();
this.backButton
.after.removeClass('show-back-button')
.fadeOut();
// set properties depending on direction
if (backDirection) {
// back direction
leavingTitle.fromTo(TRANSLATEX, CENTER, '100%');
leavingNavbarBg.fromTo(TRANSLATEX, CENTER, '100%');
this.navbar.after.removeClass('show-navbar');
let leavingBackBtnText = new Animation(leavingView.backBtnTextRef());
leavingBackBtnText.fromTo(TRANSLATEX, CENTER, (300) + 'px');
leavingNavBar.add(leavingBackBtnText);
// set properties depending on direction
if (opts.direction === 'back') {
} else {
// forward direction
leavingTitle.fromTo(TRANSLATEX, CENTER, OFF_LEFT);
}
}
// back direction
this.content
.to(TRANSLATEX, '100%')
.to(OPACITY, 1);
this.title
.to(TRANSLATEX, '100%')
.to(OPACITY, 0);
this.navbarBg
.to(TRANSLATEX, '100%')
.fadeOut();
// if (this.leaving && this.leaving.enableBack() && this.viewWidth() > 200) {
// let leavingBackButtonText = new Animation(this.leaving.backBtnTextRef());
// leavingBackButtonText.fromTo(TRANSLATEX, CENTER, (this.viewWidth() / 2) + 'px');
// this.leavingNavbar.add(leavingBackButtonText);
// }
} else {
// forward direction
this.content
.to(TRANSLATEX, OFF_LEFT)
.to(OPACITY, OFF_OPACITY);
this.title
.to(TRANSLATEX, OFF_LEFT)
.to(OPACITY, 0);
this.navbarBg
.to(TRANSLATEX, OFF_LEFT);
}
}
}