From 6aac3201034642f4331f18566e291247c3bd8efd Mon Sep 17 00:00:00 2001 From: Manol Donev Date: Thu, 6 Dec 2018 16:35:10 +0200 Subject: [PATCH 1/4] fix(android): nested fragment disappears on parent fragment removal --- tns-core-modules/ui/frame/fragment.android.ts | 41 +++++++++++++++++-- .../ui/frame/fragment.transitions.android.ts | 15 ++++++- .../ui/tab-view/tab-view.android.ts | 41 +++++++++++++++++-- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/tns-core-modules/ui/frame/fragment.android.ts b/tns-core-modules/ui/frame/fragment.android.ts index 86b3ccc2cf..df06e6ccd3 100644 --- a/tns-core-modules/ui/frame/fragment.android.ts +++ b/tns-core-modules/ui/frame/fragment.android.ts @@ -1,10 +1,22 @@ import { AndroidFragmentCallbacks, setFragmentCallbacks, setFragmentClass } from "./frame"; +import { AnimationType } from "./fragment.transitions"; + +function createDummyAnimator(duration: number): android.animation.Animator { + const alphaValues = Array.create("float", 2); + alphaValues[0] = 1; + alphaValues[1] = 1; + + const animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", alphaValues); + animator.setDuration(duration); + + return animator; +} @JavaProxy("com.tns.FragmentClass") class FragmentClass extends android.support.v4.app.Fragment { // This field is updated in the frame module upon `new` (although hacky this eases the Fragment->callbacks association a lot) private _callbacks: AndroidFragmentCallbacks; - + constructor() { super(); return global.__native(this); @@ -15,8 +27,22 @@ class FragmentClass extends android.support.v4.app.Fragment { } public onCreateAnimator(transit: number, enter: boolean, nextAnim: number): android.animation.Animator { - let result = this._callbacks.onCreateAnimator(this, transit, enter, nextAnim, super.onCreateAnimator); - return result; + // [nested frames / fragments] apply dummy animator to the nested fragment with + // the same duration as the exit animator of the removing parent fragment to work around + // https://code.google.com/p/android/issues/detail?id=55228 (child fragments disappear + // when parent fragment is removed as all children are first removed from parent) + if (!enter) { + const removingParentFragment = this.getRemovingParentFragment(); + if (removingParentFragment) { + const parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimationType.exitFakeResourceId); + if (parentAnimator) { + const duration = parentAnimator.getDuration(); + return createDummyAnimator(duration); + } + } + } + + return this._callbacks.onCreateAnimator(this, transit, enter, nextAnim, super.onCreateAnimator); } public onStop(): void { @@ -57,6 +83,15 @@ class FragmentClass extends android.support.v4.app.Fragment { super.toString(); } } + + private getRemovingParentFragment(): android.support.v4.app.Fragment { + let parentFragment = this.getParentFragment(); + while (parentFragment && !parentFragment.isRemoving()) { + parentFragment = parentFragment.getParentFragment(); + } + + return parentFragment; + } } setFragmentClass(FragmentClass); \ No newline at end of file diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts index 43eefeeff3..95d682b03e 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -86,7 +86,20 @@ export function _setAndroidFragmentTransitions( name = navigationTransition.name ? navigationTransition.name.toLowerCase() : ""; } - let useLollipopTransition = name && (name.indexOf("slide") === 0 || name === "fade" || name === "explode") && sdkVersion() >= 21; + let useLollipopTransition = !!(name && (name.indexOf("slide") === 0 || name === "fade" || name === "explode") && sdkVersion() >= 21); + // [nested frames / fragments] force disable lollipop transitions in case nested fragments + // are detected as applying dummy animator to the nested fragment with the same duration as + // the exit animator of the removing parent fragment as a workaround for + // https://code.google.com/p/android/issues/detail?id=55228 works only if custom animations are + // used + // NOTE: this effectively means you cannot use Explode transition in nested frames scenarios as + // we have implementations only for slide, fade, and flip + if (currentFragment && + currentFragment.getChildFragmentManager() && + currentFragment.getChildFragmentManager().getFragments().toArray().length > 0) { + useLollipopTransition = false; + } + if (!animated) { name = "none"; } else if (transition) { 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 8b89422d6c..b83562e361 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -12,6 +12,7 @@ import { textTransformProperty, TextTransform, getTransformedText } from "../tex import { fromFileOrResource } from "../../image-source"; import { RESOURCE_PREFIX, ad } from "../../utils/utils"; import { Frame } from "../frame"; +import { AnimationType } from "../frame/fragment.transitions"; export * from "./tab-view-common"; @@ -40,6 +41,17 @@ function getTabById(id: number): TabView { return ref && ref.get(); } +function createDummyAnimator(duration: number): android.animation.Animator { + const alphaValues = Array.create("float", 2); + alphaValues[0] = 1; + alphaValues[1] = 1; + + const animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", alphaValues); + animator.setDuration(duration); + + return animator; +} + function initializeNativeClasses() { if (PagerAdapter) { return; @@ -55,7 +67,6 @@ function initializeNativeClasses() { } static newInstance(tabId: number, index: number): TabFragmentImplementation { - const args = new android.os.Bundle(); args.putInt(TABID, tabId); args.putInt(INDEX, index); @@ -74,14 +85,38 @@ function initializeNativeClasses() { } } + public onCreateAnimator(transit: number, enter: boolean, nextAnim: number): android.animation.Animator { + // [nested frames / fragments] apply dummy animator to the nested fragment with + // the same duration as the exit animator of the removing parent fragment to work around + // https://code.google.com/p/android/issues/detail?id=55228 (child fragments disappear + // when parent fragment is removed as all children are first removed from parent) + if (!enter) { + const removingParentFragment = this.getRemovingParentFragment(); + if (removingParentFragment) { + const parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimationType.exitFakeResourceId); + if (parentAnimator) { + const duration = parentAnimator.getDuration(); + return createDummyAnimator(duration); + } + } + } + + return super.onCreateAnimator(transit, enter, nextAnim); + } + public onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View { const tabItem = this.tab.items[this.index]; return tabItem.view.nativeViewProtected; } - public onDestroyView() { - super.onDestroyView(); + private getRemovingParentFragment(): android.support.v4.app.Fragment { + let parentFragment = this.getParentFragment(); + while (parentFragment && !parentFragment.isRemoving()) { + parentFragment = parentFragment.getParentFragment(); + } + + return parentFragment; } } From 8a677e74fb1d492269896aad057581c966744f0c Mon Sep 17 00:00:00 2001 From: Manol Donev Date: Thu, 6 Dec 2018 17:21:30 +0200 Subject: [PATCH 2/4] fix flip transition with nested frames hack --- tns-core-modules/ui/frame/fragment.android.ts | 5 ++++- tns-core-modules/ui/tab-view/tab-view.android.ts | 5 ++++- tns-core-modules/ui/transition/flip-transition.android.ts | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tns-core-modules/ui/frame/fragment.android.ts b/tns-core-modules/ui/frame/fragment.android.ts index df06e6ccd3..e2b133b306 100644 --- a/tns-core-modules/ui/frame/fragment.android.ts +++ b/tns-core-modules/ui/frame/fragment.android.ts @@ -37,7 +37,10 @@ class FragmentClass extends android.support.v4.app.Fragment { const parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimationType.exitFakeResourceId); if (parentAnimator) { const duration = parentAnimator.getDuration(); - return createDummyAnimator(duration); + + // duration will be -1 if an animator is an AnimatorSet (like our FlipTransition) + // and it does not have its duration set + return createDummyAnimator(duration > 0 ? duration : 0); } } } 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 b83562e361..0b94990c03 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -96,7 +96,10 @@ function initializeNativeClasses() { const parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimationType.exitFakeResourceId); if (parentAnimator) { const duration = parentAnimator.getDuration(); - return createDummyAnimator(duration); + + // duration will be -1 if an animator is an AnimatorSet (like our FlipTransition) + // and it does not have its duration set + return createDummyAnimator(duration > 0 ? duration : 0); } } } diff --git a/tns-core-modules/ui/transition/flip-transition.android.ts b/tns-core-modules/ui/transition/flip-transition.android.ts index 612ab01177..88e61d3dff 100644 --- a/tns-core-modules/ui/transition/flip-transition.android.ts +++ b/tns-core-modules/ui/transition/flip-transition.android.ts @@ -112,6 +112,7 @@ export class FlipTransition extends Transition { } animatorSet.playTogether(objectAnimators); + animatorSet.setDuration(fullDuration); return animatorSet; } } From cb910c6275b486c891779cc7eef2872e368e58d8 Mon Sep 17 00:00:00 2001 From: Manol Donev Date: Mon, 10 Dec 2018 19:29:38 +0200 Subject: [PATCH 3/4] refactor(android): extract FragmentBase class --- tns-core-modules/ui/frame/fragment.android.ts | 41 +---------------- tns-core-modules/ui/frame/frame.android.ts | 8 +++- .../ui/tab-view/tab-view.android.ts | 45 +------------------ .../ui/transition/flip-transition.android.ts | 1 - .../android/org.nativescript.widgets.d.ts | 6 +++ 5 files changed, 15 insertions(+), 86 deletions(-) diff --git a/tns-core-modules/ui/frame/fragment.android.ts b/tns-core-modules/ui/frame/fragment.android.ts index e2b133b306..99da04835e 100644 --- a/tns-core-modules/ui/frame/fragment.android.ts +++ b/tns-core-modules/ui/frame/fragment.android.ts @@ -1,19 +1,7 @@ import { AndroidFragmentCallbacks, setFragmentCallbacks, setFragmentClass } from "./frame"; -import { AnimationType } from "./fragment.transitions"; - -function createDummyAnimator(duration: number): android.animation.Animator { - const alphaValues = Array.create("float", 2); - alphaValues[0] = 1; - alphaValues[1] = 1; - - const animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", alphaValues); - animator.setDuration(duration); - - return animator; -} @JavaProxy("com.tns.FragmentClass") -class FragmentClass extends android.support.v4.app.Fragment { +class FragmentClass extends org.nativescript.widgets.FragmentBase { // This field is updated in the frame module upon `new` (although hacky this eases the Fragment->callbacks association a lot) private _callbacks: AndroidFragmentCallbacks; @@ -27,24 +15,6 @@ class FragmentClass extends android.support.v4.app.Fragment { } public onCreateAnimator(transit: number, enter: boolean, nextAnim: number): android.animation.Animator { - // [nested frames / fragments] apply dummy animator to the nested fragment with - // the same duration as the exit animator of the removing parent fragment to work around - // https://code.google.com/p/android/issues/detail?id=55228 (child fragments disappear - // when parent fragment is removed as all children are first removed from parent) - if (!enter) { - const removingParentFragment = this.getRemovingParentFragment(); - if (removingParentFragment) { - const parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimationType.exitFakeResourceId); - if (parentAnimator) { - const duration = parentAnimator.getDuration(); - - // duration will be -1 if an animator is an AnimatorSet (like our FlipTransition) - // and it does not have its duration set - return createDummyAnimator(duration > 0 ? duration : 0); - } - } - } - return this._callbacks.onCreateAnimator(this, transit, enter, nextAnim, super.onCreateAnimator); } @@ -86,15 +56,6 @@ class FragmentClass extends android.support.v4.app.Fragment { super.toString(); } } - - private getRemovingParentFragment(): android.support.v4.app.Fragment { - let parentFragment = this.getParentFragment(); - while (parentFragment && !parentFragment.isRemoving()) { - parentFragment = parentFragment.getParentFragment(); - } - - return parentFragment; - } } setFragmentClass(FragmentClass); \ No newline at end of file diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index 417d817bb2..edfdbef65e 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -742,7 +742,13 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { } @profile - public onCreateAnimator(fragment: android.support.v4.app.Fragment, transit: number, enter: boolean, nextAnim: number, superFunc: Function): android.animation.Animator { + public onCreateAnimator(fragment: org.nativescript.widgets.FragmentBase, transit: number, enter: boolean, nextAnim: number, superFunc: Function): android.animation.Animator { + // HACK: FragmentBase class MUST handle removing nested fragment scenario to workaround + // https://code.google.com/p/android/issues/detail?id=55228 + if (!enter && fragment.getRemovingParentFragment()) { + return superFunc.call(fragment, transit, enter, nextAnim); + } + let nextAnimString: string; switch (nextAnim) { case AnimationType.enterFakeResourceId: nextAnimString = "enter"; break; 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 0b94990c03..558424242a 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -12,7 +12,6 @@ import { textTransformProperty, TextTransform, getTransformedText } from "../tex import { fromFileOrResource } from "../../image-source"; import { RESOURCE_PREFIX, ad } from "../../utils/utils"; import { Frame } from "../frame"; -import { AnimationType } from "../frame/fragment.transitions"; export * from "./tab-view-common"; @@ -41,23 +40,12 @@ function getTabById(id: number): TabView { return ref && ref.get(); } -function createDummyAnimator(duration: number): android.animation.Animator { - const alphaValues = Array.create("float", 2); - alphaValues[0] = 1; - alphaValues[1] = 1; - - const animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", alphaValues); - animator.setDuration(duration); - - return animator; -} - function initializeNativeClasses() { if (PagerAdapter) { return; } - class TabFragmentImplementation extends android.support.v4.app.Fragment { + class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { private tab: TabView; private index: number; @@ -85,42 +73,11 @@ function initializeNativeClasses() { } } - public onCreateAnimator(transit: number, enter: boolean, nextAnim: number): android.animation.Animator { - // [nested frames / fragments] apply dummy animator to the nested fragment with - // the same duration as the exit animator of the removing parent fragment to work around - // https://code.google.com/p/android/issues/detail?id=55228 (child fragments disappear - // when parent fragment is removed as all children are first removed from parent) - if (!enter) { - const removingParentFragment = this.getRemovingParentFragment(); - if (removingParentFragment) { - const parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimationType.exitFakeResourceId); - if (parentAnimator) { - const duration = parentAnimator.getDuration(); - - // duration will be -1 if an animator is an AnimatorSet (like our FlipTransition) - // and it does not have its duration set - return createDummyAnimator(duration > 0 ? duration : 0); - } - } - } - - return super.onCreateAnimator(transit, enter, nextAnim); - } - public onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View { const tabItem = this.tab.items[this.index]; return tabItem.view.nativeViewProtected; } - - private getRemovingParentFragment(): android.support.v4.app.Fragment { - let parentFragment = this.getParentFragment(); - while (parentFragment && !parentFragment.isRemoving()) { - parentFragment = parentFragment.getParentFragment(); - } - - return parentFragment; - } } const POSITION_UNCHANGED = -1; diff --git a/tns-core-modules/ui/transition/flip-transition.android.ts b/tns-core-modules/ui/transition/flip-transition.android.ts index 88e61d3dff..612ab01177 100644 --- a/tns-core-modules/ui/transition/flip-transition.android.ts +++ b/tns-core-modules/ui/transition/flip-transition.android.ts @@ -112,7 +112,6 @@ export class FlipTransition extends Transition { } animatorSet.playTogether(objectAnimators); - animatorSet.setDuration(fullDuration); return animatorSet; } } diff --git a/tns-platform-declarations/android/org.nativescript.widgets.d.ts b/tns-platform-declarations/android/org.nativescript.widgets.d.ts index d2e4462780..ae96b44f33 100644 --- a/tns-platform-declarations/android/org.nativescript.widgets.d.ts +++ b/tns-platform-declarations/android/org.nativescript.widgets.d.ts @@ -164,6 +164,12 @@ public verticalAlignment: VerticalAlignment; } + export class FragmentBase extends android.support.v4.app.Fragment { + constructor(); + + public getRemovingParentFragment(): android.support.v4.app.Fragment; + } + export enum Stretch { none, aspectFill, From c4096f91faf6e6c1b269a8b8f62e20437596a95f Mon Sep 17 00:00:00 2001 From: Manol Donev Date: Tue, 11 Dec 2018 19:20:05 +0200 Subject: [PATCH 4/4] fix rootview reset for android nested fragments --- tns-core-modules/ui/frame/frame-common.ts | 2 +- tns-core-modules/ui/frame/frame.android.ts | 11 +++++------ tns-core-modules/ui/tab-view/tab-view.android.ts | 7 ++++++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tns-core-modules/ui/frame/frame-common.ts b/tns-core-modules/ui/frame/frame-common.ts index 5533f98d1d..a92093ee87 100644 --- a/tns-core-modules/ui/frame/frame-common.ts +++ b/tns-core-modules/ui/frame/frame-common.ts @@ -457,8 +457,8 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { } public _onRootViewReset(): void { - this._removeFromFrameStack(); super._onRootViewReset(); + this._removeFromFrameStack(); } get _childrenCount(): number { diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index edfdbef65e..1f56a8eb2c 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -209,8 +209,12 @@ export class Frame extends FrameBase { } public _onRootViewReset(): void { - this.disposeCurrentFragment(); super._onRootViewReset(); + + // call this AFTER the super call to ensure descendants apply their rootview-reset logic first + // i.e. in a scenario with nested frames / frame with tabview let the descendandt cleanup the inner + // fragments first, and then cleanup the parent fragments + this.disposeCurrentFragment(); } onUnloaded() { @@ -223,11 +227,6 @@ export class Frame extends FrameBase { } private disposeCurrentFragment(): void { - // when interacting with nested fragments it seems Android is smart enough - // to automatically remove child fragments when parent fragment is removed; - // however, we must add a fragment.isAdded() guard as our logic will try to - // explicitly remove the already removed child fragment causing an - // IllegalStateException: Fragment has not been attached yet. if (!this._currentEntry || !this._currentEntry.fragment || !this._currentEntry.fragment.isAdded()) { 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 558424242a..6c2302e63a 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -555,8 +555,13 @@ export class TabView extends TabViewBase { } public _onRootViewReset(): void { - this.disposeCurrentFragments(); super._onRootViewReset(); + + // call this AFTER the super call to ensure descendants apply their rootview-reset logic first + // i.e. in a scenario with tab frames let the frames cleanup their fragments first, and then + // cleanup the tab fragments to avoid + // android.content.res.Resources$NotFoundException: Unable to find resource ID #0xfffffff6 + this.disposeCurrentFragments(); } private disposeCurrentFragments(): void {