Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions tns-core-modules/ui/frame/fragment.transitions.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
74 changes: 59 additions & 15 deletions tns-core-modules/ui/frame/frame.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -93,6 +101,7 @@ export class Frame extends FrameBase {
private _tearDownPending = false;
private _attachedToWindow = false;
public _isBack: boolean = true;
private _cachedAnimatorState: AnimatorState;

constructor() {
super();
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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...
Expand All @@ -346,7 +378,7 @@ export class Frame extends FrameBase {
}

transaction.replace(this.containerViewId, newFragment, newFragmentTag);
transaction.commit();
transaction.commitAllowingStateLoss();
}

public _goBackCore(backstackEntry: BackstackEntry) {
Expand All @@ -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 {
Expand Down Expand Up @@ -470,6 +503,27 @@ export class Frame extends FrameBase {
}
}

function getAnimatorState(entry: BackstackEntry): AnimatorState {
const expandedEntry = <any>entry;
const animatorState = <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 = <any>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);
Expand Down Expand Up @@ -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);
}

Expand Down
7 changes: 6 additions & 1 deletion tns-core-modules/ui/tab-view/tab-view.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<any>tabView)._getRootFragmentManager();
}

return tabFragment.getChildFragmentManager();
Expand Down