Skip to content

Commit 4765dd9

Browse files
authored
fix-next(android): exit fragment animation (NativeScript#6421)
1 parent 0002624 commit 4765dd9

File tree

3 files changed

+68
-18
lines changed

3 files changed

+68
-18
lines changed

tns-core-modules/ui/frame/fragment.transitions.android.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ export function _setAndroidFragmentTransitions(
151151

152152
// Having transition means we have custom animation
153153
if (transition) {
154-
fragmentTransaction.setCustomAnimations(AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId, AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId);
154+
// we do not use Android backstack so setting popEnter / popExit is meaningless (3rd and 4th optional args)
155+
fragmentTransaction.setCustomAnimations(AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId);
155156
setupAllAnimation(newEntry, transition);
156157
if (currentFragmentNeedsDifferentAnimation) {
157158
setupExitAndPopEnterAnimation(currentEntry, transition);
@@ -375,7 +376,7 @@ function clearAnimationListener(animator: ExpandedAnimator, listener: android.an
375376

376377
animator.removeListener(listener);
377378

378-
if (traceEnabled()) {
379+
if (animator.entry && traceEnabled()) {
379380
const entry = animator.entry;
380381
traceWrite(`Clear ${animator.transitionType} - ${entry.transition} for ${entry.fragmentTag}`, traceCategories.Transition);
381382
}

tns-core-modules/ui/frame/frame.android.ts

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ import { createViewFromEntry } from "../builder";
2424

2525
export * from "./frame-common";
2626

27+
interface AnimatorState {
28+
enterAnimator: android.animation.Animator;
29+
exitAnimator: android.animation.Animator;
30+
popEnterAnimator: android.animation.Animator;
31+
popExitAnimator: android.animation.Animator;
32+
transitionName: string;
33+
}
34+
2735
const INTENT_EXTRA = "com.tns.activity";
2836
const ROOT_VIEW_ID_EXTRA = "com.tns.activity.rootViewId";
2937
const FRAMEID = "_frameId";
@@ -93,6 +101,7 @@ export class Frame extends FrameBase {
93101
private _tearDownPending = false;
94102
private _attachedToWindow = false;
95103
public _isBack: boolean = true;
104+
private _cachedAnimatorState: AnimatorState;
96105

97106
constructor() {
98107
super();
@@ -170,6 +179,17 @@ export class Frame extends FrameBase {
170179
const entry = this._currentEntry;
171180
if (entry && manager && !manager.findFragmentByTag(entry.fragmentTag)) {
172181
// Simulate first navigation (e.g. no animations or transitions)
182+
// we need to cache the original animation settings so we can restore them later; otherwise as the
183+
// simulated first navigation is not animated (it is actually a zero duration animator) the "popExit" animation
184+
// is broken when transaction.setCustomAnimations(...) is used in a scenario with:
185+
// 1) forward navigation
186+
// 2) suspend / resume app
187+
// 3) back navigation -- the exiting fragment is erroneously animated with the exit animator from the
188+
// simulated navigation (NoTransition, zero duration animator) and thus the fragment immediately disappears;
189+
// the user only sees the animation of the entering fragment as per its specific enter animation settings.
190+
// NOTE: we are restoring the animation settings in Frame.setCurrent(...) as navigation completes asynchronously
191+
this._cachedAnimatorState = getAnimatorState(this._currentEntry);
192+
173193
this._currentEntry = null;
174194
// NavigateCore will eventually call _processNextNavigationEntry again.
175195
this._navigateCore(entry);
@@ -194,8 +214,12 @@ export class Frame extends FrameBase {
194214
}
195215

196216
onUnloaded() {
197-
this.disposeCurrentFragment();
198217
super.onUnloaded();
218+
219+
// calling dispose fragment after super.onUnloaded() means we are not relying on the built-in Android logic
220+
// to automatically remove child fragments when parent fragment is removed;
221+
// this fixes issue with missing nested fragment on app suspend / resume;
222+
this.disposeCurrentFragment();
199223
}
200224

201225
private disposeCurrentFragment(): void {
@@ -278,6 +302,14 @@ export class Frame extends FrameBase {
278302
// Continue with next item in the queue.
279303
this._processNextNavigationEntry();
280304
}
305+
306+
// restore cached animation settings if we just completed simulated first navigation (no animation)
307+
if (this._cachedAnimatorState) {
308+
restoreAnimatorState(this._currentEntry, this._cachedAnimatorState);
309+
310+
this._cachedAnimatorState = null;
311+
}
312+
281313
}
282314

283315
public onBackPressed(): boolean {
@@ -332,7 +364,7 @@ export class Frame extends FrameBase {
332364
const newFragmentTag = `fragment${fragmentId}[${navDepth}]`;
333365
const newFragment = this.createFragment(newEntry, newFragmentTag);
334366
const transaction = manager.beginTransaction();
335-
const animated = this._getIsAnimatedNavigation(newEntry.entry);
367+
const animated = currentEntry ? this._getIsAnimatedNavigation(newEntry.entry) : false;
336368
// NOTE: Don't use transition for the initial navigation (same as on iOS)
337369
// On API 21+ transition won't be triggered unless there was at least one
338370
// layout pass so we will wait forever for transitionCompleted handler...
@@ -346,7 +378,7 @@ export class Frame extends FrameBase {
346378
}
347379

348380
transaction.replace(this.containerViewId, newFragment, newFragmentTag);
349-
transaction.commit();
381+
transaction.commitAllowingStateLoss();
350382
}
351383

352384
public _goBackCore(backstackEntry: BackstackEntry) {
@@ -369,11 +401,12 @@ export class Frame extends FrameBase {
369401
const transitionReversed = _reverseTransitions(backstackEntry, this._currentEntry);
370402
if (!transitionReversed) {
371403
// If transition were not reversed then use animations.
372-
transaction.setCustomAnimations(AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId, AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId);
404+
// we do not use Android backstack so setting popEnter / popExit is meaningless (3rd and 4th optional args)
405+
transaction.setCustomAnimations(AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId);
373406
}
374407

375408
transaction.replace(this.containerViewId, backstackEntry.fragment, backstackEntry.fragmentTag);
376-
transaction.commit();
409+
transaction.commitAllowingStateLoss();
377410
}
378411

379412
public _removeEntry(removed: BackstackEntry): void {
@@ -470,6 +503,27 @@ export class Frame extends FrameBase {
470503
}
471504
}
472505

506+
function getAnimatorState(entry: BackstackEntry): AnimatorState {
507+
const expandedEntry = <any>entry;
508+
const animatorState = <AnimatorState>{};
509+
animatorState.enterAnimator = expandedEntry.enterAnimator;
510+
animatorState.exitAnimator = expandedEntry.exitAnimator;
511+
animatorState.popEnterAnimator = expandedEntry.popEnterAnimator;
512+
animatorState.popExitAnimator = expandedEntry.popExitAnimator;
513+
animatorState.transitionName = expandedEntry.transitionName;
514+
515+
return animatorState;
516+
}
517+
518+
function restoreAnimatorState(entry: BackstackEntry, snapshot: AnimatorState): void {
519+
const expandedEntry = <any>entry;
520+
expandedEntry.enterAnimator = snapshot.enterAnimator;
521+
expandedEntry.exitAnimator = snapshot.exitAnimator;
522+
expandedEntry.popEnterAnimator = snapshot.popEnterAnimator;
523+
expandedEntry.popExitAnimator = snapshot.popExitAnimator;
524+
expandedEntry.transitionName = snapshot.transitionName;
525+
}
526+
473527
function clearEntry(entry: BackstackEntry): void {
474528
if (entry.fragment) {
475529
_clearFragment(entry);
@@ -786,16 +840,6 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
786840
traceWrite(`${fragment}.onDestroyView()`, traceCategories.NativeLifecycle);
787841
}
788842

789-
// fixes 'java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first'.
790-
// on app resume in nested frame scenarios with support library version greater than 26.0.0
791-
const view = fragment.getView();
792-
if (view != null) {
793-
const viewParent = view.getParent();
794-
if (viewParent instanceof android.view.ViewGroup) {
795-
viewParent.removeView(view);
796-
}
797-
}
798-
799843
superFunc.call(fragment);
800844
}
801845

tns-core-modules/ui/tab-view/tab-view.android.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,13 @@ export class TabViewItem extends TabViewItemBase {
317317
}
318318
}
319319

320+
// TODO: can happen in a modal tabview scenario when the modal dialog fragment is already removed
320321
if (!tabFragment) {
321-
throw new Error(`Could not get child fragment manager for tab item with index ${this.index}`);
322+
if (traceEnabled()) {
323+
traceWrite(`Could not get child fragment manager for tab item with index ${this.index}`, traceCategory);
324+
}
325+
326+
return (<any>tabView)._getRootFragmentManager();
322327
}
323328

324329
return tabFragment.getChildFragmentManager();

0 commit comments

Comments
 (0)