Skip to content

Commit effa137

Browse files
committed
fix-next(android): exit fragment animation
1 parent 0002624 commit effa137

4 files changed

Lines changed: 103 additions & 47 deletions

File tree

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

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AnimationType } from "./fragment.transitions";
66

77
// Types.
88
import { Transition, AndroidTransitionType } from "../transition/transition";
9+
import { ExpandedEntry, ExpandedAnimator, ExpandedTransitionListener } from "./fragment.transitions";
910
import { SlideTransition } from "../transition/slide-transition";
1011
import { FadeTransition } from "../transition/fade-transition";
1112
import { FlipTransition } from "../transition/flip-transition";
@@ -19,35 +20,6 @@ interface TransitionListener {
1920
new(entry: ExpandedEntry, transition: android.transition.Transition): ExpandedTransitionListener;
2021
}
2122

22-
interface ExpandedAnimator extends android.animation.Animator {
23-
entry: ExpandedEntry;
24-
transitionType?: string;
25-
}
26-
27-
interface ExpandedTransitionListener extends android.transition.Transition.TransitionListener {
28-
entry: ExpandedEntry;
29-
transition: android.transition.Transition;
30-
}
31-
32-
interface ExpandedEntry extends BackstackEntry {
33-
enterTransitionListener: ExpandedTransitionListener;
34-
exitTransitionListener: ExpandedTransitionListener;
35-
reenterTransitionListener: ExpandedTransitionListener;
36-
returnTransitionListener: ExpandedTransitionListener;
37-
38-
enterAnimator: ExpandedAnimator;
39-
exitAnimator: ExpandedAnimator;
40-
popEnterAnimator: ExpandedAnimator;
41-
popExitAnimator: ExpandedAnimator;
42-
43-
defaultEnterAnimator: ExpandedAnimator;
44-
defaultExitAnimator: ExpandedAnimator;
45-
46-
transition: Transition;
47-
transitionName: string;
48-
frameId: number
49-
}
50-
5123
const sdkVersion = lazy(() => parseInt(device.sdkVersion));
5224
const intEvaluator = lazy(() => new android.animation.IntEvaluator());
5325
const defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator());
@@ -151,7 +123,8 @@ export function _setAndroidFragmentTransitions(
151123

152124
// Having transition means we have custom animation
153125
if (transition) {
154-
fragmentTransaction.setCustomAnimations(AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId, AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId);
126+
// we do not use Android backstack so setting popEnter / popExit is meaningless (3rd and 4th optional args)
127+
fragmentTransaction.setCustomAnimations(AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId);
155128
setupAllAnimation(newEntry, transition);
156129
if (currentFragmentNeedsDifferentAnimation) {
157130
setupExitAndPopEnterAnimation(currentEntry, transition);

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/ /** */
44

55
import { NavigationTransition, BackstackEntry } from "../frame";
6+
import { Transition } from "../transition/transition";
67

78
//@private
89
/**
@@ -15,6 +16,47 @@ export const enum AnimationType {
1516
popExitFakeResourceId = -40
1617
}
1718

19+
//@private
20+
/**
21+
* @private
22+
*/
23+
export interface ExpandedAnimator extends android.animation.Animator {
24+
entry: ExpandedEntry;
25+
transitionType?: string;
26+
}
27+
28+
//@private
29+
/**
30+
* @private
31+
*/
32+
export interface ExpandedTransitionListener extends android.transition.Transition.TransitionListener {
33+
entry: ExpandedEntry;
34+
transition: android.transition.Transition;
35+
}
36+
37+
//@private
38+
/**
39+
* @private
40+
*/
41+
export interface ExpandedEntry extends BackstackEntry {
42+
enterTransitionListener: ExpandedTransitionListener;
43+
exitTransitionListener: ExpandedTransitionListener;
44+
reenterTransitionListener: ExpandedTransitionListener;
45+
returnTransitionListener: ExpandedTransitionListener;
46+
47+
enterAnimator: ExpandedAnimator;
48+
exitAnimator: ExpandedAnimator;
49+
popEnterAnimator: ExpandedAnimator;
50+
popExitAnimator: ExpandedAnimator;
51+
52+
defaultEnterAnimator: ExpandedAnimator;
53+
defaultExitAnimator: ExpandedAnimator;
54+
55+
transition: Transition;
56+
transitionName: string;
57+
frameId: number
58+
}
59+
1860
/**
1961
* @private
2062
*/

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

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414

1515
import {
1616
_setAndroidFragmentTransitions, _onFragmentCreateAnimator, _getAnimatedEntries,
17-
_updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, AnimationType
17+
_updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, AnimationType, ExpandedEntry
1818
} from "./fragment.transitions";
1919

2020
import { profile } from "../../profiling";
@@ -93,6 +93,7 @@ export class Frame extends FrameBase {
9393
private _tearDownPending = false;
9494
private _attachedToWindow = false;
9595
public _isBack: boolean = true;
96+
private _cachedAnimationEntry: ExpandedEntry;
9697

9798
constructor() {
9899
super();
@@ -170,6 +171,17 @@ export class Frame extends FrameBase {
170171
const entry = this._currentEntry;
171172
if (entry && manager && !manager.findFragmentByTag(entry.fragmentTag)) {
172173
// Simulate first navigation (e.g. no animations or transitions)
174+
// we need to cache the original animation settings so we can restore them later; otherwise as the
175+
// simulated first navigation is not animated (it is actually a zero duration animator) the "popExit" animation
176+
// is broken when transaction.setCustomAnimations(...) is used in a scenario with:
177+
// 1) forward navigation
178+
// 2) suspend / resume app
179+
// 3) back navigation -- the exiting fragment is erroneously animated with the exit animator from the
180+
// simulated navigation (NoTransition, zero duration animator) and thus the fragment immediately disappears;
181+
// the user only sees the animation of the entering fragment as per its specific enter animation settings.
182+
// NOTE: we are restoring the animation settings in Frame.setCurrent(...) as navigation completes asynchronously
183+
this._cachedAnimationEntry = getAnimatorState(this._currentEntry);
184+
173185
this._currentEntry = null;
174186
// NavigateCore will eventually call _processNextNavigationEntry again.
175187
this._navigateCore(entry);
@@ -194,8 +206,12 @@ export class Frame extends FrameBase {
194206
}
195207

196208
onUnloaded() {
197-
this.disposeCurrentFragment();
198209
super.onUnloaded();
210+
211+
// calling dispose fragment after super.onUnloaded() means we are not relying on the built-in Android logic
212+
// to automatically remove child fragments when parent fragment is removed;
213+
// this fixes issue with missing nested fragment on app suspend / resume;
214+
this.disposeCurrentFragment();
199215
}
200216

201217
private disposeCurrentFragment(): void {
@@ -278,6 +294,14 @@ export class Frame extends FrameBase {
278294
// Continue with next item in the queue.
279295
this._processNextNavigationEntry();
280296
}
297+
298+
// restore cached animation settings if we just completed simulated first navigation (no animation)
299+
if (this._cachedAnimationEntry) {
300+
setAnimatorState(this._currentEntry, this._cachedAnimationEntry);
301+
302+
this._cachedAnimationEntry = null;
303+
}
304+
281305
}
282306

283307
public onBackPressed(): boolean {
@@ -332,7 +356,7 @@ export class Frame extends FrameBase {
332356
const newFragmentTag = `fragment${fragmentId}[${navDepth}]`;
333357
const newFragment = this.createFragment(newEntry, newFragmentTag);
334358
const transaction = manager.beginTransaction();
335-
const animated = this._getIsAnimatedNavigation(newEntry.entry);
359+
const animated = currentEntry ? this._getIsAnimatedNavigation(newEntry.entry) : false;
336360
// NOTE: Don't use transition for the initial navigation (same as on iOS)
337361
// On API 21+ transition won't be triggered unless there was at least one
338362
// layout pass so we will wait forever for transitionCompleted handler...
@@ -346,7 +370,7 @@ export class Frame extends FrameBase {
346370
}
347371

348372
transaction.replace(this.containerViewId, newFragment, newFragmentTag);
349-
transaction.commit();
373+
transaction.commitAllowingStateLoss();
350374
}
351375

352376
public _goBackCore(backstackEntry: BackstackEntry) {
@@ -369,11 +393,12 @@ export class Frame extends FrameBase {
369393
const transitionReversed = _reverseTransitions(backstackEntry, this._currentEntry);
370394
if (!transitionReversed) {
371395
// If transition were not reversed then use animations.
372-
transaction.setCustomAnimations(AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId, AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId);
396+
// we do not use Android backstack so setting popEnter / popExit is meaningless (3rd and 4th optional args)
397+
transaction.setCustomAnimations(AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId);
373398
}
374399

375400
transaction.replace(this.containerViewId, backstackEntry.fragment, backstackEntry.fragmentTag);
376-
transaction.commit();
401+
transaction.commitAllowingStateLoss();
377402
}
378403

379404
public _removeEntry(removed: BackstackEntry): void {
@@ -470,6 +495,27 @@ export class Frame extends FrameBase {
470495
}
471496
}
472497

498+
function getAnimatorState(entry: BackstackEntry): ExpandedEntry {
499+
const expandedEntry = <ExpandedEntry>entry;
500+
const snapshot = <ExpandedEntry>{};
501+
snapshot.enterAnimator = expandedEntry.enterAnimator;
502+
snapshot.exitAnimator = expandedEntry.exitAnimator;
503+
snapshot.popEnterAnimator = expandedEntry.popEnterAnimator;
504+
snapshot.popExitAnimator = expandedEntry.popExitAnimator;
505+
snapshot.transitionName = expandedEntry.transitionName;
506+
507+
return snapshot;
508+
}
509+
510+
function setAnimatorState(entry: BackstackEntry, snapshot: ExpandedEntry): void {
511+
const expandedEntry = <ExpandedEntry>entry;
512+
expandedEntry.enterAnimator = snapshot.enterAnimator;
513+
expandedEntry.exitAnimator = snapshot.exitAnimator;
514+
expandedEntry.popEnterAnimator = snapshot.popEnterAnimator;
515+
expandedEntry.popExitAnimator = snapshot.popExitAnimator;
516+
expandedEntry.transitionName = snapshot.transitionName;
517+
}
518+
473519
function clearEntry(entry: BackstackEntry): void {
474520
if (entry.fragment) {
475521
_clearFragment(entry);
@@ -786,16 +832,6 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
786832
traceWrite(`${fragment}.onDestroyView()`, traceCategories.NativeLifecycle);
787833
}
788834

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-
799835
superFunc.call(fragment);
800836
}
801837

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)