diff --git a/tests/app/ui/page/page-tests.android.ts b/tests/app/ui/page/page-tests.android.ts index ded2e561f..d95386d5c 100644 --- a/tests/app/ui/page/page-tests.android.ts +++ b/tests/app/ui/page/page-tests.android.ts @@ -105,6 +105,6 @@ export var test_Resolve_Fragment_ForPage = function () { frame.navigate(pageFactory); TKUnit.waitUntilReady(() => frame.navigationQueueIsEmpty()); - const fragment = frame.android.fragmentForPage(testPage); + const fragment = frame.android.fragmentForPage(frame._currentEntry); TKUnit.assertNotNull(fragment, "Failed to resolve native fragment for page"); } \ No newline at end of file diff --git a/tns-core-modules/application/application.ios.ts b/tns-core-modules/application/application.ios.ts index 939c98d50..e0e039510 100644 --- a/tns-core-modules/application/application.ios.ts +++ b/tns-core-modules/application/application.ios.ts @@ -154,6 +154,11 @@ class IOSApplication implements IOSApplicationDefinition { let ios = utils.ios.getter(UIApplication, UIApplication.sharedApplication); let object = this; notify({ eventName: resumeEvent, object, ios }); + const content = this._window.content; + if (content && !content.isLoaded) { + content.onLoaded(); + } + if (!displayedOnce) { notify({ eventName: displayedEvent, object, ios }); displayedOnce = true; @@ -161,10 +166,18 @@ class IOSApplication implements IOSApplicationDefinition { } private didEnterBackground(notification: NSNotification) { + const content = this._window.content; + if (content && content.isLoaded) { + content.onUnloaded(); + } notify({ eventName: suspendEvent, object: this, ios: utils.ios.getter(UIApplication, UIApplication.sharedApplication) }); } private willTerminate(notification: NSNotification) { + const content = this._window.content; + if (content && content.isLoaded) { + content.onUnloaded(); + } notify({ eventName: exitEvent, object: this, ios: utils.ios.getter(UIApplication, UIApplication.sharedApplication) }); } diff --git a/tns-core-modules/ui/frame/fragment.android.ts b/tns-core-modules/ui/frame/fragment.android.ts index 53826e785..cc6b8f960 100644 --- a/tns-core-modules/ui/frame/fragment.android.ts +++ b/tns-core-modules/ui/frame/fragment.android.ts @@ -19,6 +19,10 @@ class FragmentClass extends android.app.Fragment { return result; } + public onStop(): void { + this._callbacks.onStop(this, super.onStop); + } + public onCreate(savedInstanceState: android.os.Bundle) { if (!this._callbacks) { setFragmentCallbacks(this); diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts index 2d6221844..25651c48d 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -13,27 +13,10 @@ import lazy from "../../utils/lazy"; import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories } from "../../trace"; -// SAME as frame.android.ts!!! Not imported because we don't want cycle reference. -const CALLBACKS = "_callbacks"; - -const sdkVersion = lazy(() => parseInt(device.sdkVersion)); -const intEvaluator = lazy(() => new android.animation.IntEvaluator()); -const defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator()); - -const waitingQueue = new Set(); - interface TransitionListener { new(entry: ExpandedEntry, transition: android.transition.Transition): ExpandedTransitionListener; } -let TransitionListener: TransitionListener; -let AnimationListener: android.animation.Animator.AnimatorListener; -let loadAnimatorMethod: java.lang.reflect.Method; -let reflectionDone: boolean; -let defaultEnterAnimatorStatic: android.animation.Animator; -let defaultExitAnimatorStatic: android.animation.Animator; -let fragmentCompleted: android.app.Fragment; - interface ExpandedAnimator extends android.animation.Animator { entry: ExpandedEntry; transitionType?: string; @@ -55,8 +38,12 @@ interface ExpandedEntry extends BackstackEntry { popEnterAnimator: ExpandedAnimator; popExitAnimator: ExpandedAnimator; + defaultEnterAnimator: ExpandedAnimator; + defaultExitAnimator: ExpandedAnimator; + transition: Transition; transitionName: string; + frameId: number } interface FragmentCallbacks { @@ -64,18 +51,38 @@ interface FragmentCallbacks { entry: ExpandedEntry; } +const sdkVersion = lazy(() => parseInt(device.sdkVersion)); +const intEvaluator = lazy(() => new android.animation.IntEvaluator()); +const defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator()); + +export const waitingQueue = new Map>(); +export const completedEntries = new Map(); + +let TransitionListener: TransitionListener; +let AnimationListener: android.animation.Animator.AnimatorListener; +let loadAnimatorMethod: java.lang.reflect.Method; +let reflectionDone: boolean; +let defaultEnterAnimatorStatic: android.animation.Animator; +let defaultExitAnimatorStatic: android.animation.Animator; + export function _setAndroidFragmentTransitions( animated: boolean, navigationTransition: NavigationTransition, - currentFragment: android.app.Fragment, - newFragment: android.app.Fragment, + currentEntry: ExpandedEntry, + newEntry: ExpandedEntry, fragmentTransaction: android.app.FragmentTransaction, - manager: android.app.FragmentManager): void { + manager: android.app.FragmentManager, + frameId: number): void { - if (waitingQueue.size > 0) { - throw new Error('Calling navigation before previous queue completes.'); + const currentFragment: android.app.Fragment = currentEntry ? currentEntry.fragment : null; + const newFragment: android.app.Fragment = newEntry.fragment; + const entries = waitingQueue.get(frameId); + if (entries && entries.size > 0) { + throw new Error('Calling navigation before previous navigation finish.'); } + initDefaultAnimations(manager); + if (sdkVersion() >= 21) { allowTransitionOverlap(currentFragment); allowTransitionOverlap(newFragment); @@ -101,9 +108,6 @@ export function _setAndroidFragmentTransitions( name = 'default'; } - const callbacks = getFragmentCallbacks(newFragment); - const newEntry = callbacks.entry; - const currentEntry = currentFragment ? getFragmentCallbacks(currentFragment).entry : null; let currentFragmentNeedsDifferentAnimation = false; if (currentEntry) { _updateTransitions(currentEntry); @@ -117,7 +121,6 @@ export function _setAndroidFragmentTransitions( if (name === 'none') { transition = new NoTransition(0, null); } else if (name === 'default') { - initDefaultAnimations(manager); transition = new DefaultTransition(0, null); } else if (useLollipopTransition) { // setEnterTransition: Enter @@ -172,24 +175,50 @@ export function _setAndroidFragmentTransitions( } } + initDefaultAnimations(manager); + setupDefaultAnimations(newEntry, new DefaultTransition(0, null)); + printTransitions(currentEntry); printTransitions(newEntry); } -export function _onFragmentCreateAnimator(fragment: android.app.Fragment, nextAnim: number): android.animation.Animator { - const entry = getFragmentCallbacks(fragment).entry; +export function _onFragmentCreateAnimator(entry: ExpandedEntry, fragment: android.app.Fragment, nextAnim: number, enter: boolean): android.animation.Animator { + let animator: android.animation.Animator; switch (nextAnim) { case AnimationType.enterFakeResourceId: - return entry.enterAnimator; + animator = entry.enterAnimator; + break; + case AnimationType.exitFakeResourceId: - return entry.exitAnimator; + animator = entry.exitAnimator; + break; + case AnimationType.popEnterFakeResourceId: - return entry.popEnterAnimator; + animator = entry.popEnterAnimator; + break; + case AnimationType.popExitFakeResourceId: - return entry.popExitAnimator; + animator = entry.popExitAnimator; + break; } - return null; + if (!animator && sdkVersion() >= 21) { + const view = fragment.getView(); + const jsParent = entry.resolvedPage.parent; + const parent = view.getParent() || (jsParent && jsParent.nativeViewProtected); + const animatedEntries = _getAnimatedEntries(entry.frameId); + if (!animatedEntries || !animatedEntries.has(entry)) { + if (parent && !(parent).isLaidOut()) { + animator = enter ? entry.defaultEnterAnimator : entry.defaultExitAnimator; + } + } + } + + return animator; +} + +export function _getAnimatedEntries(frameId: number): Set { + return waitingQueue.get(frameId); } export function _updateTransitions(entry: ExpandedEntry): void { @@ -241,10 +270,6 @@ export function _reverseTransitions(previousEntry: ExpandedEntry, currentEntry: return transitionUsed; } -function getFragmentCallbacks(fragment: android.app.Fragment): FragmentCallbacks { - return fragment[CALLBACKS] as FragmentCallbacks; -} - // Transition listener can't be static because // android is cloning transitions and we can't expand them :( function getTransitionListener(entry: ExpandedEntry, transition: android.transition.Transition): ExpandedTransitionListener { @@ -257,40 +282,38 @@ function getTransitionListener(entry: ExpandedEntry, transition: android.transit } public onTransitionStart(transition: android.transition.Transition): void { - const fragment = this.entry.fragment; - waitingQueue.add(fragment); + const entry = this.entry; + addToWaitingQueue(entry); if (traceEnabled()) { - traceWrite(`START ${toShortString(transition)} transition for ${fragment}`, traceCategories.Transition); + traceWrite(`START ${toShortString(transition)} transition for ${entry.fragmentTag}`, traceCategories.Transition); } } onTransitionEnd(transition: android.transition.Transition): void { - const fragment = this.entry.fragment; + const entry = this.entry; if (traceEnabled()) { - traceWrite(`END ${toShortString(transition)} transition for ${fragment}`, traceCategories.Transition); + traceWrite(`END ${toShortString(transition)} transition for ${entry.fragmentTag}`, traceCategories.Transition); } - transitionOrAnimationCompleted(fragment); + transitionOrAnimationCompleted(entry); } onTransitionResume(transition: android.transition.Transition): void { if (traceEnabled()) { - const fragment = this.entry.fragment; + const fragment = this.entry.fragmentTag; traceWrite(`RESUME ${toShortString(transition)} transition for ${fragment}`, traceCategories.Transition); } } onTransitionPause(transition: android.transition.Transition): void { if (traceEnabled()) { - const fragment = this.entry.fragment; - traceWrite(`PAUSE ${toShortString(transition)} transition for ${fragment}`, traceCategories.Transition); + traceWrite(`PAUSE ${toShortString(transition)} transition for ${this.entry.fragmentTag}`, traceCategories.Transition); } } onTransitionCancel(transition: android.transition.Transition): void { if (traceEnabled()) { - const fragment = this.entry.fragment; - traceWrite(`CANCEL ${toShortString(transition)} transition for ${fragment}`, traceCategories.Transition); + traceWrite(`CANCEL ${toShortString(transition)} transition for ${this.entry.fragmentTag}`, traceCategories.Transition); } } } @@ -311,30 +334,30 @@ function getAnimationListener(): android.animation.Animator.IAnimatorListener { } onAnimationStart(animator: ExpandedAnimator): void { - const fragment = animator.entry.fragment; - waitingQueue.add(fragment); + const entry = animator.entry; + addToWaitingQueue(entry); if (traceEnabled()) { - traceWrite(`START ${animator.transitionType} for ${fragment}`, traceCategories.Transition); + traceWrite(`START ${animator.transitionType} for ${entry.fragmentTag}`, traceCategories.Transition); } } onAnimationRepeat(animator: ExpandedAnimator): void { if (traceEnabled()) { - traceWrite(`REPEAT ${animator.transitionType} for ${animator.entry.fragment}`, traceCategories.Transition); + traceWrite(`REPEAT ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); } } onAnimationEnd(animator: ExpandedAnimator): void { if (traceEnabled()) { - traceWrite(`END ${animator.transitionType} for ${animator.entry.fragment}`, traceCategories.Transition); + traceWrite(`END ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); } - transitionOrAnimationCompleted(animator.entry.fragment); + transitionOrAnimationCompleted(animator.entry); } onAnimationCancel(animator: ExpandedAnimator): void { if (traceEnabled()) { - traceWrite(`CANCEL ${animator.transitionType} for ${animator.entry.fragment}`, traceCategories.Transition); + traceWrite(`CANCEL ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); } } } @@ -345,6 +368,17 @@ function getAnimationListener(): android.animation.Animator.IAnimatorListener { return AnimationListener; } +function addToWaitingQueue(entry: ExpandedEntry): void { + const frameId = entry.frameId; + let entries = waitingQueue.get(frameId); + if (!entries) { + entries = new Set(); + waitingQueue.set(frameId, entries); + } + + entries.add(entry); +} + function clearAnimationListener(animator: ExpandedAnimator, listener: android.animation.Animator.IAnimatorListener): void { if (!animator) { return; @@ -352,10 +386,9 @@ function clearAnimationListener(animator: ExpandedAnimator, listener: android.an animator.removeListener(listener); - const entry = animator.entry; - const fragment = entry.fragment; if (traceEnabled()) { - traceWrite(`Clear ${animator.transitionType} - ${entry.transition} for ${fragment}`, traceCategories.Transition); + const entry = animator.entry; + traceWrite(`Clear ${animator.transitionType} - ${entry.transition} for ${entry.fragmentTag}`, traceCategories.Transition); } animator.entry = null; @@ -403,8 +436,9 @@ function clearExitAndReenterTransitions(entry: ExpandedEntry, removeListener: bo } } } -export function _clearFragment(fragment: android.app.Fragment): void { - clearEntry(getFragmentCallbacks(fragment).entry, false); + +export function _clearFragment(entry: ExpandedEntry): void { + clearEntry(entry, false); } export function _clearEntry(entry: ExpandedEntry): void { @@ -633,6 +667,22 @@ function setupAllAnimation(entry: ExpandedEntry, transition: Transition): void { entry.popExitAnimator = popExitAnimator; } +function setupDefaultAnimations(entry: ExpandedEntry, transition: Transition): void { + const listener = getAnimationListener(); + + const enterAnimator = transition.createAndroidAnimator(AndroidTransitionType.enter); + enterAnimator.transitionType = AndroidTransitionType.enter; + enterAnimator.entry = entry; + enterAnimator.addListener(listener); + entry.defaultEnterAnimator = enterAnimator; + + const exitAnimator = transition.createAndroidAnimator(AndroidTransitionType.exit); + exitAnimator.transitionType = AndroidTransitionType.exit; + exitAnimator.entry = entry; + exitAnimator.addListener(listener); + entry.defaultExitAnimator = exitAnimator; +} + function setUpNativeTransition(navigationTransition: NavigationTransition, nativeTransition: android.transition.Transition) { if (navigationTransition.duration) { nativeTransition.setDuration(navigationTransition.duration); @@ -648,22 +698,28 @@ function addNativeTransitionListener(entry: ExpandedEntry, nativeTransition: and return listener; } -function transitionOrAnimationCompleted(fragment: android.app.Fragment): void { - waitingQueue.delete(fragment); - if (waitingQueue.size === 0) { - const callbacks = getFragmentCallbacks(fragment); - const entry = callbacks.entry; - const frame = callbacks.frame; - const setAsCurrent = frame.isCurrent(entry) ? fragmentCompleted : fragment; +function transitionOrAnimationCompleted(entry: ExpandedEntry): void { + const frameId = entry.frameId; + const entries = waitingQueue.get(frameId); + entries.delete(entry); + if (entries.size === 0) { + const frame = entry.resolvedPage.frame; + // We have 0 or 1 entry per frameId in completedEntries + // So there is no need to make it to Set like waitingQueue + const previousCompletedAnimationEntry = completedEntries.get(frameId); + completedEntries.delete(frameId); + waitingQueue.delete(frameId); - fragmentCompleted = null; + let current = frame.isCurrent(entry) ? previousCompletedAnimationEntry : entry; + current = current || entry; // Will be null if Frame is shown modally... // AnimationCompleted fires again (probably bug in android). - if (setAsCurrent) { - setTimeout(() => frame.setCurrent(getFragmentCallbacks(setAsCurrent).entry)); + if (current) { + const isBack = frame._isBack; + setTimeout(() => frame.setCurrent(current, isBack)); } } else { - fragmentCompleted = fragment; + completedEntries.set(frameId, entry); } } @@ -719,8 +775,7 @@ function createDummyZeroDurationAnimator(): android.animation.Animator { function printTransitions(entry: ExpandedEntry) { if (entry && traceEnabled()) { - const fragment = entry.fragment; - let result = `${fragment} Transitions:`; + let result = `${entry.fragmentTag} Transitions:`; if (entry.transitionName) { result += `transitionName=${entry.transitionName}, `; } @@ -732,6 +787,7 @@ function printTransitions(entry: ExpandedEntry) { result += `popExitAnimator=${entry.popExitAnimator}, `; } if (sdkVersion() >= 21) { + const fragment = entry.fragment; result += `${fragment.getEnterTransition() ? " enter=" + toShortString(fragment.getEnterTransition()) : ""}`; result += `${fragment.getExitTransition() ? " exit=" + toShortString(fragment.getExitTransition()) : ""}`; result += `${fragment.getReenterTransition() ? " popEnter=" + toShortString(fragment.getReenterTransition()) : ""}`; diff --git a/tns-core-modules/ui/frame/fragment.transitions.d.ts b/tns-core-modules/ui/frame/fragment.transitions.d.ts index 7a715adcb..4d8709fb7 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.d.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.d.ts @@ -21,14 +21,19 @@ export const enum AnimationType { export function _setAndroidFragmentTransitions( animated: boolean, navigationTransition: NavigationTransition, - currentFragment: any, - newFragment: any, + currentEntry: BackstackEntry, + newEntry: BackstackEntry, fragmentTransaction: any, - manager: any /* android.app.FragmentManager */): void; + manager: any /* android.app.FragmentManager */, + frameId: number): void; /** * @private */ -export function _onFragmentCreateAnimator(fragment: any, nextAnim: number): any; +export function _onFragmentCreateAnimator(entry: BackstackEntry, fragment: any, nextAnim: number, enter: boolean): any; +/** + * @private + */ +export function _getAnimatedEntries(frameId: number): Set; /** * @private * Called once fragment is recreated after it was destroyed. @@ -54,9 +59,9 @@ export function _clearEntry(entry: BackstackEntry): void; * Removes all animations and transitions but keeps them on the entry * in order to reapply them when new fragment is created for the same entry. */ -export function _clearFragment(fragment: any): void; +export function _clearFragment(entry: BackstackEntry): void; /** * @private */ export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: any, operation: number, fromVC: any, toVC: any): any; -//@endprivate \ No newline at end of file +//@endprivate diff --git a/tns-core-modules/ui/frame/frame-common.ts b/tns-core-modules/ui/frame/frame-common.ts index a4a340779..155bdd225 100644 --- a/tns-core-modules/ui/frame/frame-common.ts +++ b/tns-core-modules/ui/frame/frame-common.ts @@ -165,38 +165,89 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { public static androidOptionSelectedEvent = "optionSelected"; private _animated: boolean; - public _currentEntry: BackstackEntry; private _transition: NavigationTransition; private _backStack = new Array(); private _navigationQueue = new Array(); + public _currentEntry: BackstackEntry; + public _executingEntry: BackstackEntry; public _isInFrameStack = false; public static defaultAnimatedNavigation = true; public static defaultTransition: NavigationTransition; // TODO: Currently our navigation will not be synchronized in case users directly call native navigation methods like Activity.startActivity. + public _addChildFromBuilder(name: string, value: any) { + if (value instanceof Page) { + this.navigate({ create: () => value }); + } + } + + @profile + public onLoaded() { + super.onLoaded(); + this._processNextNavigationEntry(); + } + public canGoBack(): boolean { - return this._backStack.length > 0; + let backstack = this._backStack.length; + let previousForwardNotInBackstack = false; + this._navigationQueue.forEach(item => { + const entry = item.entry; + if (item.isBackNavigation) { + previousForwardNotInBackstack = false; + if (!entry) { + backstack--; + } else { + const backstackIndex = this._backStack.indexOf(entry); + if (backstackIndex !== -1) { + backstack = backstackIndex; + } else { + // NOTE: We don't search for entries in navigationQueue because there is no way for + // developer to get reference to BackstackEntry unless transition is completed. + // At that point the entry is put in the backstack array. + // If we start to return Backstack entry from navigate method then + // here we should check also navigationQueue as well. + backstack--; + } + } + } else if (entry.entry.clearHistory) { + previousForwardNotInBackstack = false; + backstack = 0; + } else { + backstack++; + if (previousForwardNotInBackstack) { + backstack--; + } + + previousForwardNotInBackstack = entry.entry.backstackVisible === false; + } + }); + + // this is our first navigation which is not completed yet. + if (this._navigationQueue.length > 0 && !this._currentEntry) { + backstack--; + } + + return backstack > 0; } /** * Navigates to the previous entry (if any) in the back stack. * @param to The backstack entry to navigate back to. */ - public goBack(backstackEntry?: BackstackEntry) { + public goBack(backstackEntry?: BackstackEntry): void { if (traceEnabled()) { traceWrite(`GO BACK`, traceCategories.Navigation); } if (!this.canGoBack()) { - // TODO: Do we need to throw an error? return; } if (backstackEntry) { - const backIndex = this._backStack.indexOf(backstackEntry); - if (backIndex < 0) { + const index = this._backStack.indexOf(backstackEntry); + if (index < 0) { return; } } @@ -207,19 +258,18 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { } this._navigationQueue.push(navigationContext); - - if (this._navigationQueue.length === 1) { - this._processNavigationContext(navigationContext); - } - else { - if (traceEnabled()) { - traceWrite(`Going back scheduled`, traceCategories.Navigation); - } - } + this._processNextNavigationEntry(); } - public _removeBackstackEntries(removed: BackstackEntry[]): void { - // Handled in android. + public _removeEntry(removed: BackstackEntry): void { + const page = removed.resolvedPage; + const frame = page.frame; + (page)._frame = null; + if (frame) { + frame._removeView(page); + } else { + page._tearDownUI(true); + } } // Attempts to implement https://github.com/NativeScript/NativeScript/issues/1311 @@ -249,7 +299,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { } const entry = buildEntryFromArgs(param); - const page = resolvePageFromEntry(entry); + const page = resolvePageFromEntry(entry) as Page; // Attempts to implement https://github.com/NativeScript/NativeScript/issues/1311 // if (page["isBiOrientational"] && entry.moduleName && !this._subscribedToOrientationChangedEvent){ @@ -276,22 +326,59 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { } this._navigationQueue.push(navigationContext); - - if (this._navigationQueue.length === 1) { - this._processNavigationContext(navigationContext); - } else { - if (traceEnabled()) { - traceWrite(`Navigation scheduled`, traceCategories.Navigation); - } - } + this._processNextNavigationEntry(); } public isCurrent(entry: BackstackEntry): boolean { return this._currentEntry === entry; } - public setCurrent(entry: BackstackEntry): void { + public setCurrent(entry: BackstackEntry, isBack: boolean): void { + const newPage = entry.resolvedPage; + // In case we navigated forward to a page that was in the backstack + // with clearHistory: true + if (!newPage.frame) { + this._addView(newPage); + (newPage)._frame = this; + } + this._currentEntry = entry; + this._executingEntry = null; + newPage.onNavigatedTo(isBack); + } + + public _updateBackstack(entry: BackstackEntry, isBack: boolean): void { + this.raiseCurrentPageNavigatedEvents(isBack); + const current = this._currentEntry; + + if (isBack) { + const index = this._backStack.indexOf(entry); + this._backStack.splice(index + 1).forEach(e => this._removeEntry(e)); + this._backStack.pop(); + } else { + if (entry.entry.clearHistory) { + this._backStack.forEach(e => this._removeEntry(e)); + this._backStack.length = 0; + } else if (FrameBase._isEntryBackstackVisible(current)) { + this._backStack.push(current); + } + } + + if (current && this._backStack.indexOf(current) < 0) { + this._removeEntry(current); + } + } + + private raiseCurrentPageNavigatedEvents(isBack: boolean) { + const page = this.currentPage; + if (page) { + if (page.isLoaded) { + // Forward navigation does not remove page from frame so we raise unloaded manually. + page.onUnloaded(); + } + + page.onNavigatedFrom(isBack); + } } public _processNavigationQueue(page: Page) { @@ -300,8 +387,8 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { return; } - let entry = this._navigationQueue[0].entry; - let currentNavigationPage = entry.resolvedPage; + const entry = this._navigationQueue[0].entry; + const currentNavigationPage = entry.resolvedPage; if (page !== currentNavigationPage) { // If the page is not the one that requested navigation - skip it. return; @@ -309,12 +396,7 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { // remove completed operation. this._navigationQueue.shift(); - - if (this._navigationQueue.length > 0) { - let navigationContext = this._navigationQueue[0]; - this._processNavigationContext(navigationContext); - } - + this._processNextNavigationEntry(); this._updateActionBar(); } @@ -353,29 +435,25 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { //traceWrite("calling _updateActionBar on Frame", traceCategories.Navigation); } - protected _processNavigationContext(navigationContext: NavigationContext) { - if (navigationContext.isBackNavigation) { - this.performGoBack(navigationContext); - } else { - this.performNavigation(navigationContext); + protected _processNextNavigationEntry() { + if (!this.isLoaded || this._executingEntry) { + return; } - } - public _clearBackStack(): void { - this._backStack.length = 0; + if (this._navigationQueue.length > 0) { + const navigationContext = this._navigationQueue[0]; + if (navigationContext.isBackNavigation) { + this.performGoBack(navigationContext); + } else { + this.performNavigation(navigationContext); + } + } } @profile private performNavigation(navigationContext: NavigationContext) { - let navContext = navigationContext.entry; - - // TODO: This should happen once navigation is completed. - if (navigationContext.entry.entry.clearHistory) { - // Don't clear backstack immediately or we can't remove pages from frame. - } else if (FrameBase._isEntryBackstackVisible(this._currentEntry)) { - this._backStack.push(this._currentEntry); - } - + const navContext = navigationContext.entry; + this._executingEntry = navContext; this._onNavigatingTo(navContext, navigationContext.isBackNavigation); this._navigateCore(navContext); } @@ -383,16 +461,13 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { @profile private performGoBack(navigationContext: NavigationContext) { let backstackEntry = navigationContext.entry; + const backstack = this._backStack; if (!backstackEntry) { - backstackEntry = this._backStack.pop(); + backstackEntry = backstack[backstack.length - 1]; navigationContext.entry = backstackEntry; - } else { - const index = this._backStack.indexOf(backstackEntry); - const removed = this._backStack.splice(index + 1); - this._backStack.pop(); - this._removeBackstackEntries(removed); } - + + this._executingEntry = backstackEntry; this._onNavigatingTo(backstackEntry, true); this._goBackCore(backstackEntry); } @@ -485,8 +560,9 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { } public eachChildView(callback: (child: View) => boolean) { - if (this.currentPage) { - callback(this.currentPage); + const page = this.currentPage; + if (page) { + callback(page); } } @@ -588,7 +664,7 @@ export function topmost(): FrameBase { export function goBack(): boolean { const top = topmost(); - if (top.canGoBack()) { + if (top && top.canGoBack()) { top.goBack(); return true; } diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index d039fae75..a9111ca8a 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -1,16 +1,19 @@ // Definitions. import { - AndroidFrame as AndroidFrameDefinition, BackstackEntry, + AndroidFrame as AndroidFrameDefinition, BackstackEntry, NavigationEntry, NavigationTransition, AndroidFragmentCallbacks, AndroidActivityCallbacks } from "."; import { Page } from "../page"; // Types. import * as application from "../../application"; -import { FrameBase, NavigationContext, stack, goBack, View, Observable, traceEnabled, traceWrite, traceCategories } from "./frame-common"; -import { DIALOG_FRAGMENT_TAG } from "../page/constants"; import { - _setAndroidFragmentTransitions, _onFragmentCreateAnimator, + FrameBase, NavigationContext, stack, goBack, View, Observable, topmost, + traceEnabled, traceWrite, traceCategories +} from "./frame-common"; + +import { + _setAndroidFragmentTransitions, _onFragmentCreateAnimator, _getAnimatedEntries, _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, AnimationType } from "./fragment.transitions"; @@ -21,9 +24,10 @@ export * from "./frame-common"; const INTENT_EXTRA = "com.tns.activity"; const FRAMEID = "_frameId"; const CALLBACKS = "_callbacks"; + let navDepth = -1; let fragmentId = -1; -let activityInitialized: boolean; +export let moduleLoaded: boolean; if (global && global.__inspector) { const devtools = require("tns-core-modules/debugger/devtools-elements"); @@ -35,6 +39,7 @@ export class Frame extends FrameBase { private _android: AndroidFrame; private _delayedNavigationEntry: BackstackEntry; private _containerViewId: number = -1; + private _tearDownPending = false; public _isBack: boolean = true; constructor() { @@ -64,9 +69,57 @@ export class Frame extends FrameBase { return this._android; } + public _getFragmentManager(): android.app.FragmentManager { + const activity = this._android.activity; + return activity && activity.getFragmentManager(); + } + + protected _processNextNavigationEntry(): void { + // In case activity was destroyed because of back button pressed (e.g. app exit) + // and application is restored from recent apps, current fragment isn't recreated. + // In this case call _navigateCore in order to recreate the current fragment. + // Don't call navigate because it will fire navigation events. + // As JS instances are alive it is already done for the current page. + if (!this.isLoaded) { + return; + } + + const animatedEntries = _getAnimatedEntries(this._android.frameId); + if (animatedEntries) { + // // recreate UI on the animated fragments because we have new context. + // // We need to recreate the UI because it Frame will do it only for currentPage. + // // Once currentPage is changed due to transition end we will have no UI on the + // // new Page. + // animatedEntries.forEach(entry => { + // const page = entry.resolvedPage; + // if (page._context !== this._context) { + // page._tearDownUI(true); + // page._setupUI(this._context); + // } + // }); + + // Wait until animations are completed. + if (animatedEntries.size > 0) { + return; + } + } + + const manager = this._getFragmentManager(); + const entry = this._currentEntry; + if (entry && manager && !manager.findFragmentByTag(entry.fragmentTag)) { + // Simulate first navigation (e.g. no animations or transitions) + this._currentEntry = null; + // NavigateCore will eventually call _processNextNavigationEntry again. + this._navigateCore(entry); + this._currentEntry = entry; + } else { + super._processNextNavigationEntry(); + } + } + private createFragment(backstackEntry: BackstackEntry, fragmentTag: string): android.app.Fragment { ensureFragmentClass(); - const newFragment: android.app.Fragment = new fragmentClass(); + const newFragment = new fragmentClass(); const args = new android.os.Bundle(); args.putInt(FRAMEID, this._android.frameId); newFragment.setArguments(args); @@ -84,42 +137,72 @@ export class Frame extends FrameBase { return newFragment; } - public setCurrent(entry: BackstackEntry): void { - this.changeCurrentPage(entry); - this._currentEntry = entry; - this._isBack = true; - this._processNavigationQueue(entry.resolvedPage); - } - - private changeCurrentPage(entry: BackstackEntry) { - const isBack = this._isBack; - let page: Page = this.currentPage; - if (page) { - if (page.frame === this && - (isBack || this.backStack.indexOf(this._currentEntry) < 0)) { - // If going back or navigate forward but current entry is not backstack visible. - removeEntry(this._currentEntry, page, this, true); - } - - if (page.isLoaded) { - // Forward navigation does not remove page from frame so we raise unloaded manually. - page.onUnloaded(); - } - - page.onNavigatedFrom(isBack); + public setCurrent(entry: BackstackEntry, isBack: boolean): void { + const current = this._currentEntry; + const currentEntryChanged = current !== entry; + if (currentEntryChanged) { + this._updateBackstack(entry, isBack); } - const newPage = entry.resolvedPage; - newPage._fragmentTag = entry.fragmentTag; - this._currentEntry = entry; - newPage.onNavigatedTo(isBack); + if (currentEntryChanged) { + // If activity was destroyed we need to destroy fragment and UI + // of current and new entries. + if (this._tearDownPending) { + this._tearDownPending = false; + if (!entry.recreated) { + clearEntry(entry); + } + + if (current && !current.recreated) { + clearEntry(current); + } + + // If we have context activity was recreated. Create new fragment + // and UI for the new current page. + const context = this._context; + if (context && !entry.recreated) { + entry.fragment = this.createFragment(entry, entry.fragmentTag); + entry.resolvedPage._setupUI(context); + } + + entry.recreated = false; + current.recreated = false; + } + + super.setCurrent(entry, isBack); + + // If we had real navigation process queue. + this._processNavigationQueue(entry.resolvedPage); + } else { + // Otherwise currentPage was recreated so this wasn't real navigation. + // Continue with next item in the queue. + this._processNextNavigationEntry(); + } + } + + public _onBackPressed(): boolean { + if (this.canGoBack()) { + this.goBack(); + return true; + } else if (!this.navigationQueueIsEmpty()) { + const manager = this._getFragmentManager(); + if (manager) { + manager.executePendingTransactions(); + return true; + } + } + + return false; } @profile - public _navigateCore(backstackEntry: BackstackEntry) { - super._navigateCore(backstackEntry); + public _navigateCore(newEntry: BackstackEntry) { + super._navigateCore(newEntry); this._isBack = false; + // set frameId here so that we could use it in fragment.transitions + newEntry.frameId = this._android.frameId; + const activity = this._android.activity; if (!activity) { // Activity not associated. In this case we have two execution paths: @@ -130,15 +213,13 @@ export class Frame extends FrameBase { startActivity(currentActivity, this._android.frameId); } - this._delayedNavigationEntry = backstackEntry; + this._delayedNavigationEntry = newEntry; return; } - const manager = activity.getFragmentManager(); - - // Current Fragment - const currentFragment = this._currentEntry ? manager.findFragmentByTag(this._currentEntry.fragmentTag) : null; - const clearHistory = backstackEntry.entry.clearHistory; + const manager: android.app.FragmentManager = this._getFragmentManager(); + const clearHistory = newEntry.entry.clearHistory; + const currentEntry = this._currentEntry; // New Fragment if (clearHistory) { @@ -148,22 +229,21 @@ export class Frame extends FrameBase { navDepth++; fragmentId++; const newFragmentTag = `fragment${fragmentId}[${navDepth}]`; - const newFragment = this.createFragment(backstackEntry, newFragmentTag); + const newFragment = this.createFragment(newEntry, newFragmentTag); const transaction = manager.beginTransaction(); - const animated = this._getIsAnimatedNavigation(backstackEntry.entry); + const animated = this._getIsAnimatedNavigation(newEntry.entry); // NOTE: Don't use transition for the initial nagivation (same as on iOS) // On API 21+ transition won't be triggered unless there was at least one // layout pass so we will wait forever for transitionCompleted handler... // https://github.com/NativeScript/NativeScript/issues/4895 - const navigationTransition = this._currentEntry ? this._getNavigationTransition(backstackEntry.entry) : null; + const navigationTransition = this._currentEntry ? this._getNavigationTransition(newEntry.entry) : null; - _setAndroidFragmentTransitions(animated, navigationTransition, currentFragment, newFragment, transaction, manager); - if (clearHistory) { - destroyPages(this.backStack, true); - this._clearBackStack(); - } + _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, transaction, manager, this._android.frameId); + // if (clearHistory) { + // deleteEntries(this.backStack); + // } - if (currentFragment && animated && !navigationTransition) { + if (currentEntry && animated && !navigationTransition) { transaction.setTransition(android.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN); } @@ -172,11 +252,11 @@ export class Frame extends FrameBase { } public _goBackCore(backstackEntry: BackstackEntry) { + this._isBack = true; super._goBackCore(backstackEntry); navDepth = backstackEntry.navDepth; - const activity = this._android.activity; - const manager = activity.getFragmentManager(); + const manager: android.app.FragmentManager = this._getFragmentManager(); const transaction = manager.beginTransaction(); if (!backstackEntry.fragment) { @@ -198,26 +278,46 @@ export class Frame extends FrameBase { transaction.commit(); } - public _removeBackstackEntries(removed: BackstackEntry[]): void { - destroyPages(removed, true); + public _removeEntry(removed: BackstackEntry): void { + super._removeEntry(removed); + + if (removed.fragment) { + _clearEntry(removed); + } + + removed.fragment = null; + removed.viewSavedState = null; } public createNativeView() { - const root = new org.nativescript.widgets.ContentLayout(this._context); - if (this._containerViewId < 0) { - this._containerViewId = android.view.View.generateViewId(); - } - return root; + return new org.nativescript.widgets.ContentLayout(this._context); } public initNativeView(): void { super.initNativeView(); this._android.rootViewGroup = this.nativeViewProtected; + if (this._containerViewId < 0) { + this._containerViewId = android.view.View.generateViewId(); + } this._android.rootViewGroup.setId(this._containerViewId); } public disposeNativeView() { - // we should keep the reference to underlying native object, since frame can contain many pages. + this._tearDownPending = !!this._executingEntry; + const current = this._currentEntry; + + this.backStack.forEach(entry => { + // Don't destroy current and executing entries or UI will look blank. + // We will do it in setCurrent. + if (entry !== this._executingEntry) { + clearEntry(entry); + } + }); + + if (current && !this._executingEntry) { + clearEntry(current); + } + this._android.rootViewGroup = null; super.disposeNativeView(); } @@ -233,20 +333,6 @@ export class Frame extends FrameBase { } } - public _printNativeBackStack() { - if (!this._android.activity) { - return; - } - const manager = this._android.activity.getFragmentManager(); - const length = manager.getBackStackEntryCount(); - let i = length - 1; - console.log(`Fragment Manager Back Stack: `); - while (i >= 0) { - const fragment = manager.findFragmentByTag(manager.getBackStackEntryAt(i--).getName()); - console.log(`\t${fragment}`); - } - } - public _getNavBarVisible(page: Page): boolean { if (page.actionBarHidden !== undefined) { return !page.actionBarHidden; @@ -271,83 +357,19 @@ export class Frame extends FrameBase { } }); } - - protected _processNavigationContext(navigationContext: NavigationContext) { - let activity = this._android.activity; - if (activity) { - let isForegroundActivity = activity === application.android.foregroundActivity; - let isPaused = application.android.paused; - - if (activity && !isForegroundActivity || (isForegroundActivity && isPaused)) { - let weakActivity = new WeakRef(activity); - let resume = (args: application.AndroidActivityEventData) => { - let weakActivityInstance = weakActivity.get(); - let isCurrent = args.activity === weakActivityInstance; - if (!weakActivityInstance) { - if (traceEnabled()) { - traceWrite(`Frame _processNavigationContext: Drop For Activity GC-ed`, traceCategories.Navigation); - } - unsubscribe(); - return; - } - if (isCurrent) { - if (traceEnabled()) { - traceWrite(`Frame _processNavigationContext: Activity.Resumed, Continue`, traceCategories.Navigation); - } - super._processNavigationContext(navigationContext); - unsubscribe(); - } - }; - let unsubscribe = () => { - if (traceEnabled()) { - traceWrite(`Frame _processNavigationContext: Unsubscribe from Activity.Resumed`, traceCategories.Navigation); - } - application.android.off(application.AndroidApplication.activityResumedEvent, resume); - application.android.off(application.AndroidApplication.activityStoppedEvent, unsubscribe); - application.android.off(application.AndroidApplication.activityDestroyedEvent, unsubscribe); - }; - - if (traceEnabled()) { - traceWrite(`Frame._processNavigationContext: Subscribe for Activity.Resumed`, traceCategories.Navigation); - } - application.android.on(application.AndroidApplication.activityResumedEvent, resume); - application.android.on(application.AndroidApplication.activityStoppedEvent, unsubscribe); - application.android.on(application.AndroidApplication.activityDestroyedEvent, unsubscribe); - return; - } - } - super._processNavigationContext(navigationContext); - } } -function removeEntry(entry: BackstackEntry, page: Page, frame: View, entryUnused: boolean): void { +function clearEntry(entry: BackstackEntry): void { if (entry.fragment) { - if (entryUnused) { - _clearEntry(entry); - } else { - _clearFragment(entry.fragment); - } + _clearFragment(entry); } + entry.recreated = false; entry.fragment = null; - if (entryUnused) { - entry.viewSavedState = null; + const page = entry.resolvedPage; + if (page._context) { + entry.resolvedPage._tearDownUI(true); } - - if (frame) { - frame._removeView(page); - } -} - -function destroyPages(backStack: Array, entryUnused: boolean): void { - if (traceEnabled()) { - traceWrite(`CLEAR HISTORY`, traceCategories.Navigation); - } - - backStack.forEach((entry) => { - const page = entry.resolvedPage; - removeEntry(entry, page, page.frame, entryUnused); - }); } let framesCounter = 0; @@ -445,15 +467,10 @@ class AndroidFrame extends Observable implements AndroidFrameDefinition { return this.activity.getIntent().getAction() !== android.content.Intent.ACTION_MAIN; } - public fragmentForPage(page: Page): any { - if (!page) { - return undefined; - } - - let tag = page._fragmentTag; + public fragmentForPage(entry: BackstackEntry): any { + const tag = entry && entry.fragmentTag; if (tag) { - let manager = this.activity.getFragmentManager(); - return manager.findFragmentByTag(tag); + return this.owner._getFragmentManager().findFragmentByTag(tag); } return undefined; @@ -466,12 +483,21 @@ function findPageForFragment(fragment: android.app.Fragment, frame: Frame) { traceWrite(`Finding page for ${fragmentTag}.`, traceCategories.NativeLifecycle); } - if (fragmentTag === DIALOG_FRAGMENT_TAG) { - return; + let entry: BackstackEntry; + const current = frame._currentEntry; + const navigating = frame._executingEntry; + if (current && current.fragmentTag === fragmentTag) { + entry = current; + } else if (navigating && navigating.fragmentTag === fragmentTag) { + entry = navigating; + } + + let page: Page; + if (entry) { + entry.recreated = true; + page = entry.resolvedPage; } - const entry = frame._findEntryForTag(fragmentTag); - const page = entry ? entry.resolvedPage : undefined; if (page) { const callbacks: FragmentCallbacksImplementation = fragment[CALLBACKS]; callbacks.frame = frame; @@ -549,7 +575,7 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { case -40: nextAnimString = "popExit"; break; } - let animator = _onFragmentCreateAnimator(fragment, nextAnim); + let animator = _onFragmentCreateAnimator(this.entry, fragment, nextAnim, enter); if (!animator) { animator = superFunc.call(fragment, transit, enter, nextAnim); } @@ -570,7 +596,8 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { // There is no entry set to the fragment, so this must be destroyed fragment that was recreated by Android. // We should find its corresponding page in our backstack and set it manually. if (!this.entry) { - const frameId = fragment.getArguments().getInt(FRAMEID); + const args = fragment.getArguments(); + const frameId = args.getInt(FRAMEID); const frame = getFrameById(frameId); if (!frame) { throw new Error(`Cannot find Frame for ${fragment}`); @@ -588,19 +615,26 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { const entry = this.entry; const page = entry.resolvedPage; - try { - const frame = this.frame; - if (page.parent === frame) { - if (frame.isLoaded && !page.isLoaded) { - page.onLoaded(); - } - } else { - this.frame._addView(page); + const frame = this.frame; + if (page.parent === frame) { + // If we are navigating to a page that was destroyed + // reinitialize its UI. + if (!page._context) { + const context = container && container.getContext() || inflater && inflater.getContext(); + page._setupUI(context); } - } catch (ex) { - const label = new android.widget.TextView(container.getContext()); - label.setText(ex.message + ", " + ex.stackTrace); - return label; + } else { + this.frame._addView(page); + } + + // Load page here even if root view is not loaded yet. + // Otherwiaw it will show as blank, + // The case is Tab->Frame->Page activity recreated, fragments are + // created before Tab loads its items. + // TODO: addCheck if the fragment is visible so we don't load pages + // that are not in the selectedIndex of the Tab!!!!!! + if (!page.isLoaded) { + page.onLoaded(); } const savedState = entry.viewSavedState; @@ -636,14 +670,25 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { superFunc.call(fragment); } + @profile + public onStop(fragment: android.app.Fragment, superFunc: Function): void { + superFunc.call(fragment); + this.entry.resolvedPage.onUnloaded(); + } + @profile public toStringOverride(fragment: android.app.Fragment, superFunc: Function): string { - return `${fragment.getTag()}<${this.entry ? this.entry.resolvedPage : ""}>`; + const entry = this.entry; + if (entry) { + return `${entry.fragmentTag}<${entry.resolvedPage}>`; + } else { + return "NO ENTRY, " + superFunc.call(fragment); + } } } class ActivityCallbacksImplementation implements AndroidActivityCallbacks { - private _rootView: View; + public _rootView: View; @profile public onCreate(activity: android.app.Activity, savedInstanceState: android.os.Bundle, superFunc: Function): void { @@ -693,10 +738,10 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { // If there is savedInstanceState this call will recreate all fragments that were previously in the navigation. // We take care of associating them with a Page from our backstack in the onAttachFragment callback. - // If there is savedInstanceState and activityInitialized is false we are restarted but process was killed. + // If there is savedInstanceState and moduleLoaded is false we are restarted but process was killed. // For now we treat it like first run (e.g. we are not passing savedInstanceState so no fragments are being restored). // When we add support for application save/load state - revise this logic. - let isRestart = !!savedInstanceState && activityInitialized; + let isRestart = !!savedInstanceState && moduleLoaded; superFunc.call(activity, isRestart ? savedInstanceState : null); this._rootView = rootView; @@ -709,7 +754,7 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { frame.navigate(navParam); } - activityInitialized = true; + moduleLoaded = true; } @profile @@ -760,23 +805,19 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { @profile public onDestroy(activity: any, superFunc: Function): void { - const rootView = this._rootView; - if (rootView && rootView._context) { - rootView._tearDownUI(true); - } - - superFunc.call(activity); - if (traceEnabled()) { traceWrite("NativeScriptActivity.onDestroy();", traceCategories.NativeLifecycle); } + const rootView = this._rootView; + if (rootView) { + rootView._tearDownUI(true); + } + const exitArgs = { eventName: application.exitEvent, object: application.android, android: activity }; application.notify(exitArgs); - if (rootView instanceof Frame) { - destroyPages(rootView.backStack, false); - } + superFunc.call(activity); } @profile @@ -792,12 +833,29 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { cancel: false, }; application.android.notify(args); - if (args.cancel) { return; } - if (!goBack()) { + const view = this._rootView; + let callSuper = false; + if (view instanceof Frame) { + callSuper = !view._onBackPressed(); + } else { + const viewArgs = { + eventName: "activityBackPressed", + object: view, + activity: activity, + cancel: false, + }; + view.notify(viewArgs); + + if (!viewArgs.cancel) { //&& !view._onBackPressed()) { + callSuper = true; + } + } + + if (callSuper) { superFunc.call(activity); } } diff --git a/tns-core-modules/ui/frame/frame.d.ts b/tns-core-modules/ui/frame/frame.d.ts index f26b6986a..850940fea 100644 --- a/tns-core-modules/ui/frame/frame.d.ts +++ b/tns-core-modules/ui/frame/frame.d.ts @@ -110,8 +110,9 @@ export class Frame extends View { /** * @private * @param entry to set as current + * @param isBack true when we set current because of back navigation. */ - setCurrent(entry: BackstackEntry): void; + setCurrent(entry: BackstackEntry, isBack: boolean): void; /** * @private */ @@ -120,6 +121,10 @@ export class Frame extends View { * @private */ navigationBarHeight: number; + /** + * @private + */ + _currentEntry: BackstackEntry; /** * @private */ @@ -139,7 +144,7 @@ export class Frame extends View { /** * @private */ - _clearBackStack(): void; + _updateBackstack(entry: BackstackEntry, isBack: boolean): void; /** * @private */ @@ -300,6 +305,14 @@ export interface BackstackEntry { * @private */ viewSavedState?: any; + /** + * @private + */ + frameId?: number; + /** + * @private + */ + recreated?: boolean; //@endprivate } @@ -360,7 +373,7 @@ export interface AndroidFrame extends Observable { * Finds the native android.app.Fragment instance created for the specified Page. * @param page The Page instance to search for. */ - fragmentForPage(page: Page): any; + fragmentForPage(entry: BackstackEntry): any; } export interface AndroidActivityCallbacks { @@ -382,6 +395,7 @@ export interface AndroidFragmentCallbacks { onSaveInstanceState(fragment: any, outState: any, superFunc: Function): void; onDestroyView(fragment: any, superFunc: Function): void; onDestroy(fragment: any, superFunc: Function): void; + onStop(fragment: any, superFunc: Function): void; toStringOverride(fragment: any, superFunc: Function): string; } diff --git a/tns-core-modules/ui/frame/frame.ios.ts b/tns-core-modules/ui/frame/frame.ios.ts index e8eecd98d..76a72550d 100644 --- a/tns-core-modules/ui/frame/frame.ios.ts +++ b/tns-core-modules/ui/frame/frame.ios.ts @@ -1,5 +1,5 @@ // Definitions. -import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from "."; +import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition, NavigationEntry } from "."; import { Page } from "../page"; import { profile } from "../../profiling"; @@ -54,12 +54,11 @@ function handleNotification(notification: NSNotification): void { } export class Frame extends FrameBase { + public viewController: UINavigationControllerImpl; private _ios: iOSFrame; - private _paramToNavigate: any; public _animatedDelegate = UINavigationControllerAnimatedDelegate.new(); public _shouldSkipNativePop: boolean = false; - public _navigateToEntry: BackstackEntry; public _widthMeasureSpec: number; public _heightMeasureSpec: number; public _right: number; @@ -69,27 +68,12 @@ export class Frame extends FrameBase { constructor() { super(); this._ios = new iOSFrame(this); + this.viewController = this._ios.controller; this.nativeViewProtected = this._ios.controller.view; } - @profile - public onLoaded() { - super.onLoaded(); - - if (this._paramToNavigate) { - this.navigate(this._paramToNavigate); - this._paramToNavigate = undefined; - } - } - - public navigate(param: any) { - if (this.isLoaded) { - super.navigate(param); - this._isInitialNavigation = false; - } - else { - this._paramToNavigate = param; - } + public get ios(): iOSFrame { + return this._ios; } @profile @@ -103,7 +87,6 @@ export class Frame extends FrameBase { let clearHistory = backstackEntry.entry.clearHistory; if (clearHistory) { - this._clearBackStack(); navDepth = -1; } navDepth++; @@ -264,10 +247,6 @@ export class Frame extends FrameBase { } } - public get ios(): iOSFrame { - return this._ios; - } - public static get defaultAnimatedNavigation(): boolean { return FrameBase.defaultAnimatedNavigation; } @@ -302,12 +281,6 @@ export class Frame extends FrameBase { this._heightMeasureSpec = heightMeasureSpec; let result = this.measurePage(this.currentPage); - if (this._navigateToEntry && this.currentPage) { - let newPageSize = this.measurePage(this._navigateToEntry.resolvedPage); - result.measuredWidth = Math.max(result.measuredWidth, newPageSize.measuredWidth); - result.measuredHeight = Math.max(result.measuredHeight, newPageSize.measuredHeight); - } - let widthAndState = View.resolveSizeAndState(result.measuredWidth, width, widthMode, 0); let heightAndState = View.resolveSizeAndState(result.measuredHeight, height, heightMode, 0); @@ -332,9 +305,6 @@ export class Frame extends FrameBase { this._bottom = bottom; this._handleHigherInCallStatusBarIfNeeded(); this.layoutPage(this.currentPage); - if (this._navigateToEntry && this.currentPage) { - this.layoutPage(this._navigateToEntry.resolvedPage); - } } public layoutPage(page: Page): void { @@ -506,12 +476,21 @@ class UINavigationControllerImpl extends UINavigationController { @profile public viewWillAppear(animated: boolean): void { super.viewWillAppear(animated); - let owner = this._owner.get(); - if (owner && (!owner.isLoaded && !owner.parent)) { + const owner = this._owner.get(); + if (owner && !owner.isLoaded && !owner.parent) { owner.onLoaded(); } } + @profile + public viewDidDisappear(animated: boolean): void { + super.viewDidDisappear(animated); + const owner = this._owner.get(); + if (owner && owner.isLoaded && !owner.parent) { + owner.onUnloaded(); + } + } + @profile public viewDidLayoutSubviews(): void { let owner = this._owner.get(); diff --git a/tns-core-modules/ui/page/page.ios.ts b/tns-core-modules/ui/page/page.ios.ts index 214909252..7d9b5c5da 100644 --- a/tns-core-modules/ui/page/page.ios.ts +++ b/tns-core-modules/ui/page/page.ios.ts @@ -19,16 +19,15 @@ const ENTRY = "_entry"; const DELEGATE = "_delegate"; function isBackNavigationTo(page: Page, entry): boolean { - let frame = page.frame; + const frame = page.frame; if (!frame) { return false; } if (frame.navigationQueueIsEmpty()) { return true; - } - else { - let navigationQueue = (frame)._navigationQueue; + } else { + const navigationQueue = (frame)._navigationQueue; for (let i = 0; i < navigationQueue.length; i++) { if (navigationQueue[i].entry === entry) { return navigationQueue[i].isBackNavigation; @@ -65,7 +64,7 @@ class UIViewControllerImpl extends UIViewController { public shown: boolean; public static initWithOwner(owner: WeakRef): UIViewControllerImpl { - let controller = UIViewControllerImpl.new(); + const controller = UIViewControllerImpl.new(); controller._owner = owner; controller.automaticallyAdjustsScrollViewInsets = false; controller.shown = false; @@ -152,7 +151,7 @@ class UIViewControllerImpl extends UIViewController { // Don't raise event if currentPage was showing modal page. if (!page._presentedViewController && newEntry && (!frame || frame.currentPage !== page)) { - let isBack = isBackNavigationTo(page, newEntry); + const isBack = isBackNavigationTo(page, newEntry); page.onNavigatingTo(newEntry.entry.context, isBack, newEntry.entry.bindingContext); } @@ -165,18 +164,14 @@ class UIViewControllerImpl extends UIViewController { if (frame) { if (!page.parent) { - if (!frame._currentEntry) { - frame._currentEntry = newEntry; - } else { - frame._navigateToEntry = newEntry; - } - frame._addView(page); - frame.remeasureFrame(); } else if (page.parent !== frame) { throw new Error("Page is already shown on another frame."); } + frame.measurePage(page); + frame.layoutPage(page); + page.actionBar.update(); } @@ -206,7 +201,8 @@ class UIViewControllerImpl extends UIViewController { //https://github.com/NativeScript/NativeScript/issues/1201 page._viewWillDisappear = false; - let frame = this.navigationController ? (this.navigationController).owner : null; + const navigationController = this.navigationController; + const frame = navigationController ? (navigationController).owner : null; // Skip navigation events if modal page is shown. if (!page._presentedViewController && frame) { let newEntry = this[ENTRY]; @@ -216,25 +212,24 @@ class UIViewControllerImpl extends UIViewController { isBack = false; } - frame._navigateToEntry = null; - frame._currentEntry = newEntry; + frame._updateBackstack(newEntry, isBack); + frame.setCurrent(newEntry, isBack); frame.remeasureFrame(); frame._updateActionBar(page); - page.onNavigatedTo(isBack); - // If page was shown with custom animation - we need to set the navigationController.delegate to the animatedDelegate. frame.ios.controller.delegate = this[DELEGATE]; + frame._processNavigationQueue(page); + + // _processNavigationQueue will shift navigationQueue. Check canGoBack after that. // Workaround for disabled backswipe on second custom native transition if (frame.canGoBack()) { - this.navigationController.interactivePopGestureRecognizer.delegate = this.navigationController; - this.navigationController.interactivePopGestureRecognizer.enabled = page.enableSwipeBackNavigation; + navigationController.interactivePopGestureRecognizer.delegate = navigationController; + navigationController.interactivePopGestureRecognizer.enabled = page.enableSwipeBackNavigation; } else { - this.navigationController.interactivePopGestureRecognizer.enabled = false; + navigationController.interactivePopGestureRecognizer.enabled = false; } - - frame._processNavigationQueue(page); } if (!this.presentedViewController) { @@ -243,7 +238,7 @@ class UIViewControllerImpl extends UIViewController { // If we clean it when we have viewController then once presented VC is dismissed then page._presentedViewController = null; } - }; + } @profile public viewWillDisappear(animated: boolean): void { @@ -299,34 +294,38 @@ class UIViewControllerImpl extends UIViewController { // Manually pop backStack when Back button is pressed or navigating back with edge swipe. // Don't pop if we are hiding modally shown page. - let frame = page.frame; + // const frame = page.frame; // We are not modal page, have frame with backstack and navigation queue is empty and currentPage is closed // then pop our backstack. - if (!modalParent && frame && frame.backStack.length > 0 && frame.navigationQueueIsEmpty() && frame.currentPage === page) { - (frame)._backStack.pop(); - } + // If we are in frame wich is in tab and tab.selectedControler is not the frame + // skip navigation. + // const tab = this.tabBarController; + // const fireNavigationEvents = !tab + // || tab.selectedViewController === this.navigationController; + + // Remove from parent if page was in frame and we navigated back or + // navigate forward but current entry is not backstack visible. + // Showing page modally will not pass isBack check so currentPage won't be removed from Frame. + // const isBack = isBackNavigationFrom(this, page); + // if (frame && page.frame === frame && + // (isBack || !frame._isCurrentEntryBackstackVisible)) { + // // Remove parent when navigating back. + // frame._removeBackstackEntries([_removeBackstackEntries]) + // frame._removeView(page); + // page._frame = null; + // } page._enableLoadedEvents = true; - // Remove from parent if page was in frame and we navigated back. - // Showing page modally will not pass isBack check so currentPage won't be removed from Frame. - let isBack = isBackNavigationFrom(this, page); - if (isBack) { - // Remove parent when navigating back. - frame._removeView(page); - } - // Forward navigation does not remove page from frame so we raise unloaded manually. if (page.isLoaded) { page.onUnloaded(); } - page._enableLoadedEvents = false; - - if (!modalParent) { - // Last raise onNavigatedFrom event if we are not modally shown. - page.onNavigatedFrom(isBack); - } + // if (!modalParent && fireNavigationEvents) { + // // Last raise onNavigatedFrom event if we are not modally shown. + // page.onNavigatedFrom(isBack); + // } } } @@ -461,10 +460,10 @@ export class Page extends PageBase { public _updateStatusBarStyle(value?: string) { const frame = this.frame; if (this.frame && value) { - let navigationController = frame.ios.controller; - let navigationBar = navigationController.navigationBar; + const navigationController: UINavigationController = frame.ios.controller; + const navigationBar = navigationController.navigationBar; - navigationBar.barStyle = value === "dark" ? 1 : 0; + navigationBar.barStyle = value === "dark" ? UIBarStyle.Black : UIBarStyle.Default; } } @@ -587,9 +586,6 @@ export class Page extends PageBase { super._removeViewFromNativeVisualTree(view); } - [actionBarHiddenProperty.getDefault](): boolean { - return undefined; - } [actionBarHiddenProperty.setNative](value: boolean) { this._updateEnableSwipeBackNavigation(value); if (this.isLoaded) { @@ -602,9 +598,9 @@ export class Page extends PageBase { return UIBarStyle.Default; } [statusBarStyleProperty.setNative](value: string | UIBarStyle) { - let frame = this.frame; + const frame = this.frame; if (frame) { - let navigationBar = (frame.ios.controller).navigationBar; + const navigationBar = (frame.ios.controller).navigationBar; if (typeof value === "string") { navigationBar.barStyle = value === "dark" ? UIBarStyle.Black : UIBarStyle.Default; } else {