refactor(navCtrl): move async parts of transition into their own fn

This commit is contained in:
Adam Bradley
2015-12-01 19:59:21 -06:00
parent 8889e0a71e
commit 85e773b56b
5 changed files with 269 additions and 154 deletions

View File

@@ -53,9 +53,13 @@ export class IonicApp {
* it will automatically enable the app again. It's basically a fallback incase
* something goes wrong during a transition and the app wasn't re-enabled correctly.
*/
setEnabled(isEnabled, fallback=700) {
this._disTime = (isEnabled ? 0 : Date.now() + fallback);
this._clickBlock.show(!isEnabled, fallback + 100);
setEnabled(isEnabled, duration=700) {
this._disTime = (isEnabled ? 0 : Date.now() + duration);
if (duration > 32 || isEnabled) {
// only do a click block if the duration is longer than XXms
this._clickBlock.show(!isEnabled, duration + 64);
}
}
/**

View File

@@ -9,7 +9,7 @@ import {ViewController} from './view-controller';
import {Animation} from '../../animations/animation';
import {SwipeBackGesture} from './swipe-back';
import {isBoolean, array} from '../../util/util';
import {rafFrames} from '../../util/dom';
import {raf, rafFrames} from '../../util/dom';
/**
* _For examples on the basic usage of NavController, check out the
@@ -101,7 +101,6 @@ import {rafFrames} from '../../util/dom';
export class NavController extends Ion {
/** @internal */
static _tranitionScope: WtfScopeFn = wtfCreateScope('ionic.NavController#_transition()');
static _loadPageScope: WtfScopeFn = wtfCreateScope('ionic.NavController#loadPage()');
static _transCompleteScope: WtfScopeFn = wtfCreateScope('ionic.NavController#_transComplete()');
@@ -133,6 +132,7 @@ export class NavController extends Ion {
this._views = [];
this._trnsTime = 0;
this._trnsDelay = config.get('pageTransitionDelay');
this._sbTrans = null;
this._sbEnabled = config.get('swipeBackEnabled') || false;
@@ -365,7 +365,7 @@ export class NavController extends Ion {
}
// ensure the entering view is shown
this._renderView(viewCtrl, true);
this._cachePage(viewCtrl, true);
let resolve = null;
let promise = new Promise(res => { resolve = res; });
@@ -385,7 +385,7 @@ export class NavController extends Ion {
popView.willUnload();
// only the leaving view should be shown, all others hide
this._renderView(popView, (popView === leavingView));
this._cachePage(popView, (popView === leavingView));
}
}
@@ -592,7 +592,7 @@ export class NavController extends Ion {
if (opts.animate) {
// only the leaving view should be shown, all others hide
this._renderView(popView, (popView === leavingView));
this._cachePage(popView, (popView === leavingView));
}
}
}
@@ -647,16 +647,11 @@ export class NavController extends Ion {
}
/**
*
* @private
* @param {TODO} enteringView TODO
* @param {TODO} leavingView TODO
* @param {TODO} opts TODO
* @param {Function} done TODO
* @returns {any} TODO
*/
_transition(enteringView, leavingView, opts, done) {
if (enteringView === leavingView) {
// if the entering view and leaving view are the same thing don't continue
return done(enteringView);
}
@@ -669,122 +664,245 @@ export class NavController extends Ion {
if (!enteringView) {
// if no entering view then create a bogus one
// already consider this bogus one loaded
enteringView = new ViewController()
enteringView.loaded();
}
console.time('_transition ' + (enteringView.componentType && enteringView.componentType.name));
this._stage(enteringView, opts, () => {
if (enteringView.shouldDestroy) {
// already marked as a view that will be destroyed, don't continue
return done(enteringView);
}
/* Async steps to complete a transition
1. _render: compile the view and render it in the DOM. Load page if it hasn't loaded already. When done call postRender
2. _postRender: Run willEnter/willLeave, then wait a frame (change detection happens), then call beginTransition
3. _beforeTrans: Create the transition's animation, play the animation, wait for it to end
4. _afterTrans: Run didEnter/didLeave, call _transComplete()
5. _transComplete: Cleanup, remove cache views, then call the final callback
*/
this._cd.detectChanges();
// begin the multiple async process of transitioning to the entering view
this._render(enteringView, leavingView, opts, done);
}
this._zone.runOutsideAngular(() => {
this._setZIndex(enteringView, leavingView, opts.direction);
/**
* @private
*/
_render(enteringView, leavingView, opts, done) {
// compile/load the view into the DOM
enteringView.shouldDestroy = false;
enteringView.shouldCache = false;
if (enteringView.shouldDestroy) {
// about to be destroyed, shouldn't continue
done(enteringView);
this._postRender(enteringView, opts, () => {
} else if (enteringView.isLoaded()) {
// already compiled this view, do not load again and continue
this._postRender(enteringView, leavingView, opts, done);
if (!opts.preload) {
enteringView.willEnter();
leavingView.willLeave();
}
// set that the new view pushed on the stack is staged to be entering/leaving
// staged state is important for the transition to find the correct view
enteringView.state = STAGED_ENTERING_STATE;
leavingView.state = STAGED_LEAVING_STATE;
// init the transition animation
opts.renderDelay = opts.transitionDelay || this.config.get('pageTransitionDelay');
let transAnimation = Animation.createTransition(this._getStagedEntering(),
this._getStagedLeaving(),
opts);
if (opts.animate === false) {
// force it to not animate the elements, just apply the "to" styles
transAnimation.clearDuration();
transAnimation.duration(0);
}
let duration = transAnimation.duration();
let enableApp = (duration < 64);
// block any clicks during the transition and provide a
// fallback to remove the clickblock if something goes wrong
this.app.setEnabled(enableApp, duration);
this.setTransitioning(!enableApp, duration);
if (opts.pageType) {
transAnimation.before.addClass(opts.pageType);
}
// start the transition
transAnimation.play(() => {
// transition has completed, update each view's state
enteringView.state = ACTIVE_STATE;
leavingView.state = CACHED_STATE;
// dispose any views that shouldn't stay around
transAnimation.dispose();
if (!opts.preload) {
enteringView.didEnter();
leavingView.didLeave();
}
this._zone.run(() => {
if (this.keyboard.isOpen()) {
this.keyboard.onClose(() => {
this._transComplete();
console.timeEnd('_transition ' + (enteringView.componentType && enteringView.componentType.name));
done(enteringView);
}, 32);
} else {
this._transComplete();
console.timeEnd('_transition ' + (enteringView.componentType && enteringView.componentType.name));
done(enteringView);
}
});
} else {
// view has not been compiled/loaded yet
// continue once the view has finished compiling
// DOM WRITE
this.loadPage(enteringView, null, opts, () => {
if (enteringView.onReady) {
// this entering view needs to wait for it to be ready
// this is used by Tabs to wait for the first page of
// the first selected tab to be loaded
enteringView.onReady(() => {
enteringView.loaded();
this._postRender(enteringView, leavingView, opts, done);
});
});
} else {
enteringView.loaded();
this._postRender(enteringView, leavingView, opts, done);
}
});
}
}
/**
* @private
*/
_postRender(enteringView, leavingView, opts, done) {
// called after _render has completed and the view is compiled/loaded
if (enteringView.shouldDestroy) {
// view already marked as a view that will be destroyed, don't continue
done(enteringView);
} else if (!opts.preload) {
// the enteringView will become the active view, and is not being preloaded
// call each view's lifecycle events
// POSSIBLE DOM READ THEN DOM WRITE
enteringView.willEnter();
leavingView.willLeave();
// set the correct zIndex for the entering and leaving views
// DOM WRITE
this._setZIndex(enteringView, leavingView, opts.direction);
// lifecycle events may have updated some data
// wait one frame and allow the raf to do a change detection
// before kicking off the transition and showing the new view
raf(() => {
this._beforeTrans(enteringView, leavingView, opts, done);
});
} else {
// this view is being preloaded, don't call lifecycle events
// transition does not need to animate
opts.animate = false;
this._beforeTrans(enteringView, leavingView, opts, done);
}
}
/**
* @private
*/
_beforeTrans(enteringView, leavingView, opts, done) {
// called after one raf from postRender()
// create the transitions animation, play the animation
// when the transition ends call wait for it to end
// everything during the transition should runOutsideAngular
this._zone.runOutsideAngular(() => {
// ensure the entering view is not destroyed or cached
enteringView.shouldDestroy = false;
enteringView.shouldCache = false;
// set that the new view pushed on the stack is staged to be entering/leaving
// staged state is important for the transition to find the correct view
enteringView.state = STAGED_ENTERING_STATE;
leavingView.state = STAGED_LEAVING_STATE;
// init the transition animation
opts.renderDelay = opts.transitionDelay || self._trnsDelay;
let transAnimation = Animation.createTransition(enteringView,
leavingView,
opts);
if (opts.animate === false) {
// force it to not animate the elements, just apply the "to" styles
transAnimation.clearDuration();
transAnimation.duration(0);
}
let duration = transAnimation.duration();
let enableApp = (duration < 64);
// block any clicks during the transition and provide a
// fallback to remove the clickblock if something goes wrong
this.app.setEnabled(enableApp, duration);
this.setTransitioning(!enableApp, duration);
if (opts.pageType) {
transAnimation.before.addClass(opts.pageType);
}
// start the transition
transAnimation.play(() => {
// transition animation has ended
// dispose the animation and it's element references
transAnimation.dispose();
this._afterTrans(enteringView, leavingView, opts, done);
});
});
}
/**
* @private
*/
_stage(viewCtrl, opts, done) {
if (viewCtrl.isLoaded() || viewCtrl.shouldDestroy) {
// already compiled this view
return done();
}
_afterTrans(enteringView, leavingView, opts, done) {
// transition has completed, update each view's state
// place back into the zone, run didEnter/didLeave
// call the final callback when done
enteringView.state = ACTIVE_STATE;
leavingView.state = CACHED_STATE;
// get the pane the NavController wants to use
// the pane is where all this content will be placed into
this.loadPage(viewCtrl, null, opts, () => {
if (viewCtrl.onReady) {
viewCtrl.onReady(() => {
viewCtrl.loaded();
done();
});
// run inside of the zone again
this._zone.run(() => {
if (!opts.preload) {
enteringView.didEnter();
leavingView.didLeave();
}
if (this.keyboard.isOpen()) {
// the keyboard is still open!
// no problem, let's just close for them
this.keyboard.onClose(() => {
// keyboard has finished closing, transition complete
this._transComplete();
console.timeEnd('_transition ' + (enteringView.componentType && enteringView.componentType.name));
done(enteringView);
}, 32);
} else {
viewCtrl.loaded();
done();
// all good, transition complete
this._transComplete();
console.timeEnd('_transition ' + (enteringView.componentType && enteringView.componentType.name));
done(enteringView);
}
});
}
/**
* @private
*/
_transComplete() {
this._views.forEach(view => {
if (view) {
if (view.shouldDestroy) {
view.didUnload();
} else if (view.state === CACHED_STATE && view.shouldCache) {
view.shouldCache = false;
}
}
});
// allow clicks again, but still set an enable time
// meaning nothing with this view controller can happen for XXms
this.app.setEnabled(true);
this.setTransitioning(false);
this._sbComplete();
this._cleanup();
}
/**
* @private
*/
_cleanup(activeView) {
// 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 = [];
activeView = activeView || this.getActive();
let previousView = this.getPrevious(activeView);
this._views.forEach(view => {
if (view) {
if (view.shouldDestroy) {
destroys.push(view);
} else if (view.isLoaded()) {
let shouldShow = (view === activeView) || (view === previousView);
this._cachePage(view, shouldShow);
}
}
});
// 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();
});
}
/**
* @private
*/
@@ -852,17 +970,6 @@ export class NavController extends Ion {
});
}
_postRender(enteringView, opts, done) {
enteringView.postRender();
if (opts.animate === false) {
done();
} else {
rafFrames(2, done);
}
}
_setZIndex(enteringView, leavingView, direction) {
let enteringPageRef = enteringView && enteringView.pageRef();
if (enteringPageRef) {
@@ -885,7 +992,7 @@ export class NavController extends Ion {
}
}
_renderView(viewCtrl, shouldShow) {
_cachePage(viewCtrl, shouldShow) {
// using hidden element attribute to display:none and not render views
// renderAttr of '' means the hidden attribute will be added
// renderAttr of null means the hidden attribute will be removed
@@ -935,7 +1042,7 @@ export class NavController extends Ion {
enteringView.willEnter();
// wait for the new view to complete setup
enteringView._stage(enteringView, {}, () => {
this._render(enteringView, {}, () => {
this._zone.runOutsideAngular(() => {
// set that the new view pushed on the stack is staged to be entering/leaving
@@ -1023,7 +1130,6 @@ export class NavController extends Ion {
// all done!
this._transComplete();
});
});
@@ -1142,7 +1248,7 @@ export class NavController extends Ion {
} else if (view.isLoaded()) {
let shouldShow = (view === activeView) || (view === previousView);
this._renderView(view, shouldShow);
this._cachePage(view, shouldShow);
}
}
});

View File

@@ -39,6 +39,7 @@ class MyCmpTest{}
<button ion-item (click)="setPages()">setPages() (Go to PrimaryHeaderPage)</button>
<button ion-item (click)="setRoot()">setRoot(PrimaryHeaderPage) (Go to PrimaryHeaderPage)</button>
<button ion-item (click)="nav.pop()">Pop</button>
<button ion-item (click)="reload()">Reload</button>
<button *ng-for="#i of pages" ion-item (click)="pushPrimaryHeaderPage()">Page {{i}}</button>
</ion-list>
@@ -86,6 +87,10 @@ class FirstPage {
pushAnother() {
this.nav.push(AnotherPage);
}
reload() {
window.location.reload();
}
}
@@ -154,9 +159,15 @@ class FullPage {
})
class PrimaryHeaderPage {
constructor(
nav: NavController
nav: NavController,
viewCtrl: ViewController
) {
this.nav = nav
this.nav = nav;
this.viewCtrl = viewCtrl;
}
onPageWillEnter() {
this.viewCtrl.setBackButtonText('Previous');
}
pushAnother() {

View File

@@ -280,15 +280,6 @@ export class ViewController {
}
}
/**
* @private
*/
postRender() {
// let navbar = this.getNavbar();
// navbar && navbar.postRender();
// ctrlFn(this, 'onPagePostRender');
}
/**
* @private
* The view is about to enter and become the active view.

View File

@@ -13,37 +13,40 @@ import {RippleActivator} from './ripple';
@Injectable()
export class TapClick {
constructor(app: IonicApp, config: Config, zone: NgZone) {
this.app = app;
this.zone = zone;
let self = this;
self.app = app;
self.zone = zone;
this.lastTouch = 0;
this.disableClick = 0;
this.lastActivated = 0;
self.lastTouch = 0;
self.disableClick = 0;
self.lastActivated = 0;
if (config.get('activator') == 'ripple') {
this.activator = new RippleActivator(app, config, zone);
self.activator = new RippleActivator(app, config, zone);
} else if (config.get('activator') == 'highlight') {
this.activator = new Activator(app, config, zone);
self.activator = new Activator(app, config, zone);
}
this.usePolyfill = (config.get('tapPolyfill') === true);
self.usePolyfill = (config.get('tapPolyfill') === true);
zone.runOutsideAngular(() => {
addListener('click', this.click.bind(this), true);
addListener('click', self.click.bind(self), true);
addListener('touchstart', this.touchStart.bind(this));
addListener('touchend', this.touchEnd.bind(this));
addListener('touchcancel', this.pointerCancel.bind(this));
if (self.usePolyfill) {
addListener('touchstart', self.touchStart.bind(self));
addListener('touchend', self.touchEnd.bind(self));
addListener('touchcancel', self.pointerCancel.bind(self));
}
addListener('mousedown', this.mouseDown.bind(this), true);
addListener('mouseup', this.mouseUp.bind(this), true);
addListener('mousedown', self.mouseDown.bind(self), true);
addListener('mouseup', self.mouseUp.bind(self), true);
});
this.pointerMove = function(ev) {
console.log('pointerMove');
if ( hasPointerMoved(POINTER_MOVE_UNTIL_CANCEL, this.startCoord, pointerCoord(ev)) ) {
this.pointerCancel(ev);
self.pointerMove = function(ev) {
if ( hasPointerMoved(POINTER_MOVE_UNTIL_CANCEL, self.startCoord, pointerCoord(ev)) ) {
self.pointerCancel(ev);
}
};
}
@@ -60,7 +63,7 @@ export class TapClick {
let endCoord = pointerCoord(ev);
if (!hasPointerMoved(POINTER_TOLERANCE, this.startCoord, endCoord)) {
console.debug('create click from touch');
console.debug('create click from touch ' + Date.now());
// prevent native mouse click events for XX amount of time
this.disableClick = this.lastTouch + DISABLE_NATIVE_CLICK_AMOUNT;
@@ -78,7 +81,7 @@ export class TapClick {
mouseDown(ev) {
if (this.isDisabledNativeClick()) {
console.debug('mouseDown prevent', ev.target.tagName);
console.debug('mouseDown prevent ' + ev.target.tagName + ' ' + Date.now());
// does not prevent default on purpose
// so native blur events from inputs can happen
ev.stopPropagation();
@@ -90,7 +93,7 @@ export class TapClick {
mouseUp(ev) {
if (this.isDisabledNativeClick()) {
console.debug('mouseUp prevent', ev.target.tagName);
console.debug('mouseUp prevent ' + ev.target.tagName + ' ' + Date.now());
ev.preventDefault();
ev.stopPropagation();
}
@@ -125,21 +128,21 @@ export class TapClick {
}
pointerCancel(ev) {
console.debug('pointerCancel from', ev.type);
console.debug('pointerCancel from ' + ev.type + ' ' + Date.now());
this.activator && this.activator.clearState();
this.moveListeners(false);
}
moveListeners(shouldAdd) {
removeListener(this.usePolyfill ? 'touchmove' : 'mousemove', this.pointerMove);
this.zone.runOutsideAngular(() => {
//this.zone.runOutsideAngular(() => {
if (shouldAdd) {
addListener(this.usePolyfill ? 'touchmove' : 'mousemove', this.pointerMove);
} else {
}
});
//});
}
click(ev) {
@@ -153,7 +156,7 @@ export class TapClick {
}
if (preventReason !== null) {
console.debug('click prevent', preventReason);
console.debug('click prevent ' + preventReason + ' ' + Date.now());
ev.preventDefault();
ev.stopPropagation();
}