fix-next(android): exit fragment animation (#6421)

This commit is contained in:
Manol Donev
2018-10-26 13:32:30 +03:00
committed by dtopuzov
parent 84e973d6a8
commit c217f12c8f
3 changed files with 68 additions and 18 deletions

View File

@ -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);
}

View File

@ -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 = <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);
@ -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);
}

View File

@ -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();