diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts index bfb61ea50..43eefeeff 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -151,7 +151,8 @@ export function _setAndroidFragmentTransitions( // Having transition means we have custom animation if (transition) { - fragmentTransaction.setCustomAnimations(AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId, AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId); + // we do not use Android backstack so setting popEnter / popExit is meaningless (3rd and 4th optional args) + fragmentTransaction.setCustomAnimations(AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId); setupAllAnimation(newEntry, transition); if (currentFragmentNeedsDifferentAnimation) { setupExitAndPopEnterAnimation(currentEntry, transition); @@ -375,7 +376,7 @@ function clearAnimationListener(animator: ExpandedAnimator, listener: android.an animator.removeListener(listener); - if (traceEnabled()) { + if (animator.entry && traceEnabled()) { const entry = animator.entry; traceWrite(`Clear ${animator.transitionType} - ${entry.transition} for ${entry.fragmentTag}`, traceCategories.Transition); } diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index 2e2d7e65f..bfb911d2c 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -24,6 +24,14 @@ import { createViewFromEntry } from "../builder"; export * from "./frame-common"; +interface AnimatorState { + enterAnimator: android.animation.Animator; + exitAnimator: android.animation.Animator; + popEnterAnimator: android.animation.Animator; + popExitAnimator: android.animation.Animator; + transitionName: string; +} + const INTENT_EXTRA = "com.tns.activity"; const ROOT_VIEW_ID_EXTRA = "com.tns.activity.rootViewId"; const FRAMEID = "_frameId"; @@ -93,6 +101,7 @@ export class Frame extends FrameBase { private _tearDownPending = false; private _attachedToWindow = false; public _isBack: boolean = true; + private _cachedAnimatorState: AnimatorState; constructor() { super(); @@ -170,6 +179,17 @@ export class Frame extends FrameBase { const entry = this._currentEntry; if (entry && manager && !manager.findFragmentByTag(entry.fragmentTag)) { // Simulate first navigation (e.g. no animations or transitions) + // we need to cache the original animation settings so we can restore them later; otherwise as the + // simulated first navigation is not animated (it is actually a zero duration animator) the "popExit" animation + // is broken when transaction.setCustomAnimations(...) is used in a scenario with: + // 1) forward navigation + // 2) suspend / resume app + // 3) back navigation -- the exiting fragment is erroneously animated with the exit animator from the + // simulated navigation (NoTransition, zero duration animator) and thus the fragment immediately disappears; + // the user only sees the animation of the entering fragment as per its specific enter animation settings. + // NOTE: we are restoring the animation settings in Frame.setCurrent(...) as navigation completes asynchronously + this._cachedAnimatorState = getAnimatorState(this._currentEntry); + this._currentEntry = null; // NavigateCore will eventually call _processNextNavigationEntry again. this._navigateCore(entry); @@ -194,8 +214,12 @@ export class Frame extends FrameBase { } onUnloaded() { - this.disposeCurrentFragment(); super.onUnloaded(); + + // calling dispose fragment after super.onUnloaded() means we are not relying on the built-in Android logic + // to automatically remove child fragments when parent fragment is removed; + // this fixes issue with missing nested fragment on app suspend / resume; + this.disposeCurrentFragment(); } private disposeCurrentFragment(): void { @@ -278,6 +302,14 @@ export class Frame extends FrameBase { // Continue with next item in the queue. this._processNextNavigationEntry(); } + + // restore cached animation settings if we just completed simulated first navigation (no animation) + if (this._cachedAnimatorState) { + restoreAnimatorState(this._currentEntry, this._cachedAnimatorState); + + this._cachedAnimatorState = null; + } + } public onBackPressed(): boolean { @@ -332,7 +364,7 @@ export class Frame extends FrameBase { const newFragmentTag = `fragment${fragmentId}[${navDepth}]`; const newFragment = this.createFragment(newEntry, newFragmentTag); const transaction = manager.beginTransaction(); - const animated = this._getIsAnimatedNavigation(newEntry.entry); + const animated = currentEntry ? this._getIsAnimatedNavigation(newEntry.entry) : false; // NOTE: Don't use transition for the initial navigation (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... @@ -346,7 +378,7 @@ export class Frame extends FrameBase { } transaction.replace(this.containerViewId, newFragment, newFragmentTag); - transaction.commit(); + transaction.commitAllowingStateLoss(); } public _goBackCore(backstackEntry: BackstackEntry) { @@ -369,11 +401,12 @@ export class Frame extends FrameBase { const transitionReversed = _reverseTransitions(backstackEntry, this._currentEntry); if (!transitionReversed) { // If transition were not reversed then use animations. - transaction.setCustomAnimations(AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId, AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId); + // we do not use Android backstack so setting popEnter / popExit is meaningless (3rd and 4th optional args) + transaction.setCustomAnimations(AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId); } transaction.replace(this.containerViewId, backstackEntry.fragment, backstackEntry.fragmentTag); - transaction.commit(); + transaction.commitAllowingStateLoss(); } public _removeEntry(removed: BackstackEntry): void { @@ -470,6 +503,27 @@ export class Frame extends FrameBase { } } +function getAnimatorState(entry: BackstackEntry): AnimatorState { + const expandedEntry = entry; + const animatorState = {}; + animatorState.enterAnimator = expandedEntry.enterAnimator; + animatorState.exitAnimator = expandedEntry.exitAnimator; + animatorState.popEnterAnimator = expandedEntry.popEnterAnimator; + animatorState.popExitAnimator = expandedEntry.popExitAnimator; + animatorState.transitionName = expandedEntry.transitionName; + + return animatorState; +} + +function restoreAnimatorState(entry: BackstackEntry, snapshot: AnimatorState): void { + const expandedEntry = entry; + expandedEntry.enterAnimator = snapshot.enterAnimator; + expandedEntry.exitAnimator = snapshot.exitAnimator; + expandedEntry.popEnterAnimator = snapshot.popEnterAnimator; + expandedEntry.popExitAnimator = snapshot.popExitAnimator; + expandedEntry.transitionName = snapshot.transitionName; +} + function clearEntry(entry: BackstackEntry): void { if (entry.fragment) { _clearFragment(entry); @@ -786,16 +840,6 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { traceWrite(`${fragment}.onDestroyView()`, traceCategories.NativeLifecycle); } - // fixes 'java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first'. - // on app resume in nested frame scenarios with support library version greater than 26.0.0 - const view = fragment.getView(); - if (view != null) { - const viewParent = view.getParent(); - if (viewParent instanceof android.view.ViewGroup) { - viewParent.removeView(view); - } - } - superFunc.call(fragment); } diff --git a/tns-core-modules/ui/tab-view/tab-view.android.ts b/tns-core-modules/ui/tab-view/tab-view.android.ts index f12e115ac..041b7a716 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -317,8 +317,13 @@ export class TabViewItem extends TabViewItemBase { } } + // TODO: can happen in a modal tabview scenario when the modal dialog fragment is already removed if (!tabFragment) { - throw new Error(`Could not get child fragment manager for tab item with index ${this.index}`); + if (traceEnabled()) { + traceWrite(`Could not get child fragment manager for tab item with index ${this.index}`, traceCategory); + } + + return (tabView)._getRootFragmentManager(); } return tabFragment.getChildFragmentManager();