From 61bd51367ab257dfb612207dbfe46804f1f7099b Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Sun, 11 Oct 2015 12:47:39 -0500 Subject: [PATCH] improve iOS transitions/reduce dom updates --- ionic/components/nav-bar/nav-bar.scss | 2 +- ionic/components/nav/nav-controller.ts | 208 +++++++++------- ionic/components/nav/nav.ts | 3 + .../nav/test/nav-controller.spec.ts | 23 +- ionic/components/nav/view-controller.ts | 26 +- ionic/transitions/ios-transition.ts | 222 +++++++++--------- 6 files changed, 274 insertions(+), 210 deletions(-) diff --git a/ionic/components/nav-bar/nav-bar.scss b/ionic/components/nav-bar/nav-bar.scss index 5bee678ad8..413cfbdfbd 100644 --- a/ionic/components/nav-bar/nav-bar.scss +++ b/ionic/components/nav-bar/nav-bar.scss @@ -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; diff --git a/ionic/components/nav/nav-controller.ts b/ionic/components/nav/nav-controller.ts index 297b6d448d..e9e7cc89cf 100644 --- a/ionic/components/nav/nav-controller.ts +++ b/ionic/components/nav/nav-controller.ts @@ -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. diff --git a/ionic/components/nav/nav.ts b/ionic/components/nav/nav.ts index 18b17530b7..9320cf1c4b 100644 --- a/ionic/components/nav/nav.ts +++ b/ionic/components/nav/nav.ts @@ -130,6 +130,9 @@ import {NavController} from './nav-controller'; defaultInputs: { 'swipeBackEnabled': true }, + host: { + '[class.has-views]': '_views.length > 0' + }, template: '', directives: [forwardRef(() => NavPaneAnchor)] }) diff --git a/ionic/components/nav/test/nav-controller.spec.ts b/ionic/components/nav/test/nav-controller.spec.ts index e17713f45c..af922523d1 100644 --- a/ionic/components/nav/test/nav-controller.spec.ts +++ b/ionic/components/nav/test/nav-controller.spec.ts @@ -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); }); }); diff --git a/ionic/components/nav/view-controller.ts b/ionic/components/nav/view-controller.ts index a5d5152d65..ef0eb72bfe 100644 --- a/ionic/components/nav/view-controller.ts +++ b/ionic/components/nav/view-controller.ts @@ -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'); + } + } + } diff --git a/ionic/transitions/ios-transition.ts b/ionic/transitions/ios-transition.ts index 5c5e2591d1..a31079d4f4 100644 --- a/ionic/transitions/ios-transition.ts +++ b/ionic/transitions/ios-transition.ts @@ -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); } - } }