From 63fcf3db548b62bd3112f142e7593cb215a9f06c Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Tue, 8 Oct 2019 16:22:10 +0300 Subject: [PATCH 1/9] feat: remove Animators and replace with Transitions --- .../widgets/CustomTransition.java | 133 ++++ .../nativescript/widgets/FragmentBase.java | 36 - tns-core-modules/ui/core/view/view-common.ts | 2 +- tns-core-modules/ui/core/view/view.android.ts | 2 +- tns-core-modules/ui/core/view/view.d.ts | 4 + .../ui/frame/fragment.transitions.android.ts | 690 ++++++++---------- .../ui/frame/fragment.transitions.d.ts | 141 ++-- tns-core-modules/ui/frame/frame.android.ts | 191 +++-- .../ui/transition/fade-transition.android.ts | 10 +- .../ui/transition/flip-transition.android.ts | 78 +- .../android/org.nativescript.widgets.d.ts | 11 +- 11 files changed, 680 insertions(+), 618 deletions(-) create mode 100644 tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java delete mode 100644 tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java new file mode 100644 index 0000000000..a0f3b5cf57 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java @@ -0,0 +1,133 @@ +package org.nativescript.widgets; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.transition.Transition; +import androidx.transition.TransitionListenerAdapter; +import androidx.transition.TransitionValues; +import androidx.transition.Visibility; + +import java.util.ArrayList; + +public class CustomTransition extends Visibility { + private boolean resetOnTransitionEnd; + private AnimatorSet animatorSet; + private AnimatorSet immediateAnimatorSet; + private String transitionName; + + public CustomTransition(AnimatorSet animatorSet, String transitionName) { + this.animatorSet = animatorSet; + this.transitionName = transitionName; + } + + @Nullable + @Override + public Animator onAppear(@NonNull ViewGroup sceneRoot, @NonNull final View view, @Nullable TransitionValues startValues, + @Nullable TransitionValues endValues) { + if (endValues == null || view == null || this.animatorSet == null) { + return null; + } + + return this.setAnimatorsTarget(this.animatorSet, view); + } + + @Override + public Animator onDisappear(@NonNull ViewGroup sceneRoot, @NonNull final View view, @Nullable TransitionValues startValues, + @Nullable TransitionValues endValues) { + if (startValues == null || view == null || this.animatorSet == null) { + return null; + } + + return this.setAnimatorsTarget(this.animatorSet, view); + } + + public void setResetOnTransitionEnd(boolean resetOnTransitionEnd){ + this.resetOnTransitionEnd = resetOnTransitionEnd; + } + + public String getTransitionName(){ + return this.transitionName; + } + + private Animator setAnimatorsTarget(AnimatorSet animatorSet, final View view){ + ArrayList animatorsList = animatorSet.getChildAnimations(); + boolean resetOnTransitionEnd = this.resetOnTransitionEnd; + + for (int i = 0; i < animatorsList.size(); i++) { + animatorsList.get(i).setTarget(view); + } + + if (this.resetOnTransitionEnd) { + this.immediateAnimatorSet = this.animatorSet.clone(); + } + + CustomAnimatorListener listener = new CustomAnimatorListener(view); + animatorSet.addListener(listener); + addListener(new CustomTransitionListenerAdapter(this)); + + return this.animatorSet; + } + + private class ReverseInterpolator implements Interpolator { + @Override + public float getInterpolation(float paramFloat) { + return Math.abs(paramFloat - 1f); + } + } + + private class CustomTransitionListenerAdapter extends TransitionListenerAdapter { + private CustomTransition customTransition; + + CustomTransitionListenerAdapter(CustomTransition transition) { + this.customTransition = transition; + } + + @Override + public void onTransitionEnd(@NonNull Transition transition) { + if (this.customTransition.resetOnTransitionEnd) { + this.customTransition.immediateAnimatorSet.setDuration(0); + this.customTransition.immediateAnimatorSet.setInterpolator(new ReverseInterpolator()); + this.customTransition.immediateAnimatorSet.start(); + this.customTransition.setResetOnTransitionEnd(false); + } + + this.customTransition.immediateAnimatorSet = null; + this.customTransition = null; + transition.removeListener(this); + } + } + + private static class CustomAnimatorListener extends AnimatorListenerAdapter { + + private final View mView; + private boolean mLayerTypeChanged = false; + + CustomAnimatorListener(View view) { + mView = view; + } + + @Override + public void onAnimationStart(Animator animation) { + if (ViewCompat.hasOverlappingRendering(mView) + && mView.getLayerType() == View.LAYER_TYPE_NONE) { + mLayerTypeChanged = true; + mView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mLayerTypeChanged) { + mView.setLayerType(View.LAYER_TYPE_NONE, null); + } + } + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java deleted file mode 100644 index 88a2217196..0000000000 --- a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.nativescript.widgets; - -import android.animation.Animator; -import androidx.fragment.app.Fragment; - -public abstract class FragmentBase extends Fragment { - - @Override - public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { - // [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) { - Fragment removingParentFragment = this.getRemovingParentFragment(); - if (removingParentFragment != null) { - Animator parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimatorHelper.exitFakeResourceId); - if (parentAnimator != null) { - long duration = AnimatorHelper.getTotalDuration(parentAnimator); - return AnimatorHelper.createDummyAnimator(duration); - } - } - } - - return super.onCreateAnimator(transit, enter, nextAnim); - } - - public Fragment getRemovingParentFragment() { - Fragment parentFragment = this.getParentFragment(); - while (parentFragment != null && !parentFragment.isRemoving()) { - parentFragment = parentFragment.getParentFragment(); - } - - return parentFragment; - } -} diff --git a/tns-core-modules/ui/core/view/view-common.ts b/tns-core-modules/ui/core/view/view-common.ts index 4e93523583..1cc1f90173 100644 --- a/tns-core-modules/ui/core/view/view-common.ts +++ b/tns-core-modules/ui/core/view/view-common.ts @@ -84,7 +84,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { public static showingModallyEvent = "showingModally"; protected _closeModalCallback: Function; - + public _manager: any; public _modalParent: ViewCommon; private _modalContext: any; private _modal: ViewCommon; diff --git a/tns-core-modules/ui/core/view/view.android.ts b/tns-core-modules/ui/core/view/view.android.ts index 37ccf1f8e6..7d6d6e20ae 100644 --- a/tns-core-modules/ui/core/view/view.android.ts +++ b/tns-core-modules/ui/core/view/view.android.ts @@ -272,12 +272,12 @@ export class View extends ViewCommon { public static androidBackPressedEvent = androidBackPressedEvent; public _dialogFragment: androidx.fragment.app.DialogFragment; + public _manager: androidx.fragment.app.FragmentManager; private _isClickable: boolean; private touchListenerIsSet: boolean; private touchListener: android.view.View.OnTouchListener; private layoutChangeListenerIsSet: boolean; private layoutChangeListener: android.view.View.OnLayoutChangeListener; - private _manager: androidx.fragment.app.FragmentManager; private _rootManager: androidx.fragment.app.FragmentManager; nativeViewProtected: android.view.View; diff --git a/tns-core-modules/ui/core/view/view.d.ts b/tns-core-modules/ui/core/view/view.d.ts index 2eafa9694a..34086eee85 100644 --- a/tns-core-modules/ui/core/view/view.d.ts +++ b/tns-core-modules/ui/core/view/view.d.ts @@ -643,6 +643,10 @@ export abstract class View extends ViewBase { * @private */ _gestureObservers: any; + /** + * @private + */ + _manager: androidx.fragment.app.FragmentManager /** * @private */ diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts index 85e905b800..78b1759d39 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -3,57 +3,19 @@ // Definitions. import { NavigationType } from "./frame-common"; import { NavigationTransition, BackstackEntry } from "../frame"; -import { AnimationType } from "./fragment.transitions.types"; +import { ExpandedEntry, ExpandedTransitionListener, ExpandedAnimator } from "./fragment.transitions"; // Types. import { Transition, AndroidTransitionType } from "../transition/transition"; -import { SlideTransition } from "../transition/slide-transition"; -import { FadeTransition } from "../transition/fade-transition"; import { FlipTransition } from "../transition/flip-transition"; import { _resolveAnimationCurve } from "../animation"; -import { device } from "../../platform"; import lazy from "../../utils/lazy"; - import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories } from "../../trace"; -export { AnimationType } from "./fragment.transitions.types"; - interface TransitionListener { - new(entry: ExpandedEntry, transition: android.transition.Transition): ExpandedTransitionListener; -} - -interface ExpandedAnimator extends android.animation.Animator { - entry: ExpandedEntry; - transitionType?: string; -} - -interface ExpandedTransitionListener extends android.transition.Transition.TransitionListener { - entry: ExpandedEntry; - transition: android.transition.Transition; + new(entry: ExpandedEntry, transition: androidx.transition.Transition): ExpandedTransitionListener; } -interface ExpandedEntry extends BackstackEntry { - enterTransitionListener: ExpandedTransitionListener; - exitTransitionListener: ExpandedTransitionListener; - reenterTransitionListener: ExpandedTransitionListener; - returnTransitionListener: ExpandedTransitionListener; - - enterAnimator: ExpandedAnimator; - exitAnimator: ExpandedAnimator; - popEnterAnimator: ExpandedAnimator; - popExitAnimator: ExpandedAnimator; - - defaultEnterAnimator: ExpandedAnimator; - defaultExitAnimator: ExpandedAnimator; - - transition: Transition; - transitionName: string; - frameId: number; - useLollipopTransition: boolean; -} - -const sdkVersion = lazy(() => parseInt(device.sdkVersion)); -const intEvaluator = lazy(() => new android.animation.IntEvaluator()); const defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator()); export const waitingQueue = new Map>(); @@ -67,8 +29,9 @@ export function _setAndroidFragmentTransitions( navigationTransition: NavigationTransition, currentEntry: ExpandedEntry, newEntry: ExpandedEntry, - fragmentTransaction: androidx.fragment.app.FragmentTransaction, - frameId: number): void { + frameId: number, + fragmentTransaction: any, + isNestedDefaultTransition?: boolean): void { const currentFragment: androidx.fragment.app.Fragment = currentEntry ? currentEntry.fragment : null; const newFragment: androidx.fragment.app.Fragment = newEntry.fragment; @@ -77,10 +40,8 @@ export function _setAndroidFragmentTransitions( throw new Error("Calling navigation before previous navigation finish."); } - if (sdkVersion() >= 21) { - allowTransitionOverlap(currentFragment); - allowTransitionOverlap(newFragment); - } + allowTransitionOverlap(currentFragment); + allowTransitionOverlap(newFragment); let name = ""; let transition: Transition; @@ -90,29 +51,11 @@ export function _setAndroidFragmentTransitions( name = navigationTransition.name ? navigationTransition.name.toLowerCase() : ""; } - 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; - } - - newEntry.useLollipopTransition = useLollipopTransition; - if (!animated) { name = "none"; } else if (transition) { name = "custom"; - // specifiying transition should override default one even if name match the lollipop transition name. - useLollipopTransition = false; - } else if (!useLollipopTransition && name.indexOf("slide") !== 0 && name !== "fade" && name.indexOf("flip") !== 0) { + } else if (name.indexOf("slide") !== 0 && name !== "fade" && name.indexOf("flip") !== 0 && name.indexOf("explode") !== 0) { // If we are given name that doesn't match any of ours - fallback to default. name = "default"; } @@ -121,68 +64,66 @@ export function _setAndroidFragmentTransitions( if (currentEntry) { _updateTransitions(currentEntry); if (currentEntry.transitionName !== name || - currentEntry.transition !== transition || - !!currentEntry.useLollipopTransition !== useLollipopTransition || - !useLollipopTransition) { + currentEntry.transition !== transition || isNestedDefaultTransition) { clearExitAndReenterTransitions(currentEntry, true); currentFragmentNeedsDifferentAnimation = true; } } if (name === "none") { - transition = new NoTransition(0, null); + const noTransition = new NoTransition(0, null); + + if (isNestedDefaultTransition) { + fragmentTransaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out); + setupAllAnimation(newEntry, noTransition); + setupNewFragmentCustomTransition({ duration: 0, curve: null }, newEntry, noTransition); + } else { + setupNewFragmentCustomTransition({ duration: 0, curve: null }, newEntry, noTransition); + } + + newEntry.isNestedDefaultTransition = isNestedDefaultTransition; + + if (currentFragmentNeedsDifferentAnimation) { + setupCurrentFragmentCustomTransition({ duration: 0, curve: null }, currentEntry, noTransition); + } + } else if (name === "custom") { + setupNewFragmentCustomTransition({ duration: transition.getDuration(), curve: transition.getCurve() }, newEntry, transition); + if (currentFragmentNeedsDifferentAnimation) { + setupCurrentFragmentCustomTransition({ duration: transition.getDuration(), curve: transition.getCurve() }, currentEntry, transition); + } } else if (name === "default") { - transition = new FadeTransition(150, null); - } else if (useLollipopTransition) { - // setEnterTransition: Enter - // setExitTransition: Exit - // setReenterTransition: Pop Enter, same as Exit if not specified - // setReturnTransition: Pop Exit, same as Enter if not specified - - if (name.indexOf("slide") === 0) { - setupNewFragmentSlideTransition(navigationTransition, newEntry, name); - if (currentFragmentNeedsDifferentAnimation) { - setupCurrentFragmentSlideTransition(navigationTransition, currentEntry, name); - } - } else if (name === "fade") { - setupNewFragmentFadeTransition(navigationTransition, newEntry); - if (currentFragmentNeedsDifferentAnimation) { - setupCurrentFragmentFadeTransition(navigationTransition, currentEntry); - } - } else if (name === "explode") { - setupNewFragmentExplodeTransition(navigationTransition, newEntry); - if (currentFragmentNeedsDifferentAnimation) { - setupCurrentFragmentExplodeTransition(navigationTransition, currentEntry); - } + setupNewFragmentFadeTransition({ duration: 150, curve: null }, newEntry); + if (currentFragmentNeedsDifferentAnimation) { + setupCurrentFragmentFadeTransition({ duration: 150, curve: null }, currentEntry); } } else if (name.indexOf("slide") === 0) { - const direction = name.substr("slide".length) || "left"; //Extract the direction from the string - transition = new SlideTransition(direction, navigationTransition.duration, navigationTransition.curve); + setupNewFragmentSlideTransition(navigationTransition, newEntry, name); + if (currentFragmentNeedsDifferentAnimation) { + setupCurrentFragmentSlideTransition(navigationTransition, currentEntry, name); + } } else if (name === "fade") { - transition = new FadeTransition(navigationTransition.duration, navigationTransition.curve); - } else if (name.indexOf("flip") === 0) { - const direction = name.substr("flip".length) || "right"; //Extract the direction from the string - transition = new FlipTransition(direction, navigationTransition.duration, navigationTransition.curve); - } - - newEntry.transitionName = name; - if (name === "custom") { - newEntry.transition = transition; - } - - // Having transition means we have custom animation - if (transition) { - if (fragmentTransaction) { - // we do not use Android backstack so setting popEnter / popExit is meaningless (3rd and 4th optional args) - fragmentTransaction.setCustomAnimations(AnimationType.enterFakeResourceId, AnimationType.exitFakeResourceId); + setupNewFragmentFadeTransition(navigationTransition, newEntry); + if (currentFragmentNeedsDifferentAnimation) { + setupCurrentFragmentFadeTransition(navigationTransition, currentEntry); + } + } else if (name === "explode") { + setupNewFragmentExplodeTransition(navigationTransition, newEntry); + if (currentFragmentNeedsDifferentAnimation) { + setupCurrentFragmentExplodeTransition(navigationTransition, currentEntry); } + } else if (name === "flip") { + console.log("USE NEW FLIP: New Fragment" + navigationTransition.duration); + const direction = name.substr("flip".length) || "right"; //Extract the direction from the string + const flipTransition = new FlipTransition(direction, navigationTransition.duration, navigationTransition.curve); - setupAllAnimation(newEntry, transition); + setupNewFragmentCustomTransition(navigationTransition, newEntry, flipTransition); if (currentFragmentNeedsDifferentAnimation) { - setupExitAndPopEnterAnimation(currentEntry, transition); + setupCurrentFragmentCustomTransition(navigationTransition, currentEntry, flipTransition); } } + newEntry.transitionName = name; + if (currentEntry) { currentEntry.transitionName = name; if (name === "custom") { @@ -190,45 +131,108 @@ export function _setAndroidFragmentTransitions( } } - setupDefaultAnimations(newEntry, new FadeTransition(150, null)); - printTransitions(currentEntry); printTransitions(newEntry); } -export function _onFragmentCreateAnimator(entry: ExpandedEntry, fragment: androidx.fragment.app.Fragment, nextAnim: number, enter: boolean): android.animation.Animator { - let animator: android.animation.Animator; - switch (nextAnim) { - case AnimationType.enterFakeResourceId: - animator = entry.enterAnimator || entry.defaultEnterAnimator /* HACK */; - break; +function setupAllAnimation(entry: ExpandedEntry, transition: Transition): void { + setupExitAndPopEnterAnimation(entry, transition); + const listener = getAnimationListener(); - case AnimationType.exitFakeResourceId: - animator = entry.exitAnimator || entry.defaultExitAnimator /* HACK */; - break; + // setupAllAnimation is called only for new fragments so we don't + // need to clearAnimationListener for enter & popExit animators. + const enterAnimator = transition.createAndroidAnimator(AndroidTransitionType.enter); + enterAnimator.transitionType = AndroidTransitionType.enter; + enterAnimator.entry = entry; + enterAnimator.addListener(listener); + entry.enterAnimator = enterAnimator; - case AnimationType.popEnterFakeResourceId: - animator = entry.popEnterAnimator; - break; + const popExitAnimator = transition.createAndroidAnimator(AndroidTransitionType.popExit); + popExitAnimator.transitionType = AndroidTransitionType.popExit; + popExitAnimator.entry = entry; + popExitAnimator.addListener(listener); + entry.popExitAnimator = popExitAnimator; +} - case AnimationType.popExitFakeResourceId: - animator = entry.popExitAnimator; - break; - } +function setupExitAndPopEnterAnimation(entry: ExpandedEntry, transition: Transition): void { + const listener = getAnimationListener(); + + // remove previous listener if we are changing the animator. + clearAnimationListener(entry.exitAnimator, listener); + clearAnimationListener(entry.popEnterAnimator, listener); + + const exitAnimator = transition.createAndroidAnimator(AndroidTransitionType.exit); + exitAnimator.transitionType = AndroidTransitionType.exit; + exitAnimator.entry = entry; + exitAnimator.addListener(listener); + entry.exitAnimator = exitAnimator; - if (!animator && sdkVersion() >= 21) { - const view = fragment.getView(); - const jsParent = entry.resolvedPage.parent; - const parent = view.getParent() || (jsParent && jsParent.nativeViewProtected); - const animatedEntries = _getAnimatedEntries(entry.frameId); - if (!animatedEntries || !animatedEntries.has(entry)) { - if (parent && !(parent).isLaidOut()) { - animator = enter ? entry.defaultEnterAnimator : entry.defaultExitAnimator; + const popEnterAnimator = transition.createAndroidAnimator(AndroidTransitionType.popEnter); + popEnterAnimator.transitionType = AndroidTransitionType.popEnter; + popEnterAnimator.entry = entry; + popEnterAnimator.addListener(listener); + entry.popEnterAnimator = popEnterAnimator; +} + +function getAnimationListener(): android.animation.Animator.AnimatorListener { + if (!AnimationListener) { + @Interfaces([android.animation.Animator.AnimatorListener]) + class AnimationListenerImpl extends java.lang.Object implements android.animation.Animator.AnimatorListener { + constructor() { + super(); + + return global.__native(this); + } + + onAnimationStart(animator: ExpandedAnimator): void { + const entry = animator.entry; + console.log("Animation start " + animator.getDuration()); + addToWaitingQueue(entry); + if (traceEnabled()) { + traceWrite(`START ${animator.transitionType} for ${entry.fragmentTag}`, traceCategories.Transition); + } + } + + onAnimationRepeat(animator: ExpandedAnimator): void { + if (traceEnabled()) { + traceWrite(`REPEAT ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); + } + } + + onAnimationEnd(animator: ExpandedAnimator): void { + if (traceEnabled()) { + traceWrite(`END ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); + } + console.log("Animation end " + animator.getDuration()); + transitionOrAnimationCompleted(animator.entry); + } + + onAnimationCancel(animator: ExpandedAnimator): void { + if (traceEnabled()) { + traceWrite(`CANCEL ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); + } } } + + AnimationListener = new AnimationListenerImpl(); + } + + return AnimationListener; +} + +function clearAnimationListener(animator: ExpandedAnimator, listener: android.animation.Animator.AnimatorListener): void { + if (!animator) { + return; + } + + animator.removeListener(listener); + + if (animator.entry && traceEnabled()) { + const entry = animator.entry; + traceWrite(`Clear ${animator.transitionType} - ${entry.transition} for ${entry.fragmentTag}`, traceCategories.Transition); } - return animator; + animator.entry = null; } export function _getAnimatedEntries(frameId: number): Set { @@ -262,22 +266,21 @@ export function _reverseTransitions(previousEntry: ExpandedEntry, currentEntry: const previousFragment = previousEntry.fragment; const currentFragment = currentEntry.fragment; let transitionUsed = false; - if (sdkVersion() >= 21) { - const returnTransitionListener = currentEntry.returnTransitionListener; - if (returnTransitionListener) { - transitionUsed = true; - currentFragment.setExitTransition(returnTransitionListener.transition); - } else { - currentFragment.setExitTransition(null); - } - const reenterTransitionListener = previousEntry.reenterTransitionListener; - if (reenterTransitionListener) { - transitionUsed = true; - previousFragment.setEnterTransition(reenterTransitionListener.transition); - } else { - previousFragment.setEnterTransition(null); - } + const returnTransitionListener = currentEntry.returnTransitionListener; + if (returnTransitionListener) { + transitionUsed = true; + currentFragment.setExitTransition(returnTransitionListener.transition); + } else { + currentFragment.setExitTransition(null); + } + + const reenterTransitionListener = previousEntry.reenterTransitionListener; + if (reenterTransitionListener) { + transitionUsed = true; + previousFragment.setEnterTransition(reenterTransitionListener.transition); + } else { + previousFragment.setEnterTransition(null); } return transitionUsed; @@ -285,17 +288,17 @@ export function _reverseTransitions(previousEntry: ExpandedEntry, currentEntry: // Transition listener can't be static because // android is cloning transitions and we can't expand them :( -function getTransitionListener(entry: ExpandedEntry, transition: android.transition.Transition): ExpandedTransitionListener { +function getTransitionListener(entry: ExpandedEntry, transition: androidx.transition.Transition): ExpandedTransitionListener { if (!TransitionListener) { - @Interfaces([(android).transition.Transition.TransitionListener]) - class TransitionListenerImpl extends java.lang.Object implements android.transition.Transition.TransitionListener { - constructor(public entry: ExpandedEntry, public transition: android.transition.Transition) { + @Interfaces([(androidx).transition.Transition.TransitionListener]) + class TransitionListenerImpl extends java.lang.Object implements androidx.transition.Transition.TransitionListener { + constructor(public entry: ExpandedEntry, public transition: androidx.transition.Transition) { super(); return global.__native(this); } - public onTransitionStart(transition: android.transition.Transition): void { + public onTransitionStart(transition: androidx.transition.Transition): void { const entry = this.entry; addToWaitingQueue(entry); if (traceEnabled()) { @@ -303,7 +306,7 @@ function getTransitionListener(entry: ExpandedEntry, transition: android.transit } } - onTransitionEnd(transition: android.transition.Transition): void { + onTransitionEnd(transition: androidx.transition.Transition): void { const entry = this.entry; if (traceEnabled()) { traceWrite(`END ${toShortString(transition)} transition for ${entry.fragmentTag}`, traceCategories.Transition); @@ -312,20 +315,20 @@ function getTransitionListener(entry: ExpandedEntry, transition: android.transit transitionOrAnimationCompleted(entry); } - onTransitionResume(transition: android.transition.Transition): void { + onTransitionResume(transition: androidx.transition.Transition): void { if (traceEnabled()) { const fragment = this.entry.fragmentTag; traceWrite(`RESUME ${toShortString(transition)} transition for ${fragment}`, traceCategories.Transition); } } - onTransitionPause(transition: android.transition.Transition): void { + onTransitionPause(transition: androidx.transition.Transition): void { if (traceEnabled()) { traceWrite(`PAUSE ${toShortString(transition)} transition for ${this.entry.fragmentTag}`, traceCategories.Transition); } } - onTransitionCancel(transition: android.transition.Transition): void { + onTransitionCancel(transition: androidx.transition.Transition): void { if (traceEnabled()) { traceWrite(`CANCEL ${toShortString(transition)} transition for ${this.entry.fragmentTag}`, traceCategories.Transition); } @@ -338,51 +341,6 @@ function getTransitionListener(entry: ExpandedEntry, transition: android.transit return new TransitionListener(entry, transition); } -function getAnimationListener(): android.animation.Animator.AnimatorListener { - if (!AnimationListener) { - @Interfaces([android.animation.Animator.AnimatorListener]) - class AnimationListenerImpl extends java.lang.Object implements android.animation.Animator.AnimatorListener { - constructor() { - super(); - - return global.__native(this); - } - - onAnimationStart(animator: ExpandedAnimator): void { - const entry = animator.entry; - addToWaitingQueue(entry); - if (traceEnabled()) { - traceWrite(`START ${animator.transitionType} for ${entry.fragmentTag}`, traceCategories.Transition); - } - } - - onAnimationRepeat(animator: ExpandedAnimator): void { - if (traceEnabled()) { - traceWrite(`REPEAT ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); - } - } - - onAnimationEnd(animator: ExpandedAnimator): void { - if (traceEnabled()) { - traceWrite(`END ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); - } - - transitionOrAnimationCompleted(animator.entry); - } - - onAnimationCancel(animator: ExpandedAnimator): void { - if (traceEnabled()) { - traceWrite(`CANCEL ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); - } - } - } - - AnimationListener = new AnimationListenerImpl(); - } - - return AnimationListener; -} - function addToWaitingQueue(entry: ExpandedEntry): void { const frameId = entry.frameId; let entries = waitingQueue.get(frameId); @@ -394,61 +352,44 @@ function addToWaitingQueue(entry: ExpandedEntry): void { entries.add(entry); } -function clearAnimationListener(animator: ExpandedAnimator, listener: android.animation.Animator.AnimatorListener): void { - if (!animator) { - return; - } - - animator.removeListener(listener); - - if (animator.entry && traceEnabled()) { - const entry = animator.entry; - traceWrite(`Clear ${animator.transitionType} - ${entry.transition} for ${entry.fragmentTag}`, traceCategories.Transition); - } - - animator.entry = null; -} - function clearExitAndReenterTransitions(entry: ExpandedEntry, removeListener: boolean): void { - if (sdkVersion() >= 21) { - const fragment: androidx.fragment.app.Fragment = entry.fragment; - const exitListener = entry.exitTransitionListener; - if (exitListener) { - const exitTransition = fragment.getExitTransition(); - if (exitTransition) { - if (removeListener) { - exitTransition.removeListener(exitListener); - } - - fragment.setExitTransition(null); - if (traceEnabled()) { - traceWrite(`Cleared Exit ${exitTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition); - } + const fragment: androidx.fragment.app.Fragment = entry.fragment; + const exitListener = entry.exitTransitionListener; + if (exitListener) { + const exitTransition = fragment.getExitTransition(); + if (exitTransition) { + if (removeListener) { + exitTransition.removeListener(exitListener); } - if (removeListener) { - entry.exitTransitionListener = null; + fragment.setExitTransition(null); + if (traceEnabled()) { + traceWrite(`Cleared Exit ${exitTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition); } } - const reenterListener = entry.reenterTransitionListener; - if (reenterListener) { - const reenterTransition = fragment.getReenterTransition(); - if (reenterTransition) { - if (removeListener) { - reenterTransition.removeListener(reenterListener); - } + if (removeListener) { + entry.exitTransitionListener = null; + } + } - fragment.setReenterTransition(null); - if (traceEnabled()) { - traceWrite(`Cleared Reenter ${reenterTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition); - } + const reenterListener = entry.reenterTransitionListener; + if (reenterListener) { + const reenterTransition = fragment.getReenterTransition(); + if (reenterTransition) { + if (removeListener) { + reenterTransition.removeListener(reenterListener); } - if (removeListener) { - entry.reenterTransitionListener = null; + fragment.setReenterTransition(null); + if (traceEnabled()) { + traceWrite(`Cleared Reenter ${reenterTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition); } } + + if (removeListener) { + entry.reenterTransitionListener = null; + } } } @@ -463,55 +404,43 @@ export function _clearEntry(entry: ExpandedEntry): void { function clearEntry(entry: ExpandedEntry, removeListener: boolean): void { clearExitAndReenterTransitions(entry, removeListener); - if (sdkVersion() >= 21) { - const fragment: androidx.fragment.app.Fragment = entry.fragment; - const enterListener = entry.enterTransitionListener; - if (enterListener) { - const enterTransition = fragment.getEnterTransition(); - if (enterTransition) { - if (removeListener) { - enterTransition.removeListener(enterListener); - } - - fragment.setEnterTransition(null); - if (traceEnabled()) { - traceWrite(`Cleared Enter ${enterTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition); - } + const fragment: androidx.fragment.app.Fragment = entry.fragment; + const enterListener = entry.enterTransitionListener; + if (enterListener) { + const enterTransition = fragment.getEnterTransition(); + if (enterTransition) { + if (removeListener) { + enterTransition.removeListener(enterListener); } - if (removeListener) { - entry.enterTransitionListener = null; + fragment.setEnterTransition(null); + if (traceEnabled()) { + traceWrite(`Cleared Enter ${enterTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition); } } - const returnListener = entry.returnTransitionListener; - if (returnListener) { - const returnTransition = fragment.getReturnTransition(); - if (returnTransition) { - if (removeListener) { - returnTransition.removeListener(returnListener); - } + if (removeListener) { + entry.enterTransitionListener = null; + } + } - fragment.setReturnTransition(null); - if (traceEnabled()) { - traceWrite(`Cleared Return ${returnTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition); - } + const returnListener = entry.returnTransitionListener; + if (returnListener) { + const returnTransition = fragment.getReturnTransition(); + if (returnTransition) { + if (removeListener) { + returnTransition.removeListener(returnListener); } - if (removeListener) { - entry.returnTransitionListener = null; + fragment.setReturnTransition(null); + if (traceEnabled()) { + traceWrite(`Cleared Return ${returnTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition); } } - } - if (removeListener) { - const listener = getAnimationListener(); - clearAnimationListener(entry.enterAnimator, listener); - clearAnimationListener(entry.exitAnimator, listener); - clearAnimationListener(entry.popEnterAnimator, listener); - clearAnimationListener(entry.popExitAnimator, listener); - clearAnimationListener(entry.defaultEnterAnimator, listener); - clearAnimationListener(entry.defaultExitAnimator, listener); + if (removeListener) { + entry.returnTransitionListener = null; + } } } @@ -522,7 +451,7 @@ function allowTransitionOverlap(fragment: androidx.fragment.app.Fragment): void } } -function setEnterTransition(navigationTransition: NavigationTransition, entry: ExpandedEntry, transition: android.transition.Transition): void { +function setEnterTransition(navigationTransition: NavigationTransition, entry: ExpandedEntry, transition: androidx.transition.Transition): void { setUpNativeTransition(navigationTransition, transition); const listener = addNativeTransitionListener(entry, transition); @@ -532,7 +461,7 @@ function setEnterTransition(navigationTransition: NavigationTransition, entry: E fragment.setEnterTransition(transition); } -function setExitTransition(navigationTransition: NavigationTransition, entry: ExpandedEntry, transition: android.transition.Transition): void { +function setExitTransition(navigationTransition: NavigationTransition, entry: ExpandedEntry, transition: androidx.transition.Transition): void { setUpNativeTransition(navigationTransition, transition); const listener = addNativeTransitionListener(entry, transition); @@ -542,7 +471,7 @@ function setExitTransition(navigationTransition: NavigationTransition, entry: Ex fragment.setExitTransition(transition); } -function setReenterTransition(navigationTransition: NavigationTransition, entry: ExpandedEntry, transition: android.transition.Transition): void { +function setReenterTransition(navigationTransition: NavigationTransition, entry: ExpandedEntry, transition: androidx.transition.Transition): void { setUpNativeTransition(navigationTransition, transition); const listener = addNativeTransitionListener(entry, transition); @@ -552,7 +481,7 @@ function setReenterTransition(navigationTransition: NavigationTransition, entry: fragment.setReenterTransition(transition); } -function setReturnTransition(navigationTransition: NavigationTransition, entry: ExpandedEntry, transition: android.transition.Transition): void { +function setReturnTransition(navigationTransition: NavigationTransition, entry: ExpandedEntry, transition: androidx.transition.Transition): void { setUpNativeTransition(navigationTransition, transition); const listener = addNativeTransitionListener(entry, transition); @@ -567,23 +496,23 @@ function setupNewFragmentSlideTransition(navTransition: NavigationTransition, en const direction = name.substr("slide".length) || "left"; //Extract the direction from the string switch (direction) { case "left": - setEnterTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.RIGHT)); - setReturnTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.RIGHT)); + setEnterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.RIGHT)); + setReturnTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.RIGHT)); break; case "right": - setEnterTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.LEFT)); - setReturnTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.LEFT)); + setEnterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.LEFT)); + setReturnTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.LEFT)); break; case "top": - setEnterTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.BOTTOM)); - setReturnTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.BOTTOM)); + setEnterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.BOTTOM)); + setReturnTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.BOTTOM)); break; case "bottom": - setEnterTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.TOP)); - setReturnTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.TOP)); + setEnterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.TOP)); + setReturnTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.TOP)); break; } } @@ -592,115 +521,85 @@ function setupCurrentFragmentSlideTransition(navTransition: NavigationTransition const direction = name.substr("slide".length) || "left"; //Extract the direction from the string switch (direction) { case "left": - setExitTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.LEFT)); - setReenterTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.LEFT)); + setExitTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.LEFT)); + setReenterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.LEFT)); break; case "right": - setExitTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.RIGHT)); - setReenterTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.RIGHT)); + setExitTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.RIGHT)); + setReenterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.RIGHT)); break; case "top": - setExitTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.TOP)); - setReenterTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.TOP)); + setExitTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.TOP)); + setReenterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.TOP)); break; case "bottom": - setExitTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.BOTTOM)); - setReenterTransition(navTransition, entry, new android.transition.Slide(android.view.Gravity.BOTTOM)); + setExitTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.BOTTOM)); + setReenterTransition(navTransition, entry, new androidx.transition.Slide(android.view.Gravity.BOTTOM)); break; } } +function setupCurrentFragmentCustomTransition(navTransition: NavigationTransition, entry: ExpandedEntry, transition: Transition): void { + const exitAnimator = transition.createAndroidAnimator(AndroidTransitionType.exit); + const exitTransition = new org.nativescript.widgets.CustomTransition(exitAnimator, transition.constructor.name + AndroidTransitionType.exit.toString()); + + setExitTransition(navTransition, entry, exitTransition); + + const reenterAnimator = transition.createAndroidAnimator(AndroidTransitionType.popEnter); + const reenterTransition = new org.nativescript.widgets.CustomTransition(reenterAnimator, transition.constructor.name + AndroidTransitionType.popEnter.toString()); + + setReenterTransition(navTransition, entry, reenterTransition); +} + +function setupNewFragmentCustomTransition(navTransition: NavigationTransition, entry: ExpandedEntry, transition: Transition): void { + setupCurrentFragmentCustomTransition(navTransition, entry, transition); + + const enterAnimator = transition.createAndroidAnimator(AndroidTransitionType.enter); + const enterTransition = new org.nativescript.widgets.CustomTransition(enterAnimator, transition.constructor.name + AndroidTransitionType.enter.toString()); + setEnterTransition(navTransition, entry, enterTransition); + + const returnAnimator = transition.createAndroidAnimator(AndroidTransitionType.popExit); + const returnTransition = new org.nativescript.widgets.CustomTransition(returnAnimator, transition.constructor.name + AndroidTransitionType.popExit.toString()); + setReturnTransition(navTransition, entry, returnTransition); + +} + function setupNewFragmentFadeTransition(navTransition: NavigationTransition, entry: ExpandedEntry): void { setupCurrentFragmentFadeTransition(navTransition, entry); - const fadeInEnter = new android.transition.Fade(android.transition.Fade.IN); + const fadeInEnter = new androidx.transition.Fade(androidx.transition.Fade.IN); setEnterTransition(navTransition, entry, fadeInEnter); - const fadeOutReturn = new android.transition.Fade(android.transition.Fade.OUT); + const fadeOutReturn = new androidx.transition.Fade(androidx.transition.Fade.OUT); setReturnTransition(navTransition, entry, fadeOutReturn); } function setupCurrentFragmentFadeTransition(navTransition: NavigationTransition, entry: ExpandedEntry): void { - const fadeOutExit = new android.transition.Fade(android.transition.Fade.OUT); + const fadeOutExit = new androidx.transition.Fade(androidx.transition.Fade.OUT); setExitTransition(navTransition, entry, fadeOutExit); // NOTE: There is a bug in Fade transition so we need to set all 4 // otherwise back navigation will complete immediately (won't run the reverse transition). - const fadeInReenter = new android.transition.Fade(android.transition.Fade.IN); + const fadeInReenter = new androidx.transition.Fade(androidx.transition.Fade.IN); setReenterTransition(navTransition, entry, fadeInReenter); } function setupCurrentFragmentExplodeTransition(navTransition: NavigationTransition, entry: ExpandedEntry): void { - setExitTransition(navTransition, entry, new android.transition.Explode()); - setReenterTransition(navTransition, entry, new android.transition.Explode()); + setExitTransition(navTransition, entry, new androidx.transition.Explode()); + setReenterTransition(navTransition, entry, new androidx.transition.Explode()); } function setupNewFragmentExplodeTransition(navTransition: NavigationTransition, entry: ExpandedEntry): void { setupCurrentFragmentExplodeTransition(navTransition, entry); - setEnterTransition(navTransition, entry, new android.transition.Explode()); - setReturnTransition(navTransition, entry, new android.transition.Explode()); -} - -function setupExitAndPopEnterAnimation(entry: ExpandedEntry, transition: Transition): void { - const listener = getAnimationListener(); - - // remove previous listener if we are changing the animator. - clearAnimationListener(entry.exitAnimator, listener); - clearAnimationListener(entry.popEnterAnimator, listener); - - const exitAnimator = transition.createAndroidAnimator(AndroidTransitionType.exit); - exitAnimator.transitionType = AndroidTransitionType.exit; - exitAnimator.entry = entry; - exitAnimator.addListener(listener); - entry.exitAnimator = exitAnimator; - - const popEnterAnimator = transition.createAndroidAnimator(AndroidTransitionType.popEnter); - popEnterAnimator.transitionType = AndroidTransitionType.popEnter; - popEnterAnimator.entry = entry; - popEnterAnimator.addListener(listener); - entry.popEnterAnimator = popEnterAnimator; + setEnterTransition(navTransition, entry, new androidx.transition.Explode()); + setReturnTransition(navTransition, entry, new androidx.transition.Explode()); } -function setupAllAnimation(entry: ExpandedEntry, transition: Transition): void { - setupExitAndPopEnterAnimation(entry, transition); - const listener = getAnimationListener(); - - // setupAllAnimation is called only for new fragments so we don't - // need to clearAnimationListener for enter & popExit animators. - const enterAnimator = transition.createAndroidAnimator(AndroidTransitionType.enter); - enterAnimator.transitionType = AndroidTransitionType.enter; - enterAnimator.entry = entry; - enterAnimator.addListener(listener); - entry.enterAnimator = enterAnimator; - - const popExitAnimator = transition.createAndroidAnimator(AndroidTransitionType.popExit); - popExitAnimator.transitionType = AndroidTransitionType.popExit; - popExitAnimator.entry = entry; - popExitAnimator.addListener(listener); - entry.popExitAnimator = popExitAnimator; -} - -function setupDefaultAnimations(entry: ExpandedEntry, transition: Transition): void { - const listener = getAnimationListener(); - - const enterAnimator = transition.createAndroidAnimator(AndroidTransitionType.enter); - enterAnimator.transitionType = AndroidTransitionType.enter; - enterAnimator.entry = entry; - enterAnimator.addListener(listener); - entry.defaultEnterAnimator = enterAnimator; - - const exitAnimator = transition.createAndroidAnimator(AndroidTransitionType.exit); - exitAnimator.transitionType = AndroidTransitionType.exit; - exitAnimator.entry = entry; - exitAnimator.addListener(listener); - entry.defaultExitAnimator = exitAnimator; -} - -function setUpNativeTransition(navigationTransition: NavigationTransition, nativeTransition: android.transition.Transition) { +function setUpNativeTransition(navigationTransition: NavigationTransition, nativeTransition: androidx.transition.Transition) { if (navigationTransition.duration) { nativeTransition.setDuration(navigationTransition.duration); } @@ -709,7 +608,7 @@ function setUpNativeTransition(navigationTransition: NavigationTransition, nativ nativeTransition.setInterpolator(interpolator); } -function addNativeTransitionListener(entry: ExpandedEntry, nativeTransition: android.transition.Transition): ExpandedTransitionListener { +export function addNativeTransitionListener(entry: ExpandedEntry, nativeTransition: androidx.transition.Transition): ExpandedTransitionListener { const listener = getTransitionListener(entry, nativeTransition); nativeTransition.addListener(listener); @@ -750,7 +649,7 @@ function transitionOrAnimationCompleted(entry: ExpandedEntry): void { } } -function toShortString(nativeTransition: android.transition.Transition): string { +function toShortString(nativeTransition: androidx.transition.Transition): string { return `${nativeTransition.getClass().getSimpleName()}@${nativeTransition.hashCode().toString(16)}`; } @@ -761,19 +660,12 @@ function printTransitions(entry: ExpandedEntry) { result += `transitionName=${entry.transitionName}, `; } - if (entry.transition) { - result += `enterAnimator=${entry.enterAnimator}, `; - result += `exitAnimator=${entry.exitAnimator}, `; - result += `popEnterAnimator=${entry.popEnterAnimator}, `; - result += `popExitAnimator=${entry.popExitAnimator}, `; - } - if (sdkVersion() >= 21) { - const fragment = entry.fragment; - result += `${fragment.getEnterTransition() ? " enter=" + toShortString(fragment.getEnterTransition()) : ""}`; - result += `${fragment.getExitTransition() ? " exit=" + toShortString(fragment.getExitTransition()) : ""}`; - result += `${fragment.getReenterTransition() ? " popEnter=" + toShortString(fragment.getReenterTransition()) : ""}`; - result += `${fragment.getReturnTransition() ? " popExit=" + toShortString(fragment.getReturnTransition()) : ""}`; - } + const fragment = entry.fragment; + result += `${fragment.getEnterTransition() ? " enter=" + toShortString(fragment.getEnterTransition()) : ""}`; + result += `${fragment.getExitTransition() ? " exit=" + toShortString(fragment.getExitTransition()) : ""}`; + result += `${fragment.getReenterTransition() ? " popEnter=" + toShortString(fragment.getReenterTransition()) : ""}`; + result += `${fragment.getReturnTransition() ? " popExit=" + toShortString(fragment.getReturnTransition()) : ""}`; + traceWrite(result, traceCategories.Transition); } } @@ -785,16 +677,24 @@ function javaObjectArray(...params: java.lang.Object[]) { return nativeArray; } -function createDummyZeroDurationAnimator(): android.animation.Animator { - const animator = android.animation.ValueAnimator.ofObject(intEvaluator(), javaObjectArray(java.lang.Integer.valueOf(0), java.lang.Integer.valueOf(1))); - // TODO: investigate why this is necessary for 3 levels of nested frames - animator.setDuration(1); +function createDummyZeroDurationAnimator(duration: number): android.animation.AnimatorSet { + const animatorSet = new android.animation.AnimatorSet(); + const objectAnimators = Array.create(android.animation.Animator, 1); + + const values = Array.create("float", 2); + values[0] = 0.0; + values[1] = 1.0; + + const animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); + animator.setDuration(duration); + objectAnimators[0] = animator; + animatorSet.playTogether(objectAnimators); - return animator; + return animatorSet; } class NoTransition extends Transition { - public createAndroidAnimator(transitionType: string): android.animation.Animator { - return createDummyZeroDurationAnimator(); + public createAndroidAnimator(transitionType: string): android.animation.AnimatorSet { + return createDummyZeroDurationAnimator(this.getDuration()); } } diff --git a/tns-core-modules/ui/frame/fragment.transitions.d.ts b/tns-core-modules/ui/frame/fragment.transitions.d.ts index 902aebe8f7..5c9305b1ba 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.d.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.d.ts @@ -2,59 +2,92 @@ * @module "ui/transition" */ /** */ -import { NavigationTransition, BackstackEntry } from "../frame"; + import { NavigationTransition, BackstackEntry } from "../frame"; + // Types. + import { Transition, AndroidTransitionType } from "../transition/transition"; + + /** + * @private + */ + + export interface ExpandedTransitionListener extends androidx.transition.Transition.TransitionListener { + entry: ExpandedEntry; + transition: androidx.transition.Transition; + } -/** - * @private - */ -export { AnimationType } from "./fragment.transitions.types"; + export interface ExpandedAnimator extends android.animation.Animator { + entry: ExpandedEntry; + transitionType?: string; +} + + export interface ExpandedEntry extends BackstackEntry { + + enterTransitionListener: ExpandedTransitionListener; + exitTransitionListener: ExpandedTransitionListener; + reenterTransitionListener: ExpandedTransitionListener; + returnTransitionListener: ExpandedTransitionListener; + + enterAnimator: ExpandedAnimator; + exitAnimator: ExpandedAnimator; + popEnterAnimator: ExpandedAnimator; + popExitAnimator: ExpandedAnimator; -/** - * @private - */ -export function _setAndroidFragmentTransitions( - animated: boolean, - navigationTransition: NavigationTransition, - currentEntry: BackstackEntry, - newEntry: BackstackEntry, - fragmentTransaction: any, - frameId: number): void; -/** - * @private - */ -export function _onFragmentCreateAnimator(entry: BackstackEntry, fragment: any, nextAnim: number, enter: boolean): any; -/** - * @private - */ -export function _getAnimatedEntries(frameId: number): Set; -/** - * @private - * Called once fragment is recreated after it was destroyed. - * Reapply animations and transitions from entry to fragment if any. - */ -export function _updateTransitions(entry: BackstackEntry): void; -/** - * @private - * Called once fragment is going to reappear from backstack. - * Reverse transitions from entry to fragment if any. - */ -export function _reverseTransitions(previousEntry: BackstackEntry, currentEntry: BackstackEntry): boolean; -/** - * @private - * Called when entry is removed from backstack (either back navigation or - * navigate with clear history). Removes all animations and transitions from entry - * and fragment and clears all listeners in order to prevent memory leaks. - */ -export function _clearEntry(entry: BackstackEntry): void; -/** - * @private - * Called when fragment is destroyed because activity is destroyed. - * Removes all animations and transitions but keeps them on the entry - * in order to reapply them when new fragment is created for the same entry. - */ -export function _clearFragment(entry: BackstackEntry): void; -/** - * @private - */ -export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: any, operation: number, fromVC: any, toVC: any): any; -//@endprivate + transition: Transition; + transitionName: string; + frameId: number; + + isNestedDefaultTransition: boolean + } + + /** + * @private + */ + export function _setAndroidFragmentTransitions( + animated: boolean, + navigationTransition: NavigationTransition, + currentEntry: BackstackEntry, + newEntry: BackstackEntry, + frameId: number, + fragmentTransaction: any, + isNestedDefaultTransition?: boolean): void; + /** + * @private + */ + export function _getAnimatedEntries(frameId: number): Set; + /** + * @private + * Called once fragment is recreated after it was destroyed. + * Reapply animations and transitions from entry to fragment if any. + */ + export function _updateTransitions(entry: BackstackEntry): void; + /** + * @private + * Called once fragment is going to reappear from backstack. + * Reverse transitions from entry to fragment if any. + */ + export function _reverseTransitions(previousEntry: BackstackEntry, currentEntry: BackstackEntry): boolean; + /** + * @private + * Called when entry is removed from backstack (either back navigation or + * navigate with clear history). Removes all animations and transitions from entry + * and fragment and clears all listeners in order to prevent memory leaks. + */ + export function _clearEntry(entry: BackstackEntry): void; + /** + * @private + * Called when fragment is destroyed because activity is destroyed. + * Removes all animations and transitions but keeps them on the entry + * in order to reapply them when new fragment is created for the same entry. + */ + export function _clearFragment(entry: BackstackEntry): void; + /** + * @private + */ + export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: any, operation: number, fromVC: any, toVC: any): any; + + /** + * @private + */ + export function addNativeTransitionListener(entry: ExpandedEntry, nativeTransition: androidx.transition.Transition): ExpandedTransitionListener; + //@endprivate + \ 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 fc52f37b8d..f536059ae5 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -14,8 +14,8 @@ import { } from "./frame-common"; import { - _setAndroidFragmentTransitions, _onFragmentCreateAnimator, _getAnimatedEntries, - _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, AnimationType + _setAndroidFragmentTransitions, _getAnimatedEntries, ExpandedEntry, + _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, addNativeTransitionListener, ExpandedTransitionListener } from "./fragment.transitions"; // TODO: Remove this and get it from global to decouple builder for angular @@ -26,12 +26,13 @@ import { profile } from "../../profiling"; export * from "./frame-common"; -interface AnimatorState { - enterAnimator: any; - exitAnimator: any; - popEnterAnimator: any; - popExitAnimator: any; +interface TransitionState { + enterTransitionListener: any; + exitTransitionListener: any; + reenterTransitionListener: any; + returnTransitionListener: any; transitionName: string; + entry: BackstackEntry; } const ANDROID_PLATFORM = "android"; @@ -118,7 +119,7 @@ export class Frame extends FrameBase { private _containerViewId: number = -1; private _tearDownPending = false; private _attachedToWindow = false; - private _cachedAnimatorState: AnimatorState; + private _cachedTransitionState: TransitionState; constructor() { super(); @@ -154,6 +155,11 @@ export class Frame extends FrameBase { _onAttachedToWindow(): void { super._onAttachedToWindow(); this._attachedToWindow = true; + + if (this._manager && this._manager.isDestroyed()) { + return; + } + this._processNextNavigationEntry(); } @@ -182,7 +188,9 @@ export class Frame extends FrameBase { const manager = this._getFragmentManager(); const entry = this._currentEntry; - if (entry && manager && !manager.findFragmentByTag(entry.fragmentTag)) { + const isNewEntry = !this._cachedTransitionState || entry !== this._cachedTransitionState.entry; + + if (isNewEntry && 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 @@ -193,12 +201,17 @@ export class Frame extends FrameBase { // 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); - this._currentEntry = entry; + let cachedTransitionState = getTransitionState(this._currentEntry); + + if (cachedTransitionState) { + this._cachedTransitionState = cachedTransitionState; + this._currentEntry = null; + // NavigateCore will eventually call _processNextNavigationEntry again. + this._navigateCore(entry); + this._currentEntry = entry; + } else { + super._processNextNavigationEntry(); + } } else { super._processNextNavigationEntry(); } @@ -246,7 +259,14 @@ export class Frame extends FrameBase { const manager: androidx.fragment.app.FragmentManager = this._getFragmentManager(); const transaction = manager.beginTransaction(); - transaction.remove(this._currentEntry.fragment); + const fragment = this._currentEntry.fragment; + const fragmentExitTransition = fragment.getExitTransition(); + + if (fragmentExitTransition && fragmentExitTransition instanceof org.nativescript.widgets.CustomTransition) { + fragmentExitTransition.setResetOnTransitionEnd(true); + } + + transaction.remove(fragment); transaction.commitNowAllowingStateLoss(); } @@ -314,9 +334,9 @@ export class Frame extends FrameBase { } // restore cached animation settings if we just completed simulated first navigation (no animation) - if (this._cachedAnimatorState) { - restoreAnimatorState(this._currentEntry, this._cachedAnimatorState); - this._cachedAnimatorState = null; + if (this._cachedTransitionState) { + restoreTransitionState(this._currentEntry, this._cachedTransitionState); + this._cachedTransitionState = null; } // restore original fragment transitions if we just completed replace navigation (hmr) @@ -328,7 +348,7 @@ export class Frame extends FrameBase { const currentEntry = null; const newEntry = entry; const transaction = null; - _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, transaction, this._android.frameId); + _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction); } } @@ -404,10 +424,13 @@ export class Frame extends FrameBase { navigationTransition = null; } - _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, transaction, this._android.frameId); + let isNestedDefaultTransition = !currentEntry; + + _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction, isNestedDefaultTransition); if (currentEntry && animated && !navigationTransition) { - transaction.setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + //TO DO: Check whether or not this is still necessary. For Modal views? + //transaction.setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN); } transaction.replace(this.containerViewId, newFragment, newFragmentTag); @@ -430,12 +453,7 @@ export class Frame extends FrameBase { _updateTransitions(backstackEntry); } - const transitionReversed = _reverseTransitions(backstackEntry, this._currentEntry); - if (!transitionReversed) { - // If transition were not reversed then use animations. - // we do not use Android backstack so setting popEnter / popExit is meaningless (3rd and 4th optional args) - transaction.setCustomAnimations(AnimationType.popEnterFakeResourceId, AnimationType.popExitFakeResourceId); - } + _reverseTransitions(backstackEntry, this._currentEntry); transaction.replace(this.containerViewId, backstackEntry.fragment, backstackEntry.fragmentTag); transaction.commitAllowingStateLoss(); @@ -534,48 +552,50 @@ export class Frame extends FrameBase { }); } } - -function cloneExpandedAnimator(expandedAnimator: any) { - if (!expandedAnimator) { +function cloneExpandedTransitionListener(expandedTransitionListener: ExpandedTransitionListener) { + if (!expandedTransitionListener) { return null; } - const clone = expandedAnimator.clone(); - clone.entry = expandedAnimator.entry; - clone.transitionType = expandedAnimator.transitionType; + const cloneTransition = expandedTransitionListener.transition.clone(); - return clone; + return addNativeTransitionListener(expandedTransitionListener.entry, cloneTransition); } -function getAnimatorState(entry: BackstackEntry): AnimatorState { +function getTransitionState(entry: BackstackEntry): TransitionState { const expandedEntry = entry; - const animatorState = {}; - - animatorState.enterAnimator = cloneExpandedAnimator(expandedEntry.enterAnimator); - animatorState.exitAnimator = cloneExpandedAnimator(expandedEntry.exitAnimator); - animatorState.popEnterAnimator = cloneExpandedAnimator(expandedEntry.popEnterAnimator); - animatorState.popExitAnimator = cloneExpandedAnimator(expandedEntry.popExitAnimator); - animatorState.transitionName = expandedEntry.transitionName; + const transitionState = {}; + + if (expandedEntry.enterTransitionListener && expandedEntry.exitTransitionListener) { + transitionState.enterTransitionListener = cloneExpandedTransitionListener(expandedEntry.enterTransitionListener); + transitionState.exitTransitionListener = cloneExpandedTransitionListener(expandedEntry.exitTransitionListener); + transitionState.reenterTransitionListener = cloneExpandedTransitionListener(expandedEntry.reenterTransitionListener); + transitionState.returnTransitionListener = cloneExpandedTransitionListener(expandedEntry.returnTransitionListener); + transitionState.transitionName = expandedEntry.transitionName; + transitionState.entry = entry; + } else { + return null; + } - return animatorState; + return transitionState; } -function restoreAnimatorState(entry: BackstackEntry, snapshot: AnimatorState): void { +function restoreTransitionState(entry: BackstackEntry, snapshot: TransitionState): void { const expandedEntry = entry; - if (snapshot.enterAnimator) { - expandedEntry.enterAnimator = snapshot.enterAnimator; + if (snapshot.enterTransitionListener) { + expandedEntry.enterTransitionListener = snapshot.enterTransitionListener; } - if (snapshot.exitAnimator) { - expandedEntry.exitAnimator = snapshot.exitAnimator; + if (snapshot.exitTransitionListener) { + expandedEntry.exitTransitionListener = snapshot.exitTransitionListener; } - if (snapshot.popEnterAnimator) { - expandedEntry.popEnterAnimator = snapshot.popEnterAnimator; + if (snapshot.reenterTransitionListener) { + expandedEntry.reenterTransitionListener = snapshot.reenterTransitionListener; } - if (snapshot.popExitAnimator) { - expandedEntry.popExitAnimator = snapshot.popExitAnimator; + if (snapshot.returnTransitionListener) { + expandedEntry.returnTransitionListener = snapshot.returnTransitionListener; } expandedEntry.transitionName = snapshot.transitionName; @@ -777,6 +797,7 @@ export function setFragmentClass(clazz: any) { class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { public frame: Frame; public entry: BackstackEntry; + private backgroundBitmap: android.graphics.Bitmap = null; @profile public onHiddenChanged(fragment: androidx.fragment.app.Fragment, hidden: boolean, superFunc: Function): void { @@ -787,31 +808,17 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { } @profile - 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; - case AnimationType.exitFakeResourceId: nextAnimString = "exit"; break; - case AnimationType.popEnterFakeResourceId: nextAnimString = "popEnter"; break; - case AnimationType.popExitFakeResourceId: nextAnimString = "popExit"; break; - } + public onCreateAnimator(fragment: androidx.fragment.app.Fragment, transit: number, enter: boolean, nextAnim: number, superFunc: Function): android.animation.Animator { + let animator = null; + const entry = (this.entry); - let animator = _onFragmentCreateAnimator(this.entry, fragment, nextAnim, enter); - if (!animator) { - animator = superFunc.call(fragment, transit, enter, nextAnim); + // Return enterAnimator only when new (no current entry) nested transition. + if (enter && entry.isNestedDefaultTransition) { + animator = entry.enterAnimator; + entry.isNestedDefaultTransition = false; } - if (traceEnabled()) { - traceWrite(`${fragment}.onCreateAnimator(${transit}, ${enter ? "enter" : "exit"}, ${nextAnimString}): ${animator ? "animator" : "no animator"}`, traceCategories.NativeLifecycle); - } - - return animator; + return animator || superFunc.call(fragment, transit, enter, nextAnim); } @profile @@ -922,7 +929,13 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { if (traceEnabled()) { traceWrite(`${fragment}.onDestroyView()`, traceCategories.NativeLifecycle); } + const parentFragment = fragment.getParentFragment(); + if (parentFragment && (parentFragment.isRemoving())) { + const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(this.backgroundBitmap); + this.frame.nativeViewProtected.setBackgroundDrawable(bitmapDrawable); + this.backgroundBitmap = null; + } superFunc.call(fragment); } @@ -955,6 +968,18 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { } } + @profile + public onPause(fragment: androidx.fragment.app.Fragment, superFunc: Function): void { + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + const parentFragment = fragment.getParentFragment(); + + if (parentFragment && (parentFragment.isRemoving())) { + this.backgroundBitmap = this.loadBitmapFromView(this.frame.nativeViewProtected); + } + superFunc.call(fragment); + } + @profile public onStop(fragment: androidx.fragment.app.Fragment, superFunc: Function): void { superFunc.call(fragment); @@ -969,6 +994,22 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { return "NO ENTRY, " + superFunc.call(fragment); } } + + private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap { + // Another way to get view bitmap. Test performance vs setDrawingCacheEnabled + // const width = view.getWidth(); + // const height = view.getHeight(); + // const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888); + // const canvas = new android.graphics.Canvas(bitmap); + // view.layout(0, 0, width, height); + // view.draw(canvas); + + view.setDrawingCacheEnabled(true); + const bitmap = android.graphics.Bitmap.createBitmap(view.getDrawingCache()); + view.setDrawingCacheEnabled(false); + + return bitmap; + } } class ActivityCallbacksImplementation implements AndroidActivityCallbacks { diff --git a/tns-core-modules/ui/transition/fade-transition.android.ts b/tns-core-modules/ui/transition/fade-transition.android.ts index b2dcdc8a8e..d5335ece24 100644 --- a/tns-core-modules/ui/transition/fade-transition.android.ts +++ b/tns-core-modules/ui/transition/fade-transition.android.ts @@ -1,7 +1,8 @@ import { Transition, AndroidTransitionType } from "./transition"; export class FadeTransition extends Transition { - public createAndroidAnimator(transitionType: string): android.animation.Animator { + public createAndroidAnimator(transitionType: string): android.animation.AnimatorSet { + const animatorSet = new android.animation.AnimatorSet(); const alphaValues = Array.create("float", 2); switch (transitionType) { case AndroidTransitionType.enter: @@ -16,14 +17,15 @@ export class FadeTransition extends Transition { break; } - const animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", alphaValues); + const animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", alphaValues); const duration = this.getDuration(); if (duration !== undefined) { animator.setDuration(duration); } animator.setInterpolator(this.getCurve()); - - return animator; + animatorSet.play(animator); + + return animatorSet; } } diff --git a/tns-core-modules/ui/transition/flip-transition.android.ts b/tns-core-modules/ui/transition/flip-transition.android.ts index 64a11cde10..2423a13c96 100644 --- a/tns-core-modules/ui/transition/flip-transition.android.ts +++ b/tns-core-modules/ui/transition/flip-transition.android.ts @@ -9,10 +9,10 @@ export class FlipTransition extends Transition { this._direction = direction; } - public createAndroidAnimator(transitionType: string): android.animation.Animator { + public createAndroidAnimator(transitionType: string): android.animation.AnimatorSet { let objectAnimators; let values; - let animator: android.animation.ObjectAnimator; + let animator: android.animation.Animator; //android.animation.ObjectAnimator; const animatorSet = new android.animation.AnimatorSet(); const fullDuration = this.getDuration() || 300; const interpolator = this.getCurve(); @@ -20,30 +20,23 @@ export class FlipTransition extends Transition { switch (transitionType) { case AndroidTransitionType.enter: // card_flip_right_in - objectAnimators = Array.create(android.animation.Animator, 3); - - values = Array.create("float", 2); - values[0] = 1.0; - values[1] = 0.0; - animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); - animator.setDuration(0); - objectAnimators[0] = animator; + objectAnimators = Array.create(android.animation.Animator, 2); values = Array.create("float", 2); values[0] = rotationY; values[1] = 0.0; - animator = android.animation.ObjectAnimator.ofFloat(null, "rotationY", values); + animator = android.animation.ObjectAnimator.ofFloat(null, "rotationY", values); animator.setInterpolator(interpolator); animator.setDuration(fullDuration); - objectAnimators[1] = animator; + objectAnimators[0] = animator; - values = Array.create("float", 2); + values = Array.create("float", 3); values[0] = 0.0; - values[1] = 1.0; - animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); - animator.setStartDelay(fullDuration / 2); - animator.setDuration(1); - objectAnimators[2] = animator; + values[1] = 0.0; + values[2] = 255.0; + animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); + animator.setDuration(fullDuration / 2); + objectAnimators[1] = animator; break; case AndroidTransitionType.exit: // card_flip_right_out objectAnimators = Array.create(android.animation.Animator, 2); @@ -51,44 +44,37 @@ export class FlipTransition extends Transition { values = Array.create("float", 2); values[0] = 0.0; values[1] = -rotationY; - animator = android.animation.ObjectAnimator.ofFloat(null, "rotationY", values); + animator = android.animation.ObjectAnimator.ofFloat(null, "rotationY", values); animator.setInterpolator(interpolator); animator.setDuration(fullDuration); objectAnimators[0] = animator; - values = Array.create("float", 2); - values[0] = 1.0; + values = Array.create("float", 3); + values[0] = 255.0; values[1] = 0.0; - animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); - animator.setStartDelay(fullDuration / 2); - animator.setDuration(1); + values[2] = 0.0; + animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); + animator.setDuration(fullDuration / 2); objectAnimators[1] = animator; break; case AndroidTransitionType.popEnter: // card_flip_left_in - objectAnimators = Array.create(android.animation.Animator, 3); - - values = Array.create("float", 2); - values[0] = 1.0; - values[1] = 0.0; - animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); - animator.setDuration(0); - objectAnimators[0] = animator; + objectAnimators = Array.create(android.animation.Animator, 2); values = Array.create("float", 2); values[0] = -rotationY; values[1] = 0.0; - animator = android.animation.ObjectAnimator.ofFloat(null, "rotationY", values); + animator = android.animation.ObjectAnimator.ofFloat(null, "rotationY", values); animator.setInterpolator(interpolator); animator.setDuration(fullDuration); - objectAnimators[1] = animator; + objectAnimators[0] = animator; - values = Array.create("float", 2); + values = Array.create("float", 3); values[0] = 0.0; - values[1] = 1.0; - animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); - animator.setStartDelay(fullDuration / 2); - animator.setDuration(1); - objectAnimators[2] = animator; + values[1] = 0.0; + values[2] = 255.0; + animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); + animator.setDuration(fullDuration / 2); + objectAnimators[1] = animator; break; case AndroidTransitionType.popExit: // card_flip_left_out objectAnimators = Array.create(android.animation.Animator, 2); @@ -96,17 +82,17 @@ export class FlipTransition extends Transition { values = Array.create("float", 2); values[0] = 0.0; values[1] = rotationY; - animator = android.animation.ObjectAnimator.ofFloat(null, "rotationY", values); + animator = android.animation.ObjectAnimator.ofFloat(null, "rotationY", values); animator.setInterpolator(interpolator); animator.setDuration(fullDuration); objectAnimators[0] = animator; - values = Array.create("float", 2); - values[0] = 1.0; + values = Array.create("float", 3); + values[0] = 255.0; values[1] = 0.0; - animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); - animator.setStartDelay(fullDuration / 2); - animator.setDuration(1); + values[2] = 0.0; + animator = android.animation.ObjectAnimator.ofFloat(null, "alpha", values); + animator.setDuration(fullDuration / 2); objectAnimators[1] = animator; break; } diff --git a/tns-platform-declarations/android/org.nativescript.widgets.d.ts b/tns-platform-declarations/android/org.nativescript.widgets.d.ts index dc17e76389..571c1dfa96 100644 --- a/tns-platform-declarations/android/org.nativescript.widgets.d.ts +++ b/tns-platform-declarations/android/org.nativescript.widgets.d.ts @@ -1,6 +1,11 @@ declare module org { module nativescript { module widgets { + export class CustomTransition extends androidx.transition.Visibility { + constructor(animatorSet: android.animation.AnimatorSet, transitionName: string); + public setResetOnTransitionEnd(resetOnTransitionEnd: boolean): void; + public getTransitionName(): string; + } export module Async { export class CompleteCallback { constructor(implementation: ICompleteCallback); @@ -166,12 +171,6 @@ public verticalAlignment: VerticalAlignment; } - export class FragmentBase extends androidx.fragment.app.Fragment { - constructor(); - - public getRemovingParentFragment(): androidx.fragment.app.Fragment; - } - export enum Stretch { none, aspectFill, From 7527507f864b4fae82576b4ce975e5e060c30216 Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Tue, 8 Oct 2019 16:24:14 +0300 Subject: [PATCH 2/9] fix: handle disappearing nested fragments for tabs. Extract TabFragmentImplementation in tab-navigation base for both tabs and bottom navigation --- .../bottom-navigation.android.ts | 55 +-------- tns-core-modules/ui/frame/fragment.android.ts | 6 +- .../ui/frame/fragment.transitions.android.ts | 2 - tns-core-modules/ui/frame/frame.d.ts | 1 + .../tab-navigation-base.d.ts | 11 ++ .../tab-navigation-base.ts | 109 +++++++++++++++++- .../ui/tab-view/tab-view.android.ts | 62 +++++++++- tns-core-modules/ui/tabs/tabs.android.ts | 55 +-------- 8 files changed, 185 insertions(+), 116 deletions(-) diff --git a/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts b/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts index 0fafaf048a..806a554311 100644 --- a/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts +++ b/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts @@ -12,7 +12,7 @@ import { Color, CSSType } from "../core/view"; import { Frame, View } from "../frame"; import { Font } from "../styling/font"; import { - getIconSpecSize, itemsProperty, selectedIndexProperty, TabNavigationBase, tabStripProperty + getIconSpecSize, itemsProperty, selectedIndexProperty, TabNavigationBase, tabStripProperty, TabFragmentImplementation, _tabs } from "../tab-navigation-base/tab-navigation-base"; import { getTransformedText } from "../text-base"; @@ -27,8 +27,6 @@ export * from "../tab-navigation-base/tab-strip-item"; const PRIMARY_COLOR = "colorPrimary"; const DEFAULT_ELEVATION = 8; -const TABID = "_tabId"; -const INDEX = "_index"; const ownerSymbol = Symbol("_owner"); let TabFragment: any; @@ -39,58 +37,11 @@ function makeFragmentName(viewId: number, id: number): string { return "android:bottomnavigation:" + viewId + ":" + id; } -function getTabById(id: number): BottomNavigation { - const ref = tabs.find(ref => { - const tab = ref.get(); - - return tab && tab._domId === id; - }); - - return ref && ref.get(); -} - function initializeNativeClasses() { if (BottomNavigationBar) { return; } - class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { - private tab: BottomNavigation; - private index: number; - - constructor() { - super(); - - return global.__native(this); - } - - static newInstance(tabId: number, index: number): TabFragmentImplementation { - const args = new android.os.Bundle(); - args.putInt(TABID, tabId); - args.putInt(INDEX, index); - const fragment = new TabFragmentImplementation(); - fragment.setArguments(args); - - return fragment; - } - - public onCreate(savedInstanceState: android.os.Bundle): void { - super.onCreate(savedInstanceState); - const args = this.getArguments(); - this.tab = getTabById(args.getInt(TABID)); - this.index = args.getInt(INDEX); - if (!this.tab) { - throw new Error(`Cannot find BottomNavigation`); - } - } - - 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.nativeViewProtected; - } - } - class BottomNavigationBarImplementation extends org.nativescript.widgets.BottomNavigationBar { constructor(context: android.content.Context, public owner: BottomNavigation) { @@ -178,8 +129,6 @@ function setElevation(bottomNavigationBar: org.nativescript.widgets.BottomNaviga } } -export const tabs = new Array>(); - function iterateIndexRange(index: number, eps: number, lastIndex: number, callback: (i) => void) { const rangeStart = Math.max(0, index - eps); const rangeEnd = Math.min(index + eps, lastIndex); @@ -199,7 +148,7 @@ export class BottomNavigation extends TabNavigationBase { constructor() { super(); - tabs.push(new WeakRef(this)); + _tabs.push(new WeakRef(this)); } get _hasFragments(): boolean { diff --git a/tns-core-modules/ui/frame/fragment.android.ts b/tns-core-modules/ui/frame/fragment.android.ts index e6cb0ec83a..a190b675e4 100644 --- a/tns-core-modules/ui/frame/fragment.android.ts +++ b/tns-core-modules/ui/frame/fragment.android.ts @@ -1,7 +1,7 @@ import { AndroidFragmentCallbacks, setFragmentCallbacks, setFragmentClass } from "./frame"; @JavaProxy("com.tns.FragmentClass") -class FragmentClass extends org.nativescript.widgets.FragmentBase { +class FragmentClass extends androidx.fragment.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; @@ -23,6 +23,10 @@ class FragmentClass extends org.nativescript.widgets.FragmentBase { this._callbacks.onStop(this, super.onStop); } + public onPause(): void { + this._callbacks.onPause(this, super.onStop); + } + public onCreate(savedInstanceState: android.os.Bundle) { if (!this._callbacks) { setFragmentCallbacks(this); diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts index 78b1759d39..d97a47d640 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -186,7 +186,6 @@ function getAnimationListener(): android.animation.Animator.AnimatorListener { onAnimationStart(animator: ExpandedAnimator): void { const entry = animator.entry; - console.log("Animation start " + animator.getDuration()); addToWaitingQueue(entry); if (traceEnabled()) { traceWrite(`START ${animator.transitionType} for ${entry.fragmentTag}`, traceCategories.Transition); @@ -203,7 +202,6 @@ function getAnimationListener(): android.animation.Animator.AnimatorListener { if (traceEnabled()) { traceWrite(`END ${animator.transitionType} for ${animator.entry.fragmentTag}`, traceCategories.Transition); } - console.log("Animation end " + animator.getDuration()); transitionOrAnimationCompleted(animator.entry); } diff --git a/tns-core-modules/ui/frame/frame.d.ts b/tns-core-modules/ui/frame/frame.d.ts index 14a6429041..8833f93859 100644 --- a/tns-core-modules/ui/frame/frame.d.ts +++ b/tns-core-modules/ui/frame/frame.d.ts @@ -418,6 +418,7 @@ export interface AndroidFragmentCallbacks { onSaveInstanceState(fragment: any, outState: any, superFunc: Function): void; onDestroyView(fragment: any, superFunc: Function): void; onDestroy(fragment: any, superFunc: Function): void; + onPause(fragment: any, superFunc: Function): void; onStop(fragment: any, superFunc: Function): void; toStringOverride(fragment: any, superFunc: Function): string; } diff --git a/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.d.ts b/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.d.ts index 1a6b244185..938dcb7c6b 100644 --- a/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.d.ts +++ b/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.d.ts @@ -25,6 +25,16 @@ export interface SelectedIndexChangedEventData extends EventData { newIndex: number; } +/** + * Serves as a base class for tab fragments. + */ +export class TabFragmentImplementation extends androidx.fragment.app.Fragment { + static newInstance(tabId: number, index: number): TabFragmentImplementation; + public onCreate(savedInstanceState: android.os.Bundle): void; + public onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View; + public onPause(): void; +} + /** * Serves as a base class for tab navigation. */ @@ -219,6 +229,7 @@ export class TabNavigationBase extends View { export function getIconSpecSize(size: { width: number, height: number }): { width: number, height: number } +export const _tabs: Array>; export const itemsProperty: Property; export const tabStripProperty: Property export const selectedIndexProperty: CoercibleProperty; diff --git a/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts b/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts index bc0cb9d249..54d50b7a19 100644 --- a/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts +++ b/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts @@ -6,21 +6,116 @@ import { TabStripItem } from "../tab-strip-item"; import { ViewBase, AddArrayFromBuilder, AddChildFromBuilder, EventData } from "../../core/view"; // Requires -import { View, Property, CoercibleProperty, isIOS } from "../../core/view"; +import { View, Property, CoercibleProperty, isIOS, Color } from "../../core/view"; // TODO: Impl trace // export const traceCategory = "TabView"; +const TABID = "_tabId"; +const INDEX = "_index"; + +function getTabById(id: number): any { + const ref = _tabs.find(ref => { + const tab = ref.get(); + + return tab && tab._domId === id; + }); + + return ref && ref.get(); +} + export module knownCollections { export const items = "items"; } +export class TabFragmentImplementation extends androidx.fragment.app.Fragment { + private owner: TabNavigationBase; + private index: number; + private backgroundBitmap: android.graphics.Bitmap = null; + + constructor() { + super(); + + return global.__native(this); + } + + static newInstance(tabId: number, index: number): TabFragmentImplementation { + const args = new android.os.Bundle(); + args.putInt(TABID, tabId); + args.putInt(INDEX, index); + const fragment = new TabFragmentImplementation(); + fragment.setArguments(args); + + return fragment; + } + + public onCreate(savedInstanceState: android.os.Bundle): void { + super.onCreate(savedInstanceState); + const args = this.getArguments(); + this.owner = getTabById(args.getInt(TABID)); + this.index = args.getInt(INDEX); + if (!this.owner) { + throw new Error(`Cannot find TabView`); + } + } + + public onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View { + const tabItem = this.owner.items[this.index]; + + return tabItem.nativeViewProtected; + } + + public onDestroyView() { + const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + if (hasRemovingParent && this.owner.selectedIndex === this.index) { + const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(this.backgroundBitmap); + this.owner._originalBackground = this.owner.backgroundColor || new Color("White"); + this.owner.nativeViewProtected.setBackgroundDrawable(bitmapDrawable); + this.backgroundBitmap = null; + } + + super.onDestroyView(); + } + + public onPause(): void { + const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + if (hasRemovingParent && this.owner.selectedIndex === this.index) { + this.backgroundBitmap = this.loadBitmapFromView(this.owner.nativeViewProtected); + } + + super.onPause(); + } + + private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap { + // Another way to get view bitmap. Test performance vs setDrawingCacheEnabled + // const width = view.getWidth(); + // const height = view.getHeight(); + // const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888); + // const canvas = new android.graphics.Canvas(bitmap); + // view.layout(0, 0, width, height); + // view.draw(canvas); + + view.setDrawingCacheEnabled(true); + const bitmap = android.graphics.Bitmap.createBitmap(view.getDrawingCache()); + view.setDrawingCacheEnabled(false); + + return bitmap; + } +} + export class TabNavigationBase extends View implements TabNavigationBaseDefinition, AddChildFromBuilder, AddArrayFromBuilder { public static selectedIndexChangedEvent = "selectedIndexChanged"; public items: TabContentItem[]; public tabStrip: TabStrip; public selectedIndex: number; + public _originalBackground: any; public _addArrayFromBuilder(name: string, value: Array) { if (name === "items") { @@ -54,6 +149,16 @@ export class TabNavigationBase extends View implements TabNavigationBaseDefiniti return items ? items.length : 0; } + public onLoaded(): void { + if (this._originalBackground) { + this.backgroundColor = null; + this.backgroundColor = this._originalBackground; + this._originalBackground = null; + } + + super.onLoaded(); + } + public eachChild(callback: (child: ViewBase) => boolean) { const items = this.items; if (items) { @@ -265,6 +370,8 @@ export const selectedIndexProperty = new CoercibleProperty>(); + export const itemsProperty = new Property({ name: "items", valueChanged: (target, oldValue, newValue) => { target.onItemsChanged(oldValue, newValue); 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 5304ccee21..2b2946d22d 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -47,9 +47,10 @@ function initializeNativeClasses() { return; } - class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { - private tab: TabView; + class TabFragmentImplementation extends androidx.fragment.app.Fragment { + private owner: TabView; private index: number; + private backgroundBitmap: android.graphics.Bitmap = null; constructor() { super(); @@ -70,18 +71,61 @@ function initializeNativeClasses() { public onCreate(savedInstanceState: android.os.Bundle): void { super.onCreate(savedInstanceState); const args = this.getArguments(); - this.tab = getTabById(args.getInt(TABID)); + this.owner = getTabById(args.getInt(TABID)); this.index = args.getInt(INDEX); - if (!this.tab) { + if (!this.owner) { throw new Error(`Cannot find TabView`); } } public onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View { - const tabItem = this.tab.items[this.index]; + const tabItem = this.owner.items[this.index]; return tabItem.view.nativeViewProtected; } + + public onDestroyView() { + const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + if (hasRemovingParent && this.owner.selectedIndex === this.index) { + const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(this.backgroundBitmap); + this.owner._originalBackground = this.owner.backgroundColor || new Color("White"); + this.owner.nativeViewProtected.setBackground(bitmapDrawable); + this.backgroundBitmap = null; + } + + super.onDestroyView(); + } + + public onPause(): void { + const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + if (hasRemovingParent && this.owner.selectedIndex === this.index) { + this.backgroundBitmap = this.loadBitmapFromView(this.owner.nativeViewProtected); + } + + super.onPause(); + } + + private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap { + // Another way to get view bitmap. Test performance vs setDrawingCacheEnabled + const width = view.getWidth(); + const height = view.getHeight(); + const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888); + const canvas = new android.graphics.Canvas(bitmap); + view.layout(0, 0, width, height); + view.draw(canvas); + + // view.setDrawingCacheEnabled(true); + // const bitmap = android.graphics.Bitmap.createBitmap(view.getDrawingCache()); + // view.setDrawingCacheEnabled(false); + + return bitmap; + } } const POSITION_UNCHANGED = -1; @@ -397,6 +441,7 @@ export class TabView extends TabViewBase { private _viewPager: androidx.viewpager.widget.ViewPager; private _pagerAdapter: androidx.viewpager.widget.PagerAdapter; private _androidViewId: number = -1; + public _originalBackground: any; constructor() { super(); @@ -532,8 +577,13 @@ export class TabView extends TabViewBase { } public onLoaded(): void { - super.onLoaded(); + if (this._originalBackground) { + this.backgroundColor = null; + this.backgroundColor = this._originalBackground; + this._originalBackground = null; + } + super.onLoaded(); this.setAdapterItems(this.items); } diff --git a/tns-core-modules/ui/tabs/tabs.android.ts b/tns-core-modules/ui/tabs/tabs.android.ts index d5cfbf212b..f20e933cd3 100644 --- a/tns-core-modules/ui/tabs/tabs.android.ts +++ b/tns-core-modules/ui/tabs/tabs.android.ts @@ -12,7 +12,7 @@ import { Color } from "../core/view"; import { Frame } from "../frame"; import { Font } from "../styling/font"; import { - getIconSpecSize, itemsProperty, selectedIndexProperty, tabStripProperty + getIconSpecSize, itemsProperty, selectedIndexProperty, tabStripProperty, _tabs, TabFragmentImplementation } from "../tab-navigation-base/tab-navigation-base"; import { getTransformedText } from "../text-base"; import { offscreenTabLimitProperty, swipeEnabledProperty, TabsBase } from "./tabs-common"; @@ -27,8 +27,6 @@ interface PagerAdapter { new(owner: Tabs): androidx.viewpager.widget.PagerAdapter; } -const TABID = "_tabId"; -const INDEX = "_index"; let PagerAdapter: PagerAdapter; let TabsBar: any; @@ -36,58 +34,11 @@ function makeFragmentName(viewId: number, id: number): string { return "android:viewpager:" + viewId + ":" + id; } -function getTabById(id: number): Tabs { - const ref = tabs.find(ref => { - const tab = ref.get(); - - return tab && tab._domId === id; - }); - - return ref && ref.get(); -} - function initializeNativeClasses() { if (PagerAdapter) { return; } - class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { - private tab: Tabs; - private index: number; - - constructor() { - super(); - - return global.__native(this); - } - - static newInstance(tabId: number, index: number): TabFragmentImplementation { - const args = new android.os.Bundle(); - args.putInt(TABID, tabId); - args.putInt(INDEX, index); - const fragment = new TabFragmentImplementation(); - fragment.setArguments(args); - - return fragment; - } - - public onCreate(savedInstanceState: android.os.Bundle): void { - super.onCreate(savedInstanceState); - const args = this.getArguments(); - this.tab = getTabById(args.getInt(TABID)); - this.index = args.getInt(INDEX); - if (!this.tab) { - throw new Error(`Cannot find TabView`); - } - } - - 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.nativeViewProtected; - } - } - const POSITION_UNCHANGED = -1; const POSITION_NONE = -2; @@ -310,8 +261,6 @@ function setElevation(grid: org.nativescript.widgets.GridLayout, tabsBar: org.na } } -export const tabs = new Array>(); - function iterateIndexRange(index: number, eps: number, lastIndex: number, callback: (i) => void) { const rangeStart = Math.max(0, index - eps); const rangeEnd = Math.min(index + eps, lastIndex); @@ -328,7 +277,7 @@ export class Tabs extends TabsBase { constructor() { super(); - tabs.push(new WeakRef(this)); + _tabs.push(new WeakRef(this)); } get _hasFragments(): boolean { From e5c9c5ec7199c40e87ae526c081db1555605de20 Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Wed, 9 Oct 2019 11:25:33 +0300 Subject: [PATCH 3/9] chore: bump webpack cycles counter --- tests/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/webpack.config.js b/tests/webpack.config.js index 574fd1a571..a6f7b7bb3c 100644 --- a/tests/webpack.config.js +++ b/tests/webpack.config.js @@ -12,7 +12,7 @@ const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeS const TerserPlugin = require("terser-webpack-plugin"); const hashSalt = Date.now().toString(); -const ANDROID_MAX_CYCLES = 68; +const ANDROID_MAX_CYCLES = 66; const IOS_MAX_CYCLES = 39; let numCyclesDetected = 0; From 864d406300f6a5084ae7654ebe1219df6269ec8e Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Wed, 9 Oct 2019 11:41:55 +0300 Subject: [PATCH 4/9] feat(android-widgets): add androidx.transition:transition as dependency --- tns-core-modules-widgets/android/widgets/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/tns-core-modules-widgets/android/widgets/build.gradle b/tns-core-modules-widgets/android/widgets/build.gradle index 32771c7dbb..ae8c43767a 100644 --- a/tns-core-modules-widgets/android/widgets/build.gradle +++ b/tns-core-modules-widgets/android/widgets/build.gradle @@ -77,6 +77,7 @@ dependencies { def androidxVersion = computeAndroidXVersion() implementation 'androidx.viewpager:viewpager:' + androidxVersion implementation 'androidx.fragment:fragment:' + androidxVersion + implementation 'androidx.transition:transition:' + androidxVersion } else { println 'Using support library' implementation 'com.android.support:support-v4:' + computeSupportVersion() From 548992e5f8adfd13ecd3d1436843b2cd0a33608d Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Wed, 9 Oct 2019 13:23:43 +0300 Subject: [PATCH 5/9] chore: fix typescript errors --- tns-core-modules/ui/core/view/view.d.ts | 3 +- .../ui/frame/fragment.transitions.android.ts | 30 +++- .../ui/frame/fragment.transitions.d.ts | 139 +++++++----------- tns-core-modules/ui/frame/frame.android.ts | 8 +- 4 files changed, 88 insertions(+), 92 deletions(-) diff --git a/tns-core-modules/ui/core/view/view.d.ts b/tns-core-modules/ui/core/view/view.d.ts index f887e4794c..d0543484a1 100644 --- a/tns-core-modules/ui/core/view/view.d.ts +++ b/tns-core-modules/ui/core/view/view.d.ts @@ -645,8 +645,9 @@ export abstract class View extends ViewBase { _gestureObservers: any; /** * @private + * androidx.fragment.app.FragmentManager */ - _manager: androidx.fragment.app.FragmentManager + _manager: any; /** * @private */ diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts index 07d70364a7..d52b182e3a 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -3,7 +3,6 @@ // Definitions. import { NavigationType } from "./frame-common"; import { NavigationTransition, BackstackEntry } from "../frame"; -import { ExpandedEntry, ExpandedTransitionListener, ExpandedAnimator } from "./fragment.transitions"; // Types. import { Transition, AndroidTransitionType } from "../transition/transition"; @@ -24,6 +23,35 @@ export const completedEntries = new Map(); let TransitionListener: TransitionListener; let AnimationListener: android.animation.Animator.AnimatorListener; +interface ExpandedTransitionListener extends androidx.transition.Transition.TransitionListener { + entry: ExpandedEntry; + transition: androidx.transition.Transition; +} + +interface ExpandedAnimator extends android.animation.Animator { + entry: ExpandedEntry; + transitionType?: string; +} + +interface ExpandedEntry extends BackstackEntry { + + enterTransitionListener: ExpandedTransitionListener; + exitTransitionListener: ExpandedTransitionListener; + reenterTransitionListener: ExpandedTransitionListener; + returnTransitionListener: ExpandedTransitionListener; + + enterAnimator: ExpandedAnimator; + exitAnimator: ExpandedAnimator; + popEnterAnimator: ExpandedAnimator; + popExitAnimator: ExpandedAnimator; + + transition: Transition; + transitionName: string; + frameId: number; + + isNestedDefaultTransition: boolean; +} + export function _setAndroidFragmentTransitions( animated: boolean, navigationTransition: NavigationTransition, diff --git a/tns-core-modules/ui/frame/fragment.transitions.d.ts b/tns-core-modules/ui/frame/fragment.transitions.d.ts index 5c9305b1ba..d4e06f1e73 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.d.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.d.ts @@ -2,92 +2,59 @@ * @module "ui/transition" */ /** */ - import { NavigationTransition, BackstackEntry } from "../frame"; - // Types. - import { Transition, AndroidTransitionType } from "../transition/transition"; - - /** - * @private - */ - - export interface ExpandedTransitionListener extends androidx.transition.Transition.TransitionListener { - entry: ExpandedEntry; - transition: androidx.transition.Transition; - } +import { NavigationTransition, BackstackEntry } from "../frame"; +// Types. +import { Transition, AndroidTransitionType } from "../transition/transition"; - export interface ExpandedAnimator extends android.animation.Animator { - entry: ExpandedEntry; - transitionType?: string; -} - - export interface ExpandedEntry extends BackstackEntry { - - enterTransitionListener: ExpandedTransitionListener; - exitTransitionListener: ExpandedTransitionListener; - reenterTransitionListener: ExpandedTransitionListener; - returnTransitionListener: ExpandedTransitionListener; - - enterAnimator: ExpandedAnimator; - exitAnimator: ExpandedAnimator; - popEnterAnimator: ExpandedAnimator; - popExitAnimator: ExpandedAnimator; +/** + * @private + */ +export function _setAndroidFragmentTransitions( + animated: boolean, + navigationTransition: NavigationTransition, + currentEntry: BackstackEntry, + newEntry: BackstackEntry, + frameId: number, + fragmentTransaction: any, + isNestedDefaultTransition?: boolean): void; +/** + * @private + */ +export function _getAnimatedEntries(frameId: number): Set; +/** + * @private + * Called once fragment is recreated after it was destroyed. + * Reapply animations and transitions from entry to fragment if any. + */ +export function _updateTransitions(entry: BackstackEntry): void; +/** + * @private + * Called once fragment is going to reappear from backstack. + * Reverse transitions from entry to fragment if any. + */ +export function _reverseTransitions(previousEntry: BackstackEntry, currentEntry: BackstackEntry): boolean; +/** + * @private + * Called when entry is removed from backstack (either back navigation or + * navigate with clear history). Removes all animations and transitions from entry + * and fragment and clears all listeners in order to prevent memory leaks. + */ +export function _clearEntry(entry: BackstackEntry): void; +/** + * @private + * Called when fragment is destroyed because activity is destroyed. + * Removes all animations and transitions but keeps them on the entry + * in order to reapply them when new fragment is created for the same entry. + */ +export function _clearFragment(entry: BackstackEntry): void; +/** + * @private + */ +export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: any, operation: number, fromVC: any, toVC: any): any; - transition: Transition; - transitionName: string; - frameId: number; - - isNestedDefaultTransition: boolean - } - - /** - * @private - */ - export function _setAndroidFragmentTransitions( - animated: boolean, - navigationTransition: NavigationTransition, - currentEntry: BackstackEntry, - newEntry: BackstackEntry, - frameId: number, - fragmentTransaction: any, - isNestedDefaultTransition?: boolean): void; - /** - * @private - */ - export function _getAnimatedEntries(frameId: number): Set; - /** - * @private - * Called once fragment is recreated after it was destroyed. - * Reapply animations and transitions from entry to fragment if any. - */ - export function _updateTransitions(entry: BackstackEntry): void; - /** - * @private - * Called once fragment is going to reappear from backstack. - * Reverse transitions from entry to fragment if any. - */ - export function _reverseTransitions(previousEntry: BackstackEntry, currentEntry: BackstackEntry): boolean; - /** - * @private - * Called when entry is removed from backstack (either back navigation or - * navigate with clear history). Removes all animations and transitions from entry - * and fragment and clears all listeners in order to prevent memory leaks. - */ - export function _clearEntry(entry: BackstackEntry): void; - /** - * @private - * Called when fragment is destroyed because activity is destroyed. - * Removes all animations and transitions but keeps them on the entry - * in order to reapply them when new fragment is created for the same entry. - */ - export function _clearFragment(entry: BackstackEntry): void; - /** - * @private - */ - export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: any, operation: number, fromVC: any, toVC: any): any; - - /** - * @private - */ - export function addNativeTransitionListener(entry: ExpandedEntry, nativeTransition: androidx.transition.Transition): ExpandedTransitionListener; +/** + * @private + * nativeTransition: androidx.transition.Transition + */ +export function addNativeTransitionListener(entry: any, nativeTransition: any): any; //@endprivate - \ 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 5329baedbe..35618cc49d 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -14,8 +14,8 @@ import { } from "./frame-common"; import { - _setAndroidFragmentTransitions, _getAnimatedEntries, ExpandedEntry, - _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, addNativeTransitionListener, ExpandedTransitionListener + _setAndroidFragmentTransitions, _getAnimatedEntries, + _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, addNativeTransitionListener } from "./fragment.transitions"; // TODO: Remove this and get it from global to decouple builder for angular @@ -553,7 +553,7 @@ export class Frame extends FrameBase { }); } } -function cloneExpandedTransitionListener(expandedTransitionListener: ExpandedTransitionListener) { +function cloneExpandedTransitionListener(expandedTransitionListener: any) { if (!expandedTransitionListener) { return null; } @@ -811,7 +811,7 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { @profile public onCreateAnimator(fragment: androidx.fragment.app.Fragment, transit: number, enter: boolean, nextAnim: number, superFunc: Function): android.animation.Animator { let animator = null; - const entry = (this.entry); + const entry = this.entry; // Return enterAnimator only when new (no current entry) nested transition. if (enter && entry.isNestedDefaultTransition) { From da2e8a8e9a0382be89dc0f6efd2a90fc1212094f Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Wed, 9 Oct 2019 16:45:15 +0300 Subject: [PATCH 6/9] fix(frame-android): child already has a parent. Replace removeView with removeAllViews --- tns-core-modules/ui/frame/frame.android.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index 35618cc49d..3f3c32db12 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -910,7 +910,7 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { parentView.addViewInLayout(nativeView, -1, new org.nativescript.widgets.CommonLayoutParams()); } - parentView.removeView(nativeView); + parentView.removeAllViews(); } } From be232d2a0798840ea0958b11f27fde1afe68b8d9 Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Thu, 10 Oct 2019 13:55:26 +0300 Subject: [PATCH 7/9] fix(tests): wait for fragment before isAdded() check --- tests/app/ui/tab-view/tab-view-root-tests.ts | 2 +- .../tab-navigation-base/tab-navigation-base.ts | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/tests/app/ui/tab-view/tab-view-root-tests.ts b/tests/app/ui/tab-view/tab-view-root-tests.ts index 672c1dc2a1..7eab7a11cb 100644 --- a/tests/app/ui/tab-view/tab-view-root-tests.ts +++ b/tests/app/ui/tab-view/tab-view-root-tests.ts @@ -22,7 +22,7 @@ function waitUntilTabViewReady(page: Page, action: Function) { action(); if (isAndroid) { - TKUnit.waitUntilReady(() => page.frame._currentEntry.fragment.isAdded()); + TKUnit.waitUntilReady(() => page.frame._currentEntry.fragment && page.frame._currentEntry.fragment.isAdded()); } else { TKUnit.waitUntilReady(() => page.isLoaded); } diff --git a/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts b/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts index 62ee57642c..ddd1429de8 100644 --- a/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts +++ b/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts @@ -11,19 +11,6 @@ import { View, Property, CoercibleProperty, isIOS, Color } from "../../core/view // TODO: Impl trace // export const traceCategory = "TabView"; -const TABID = "_tabId"; -const INDEX = "_index"; - -function getTabById(id: number): any { - const ref = _tabs.find(ref => { - const tab = ref.get(); - - return tab && tab._domId === id; - }); - - return ref && ref.get(); -} - export module knownCollections { export const items = "items"; } From 6d299742d046dfdab4da3a6d08c6c26c7ebc107b Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Thu, 10 Oct 2019 17:24:46 +0300 Subject: [PATCH 8/9] fix(bottom-navigation): prevent changeTab logic when fragment manager is destroyed --- .../ui/bottom-navigation/bottom-navigation.android.ts | 8 +++++++- tns-core-modules/ui/frame/frame.android.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts b/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts index bd2bd4e423..42fda2a85b 100644 --- a/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts +++ b/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts @@ -387,8 +387,14 @@ export class BottomNavigation extends TabNavigationBase { _onAttachedToWindow(): void { super._onAttachedToWindow(); - this._attachedToWindow = true; + + // _onAttachedToWindow called from OS again after it was detach + // TO DO: Consider testing and removing it when update to androidx.fragment:1.2.0 + if (this._manager && this._manager.isDestroyed()) { + return; + } + this.changeTab(this.selectedIndex); } diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index 3f3c32db12..ee75c6c3d6 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -157,6 +157,8 @@ export class Frame extends FrameBase { super._onAttachedToWindow(); this._attachedToWindow = true; + // _onAttachedToWindow called from OS again after it was detach + // TO DO: Consider testing and removing it when update to androidx.fragment:1.2.0 if (this._manager && this._manager.isDestroyed()) { return; } From 1fbeb15e9736e1e65d236e6df4edc644f843210b Mon Sep 17 00:00:00 2001 From: ADjenkov Date: Tue, 15 Oct 2019 15:20:12 +0300 Subject: [PATCH 9/9] chore: apply PR comments changes --- .../widgets/CustomTransition.java | 14 +++++---- .../nativescript/widgets/FragmentBase.java | 15 ++++++++++ .../bottom-navigation.android.ts | 14 ++++----- tns-core-modules/ui/frame/fragment.android.ts | 2 +- .../ui/frame/fragment.transitions.android.ts | 3 ++ tns-core-modules/ui/frame/frame.android.ts | 20 +++++++------ .../ui/tab-view/tab-view.android.ts | 30 +++++++++---------- tns-core-modules/ui/tabs/tabs.android.ts | 10 +++---- .../android/org.nativescript.widgets.d.ts | 6 ++++ 9 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java index a0f3b5cf57..66bccc2768 100644 --- a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java @@ -49,29 +49,31 @@ public Animator onDisappear(@NonNull ViewGroup sceneRoot, @NonNull final View vi return this.setAnimatorsTarget(this.animatorSet, view); } - public void setResetOnTransitionEnd(boolean resetOnTransitionEnd){ + public void setResetOnTransitionEnd(boolean resetOnTransitionEnd) { this.resetOnTransitionEnd = resetOnTransitionEnd; } public String getTransitionName(){ - return this.transitionName; + return this.transitionName; } - private Animator setAnimatorsTarget(AnimatorSet animatorSet, final View view){ + private Animator setAnimatorsTarget(AnimatorSet animatorSet, final View view) { ArrayList animatorsList = animatorSet.getChildAnimations(); boolean resetOnTransitionEnd = this.resetOnTransitionEnd; for (int i = 0; i < animatorsList.size(); i++) { - animatorsList.get(i).setTarget(view); + animatorsList.get(i).setTarget(view); } + // Reset animation to its initial state to prevent mirrorered effect if (this.resetOnTransitionEnd) { - this.immediateAnimatorSet = this.animatorSet.clone(); + this.immediateAnimatorSet = this.animatorSet.clone(); } + // Switching to hardware layer during transition to improve animation performance CustomAnimatorListener listener = new CustomAnimatorListener(view); animatorSet.addListener(listener); - addListener(new CustomTransitionListenerAdapter(this)); + this.addListener(new CustomTransitionListenerAdapter(this)); return this.animatorSet; } diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java new file mode 100644 index 0000000000..1270ca0380 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java @@ -0,0 +1,15 @@ +package org.nativescript.widgets; + +import android.animation.Animator; +import androidx.fragment.app.Fragment; + +public abstract class FragmentBase extends Fragment { + public Fragment getRemovingParentFragment() { + Fragment parentFragment = this.getParentFragment(); + while (parentFragment != null && !parentFragment.isRemoving()) { + parentFragment = parentFragment.getParentFragment(); + } + + return parentFragment; + } +} \ No newline at end of file diff --git a/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts b/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts index 42fda2a85b..8353f38c54 100644 --- a/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts +++ b/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts @@ -55,7 +55,7 @@ function initializeNativeClasses() { return; } - class TabFragmentImplementation extends androidx.fragment.app.Fragment { + class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { private owner: BottomNavigation; private index: number; private backgroundBitmap: android.graphics.Bitmap = null; @@ -82,7 +82,7 @@ function initializeNativeClasses() { this.owner = getTabById(args.getInt(TABID)); this.index = args.getInt(INDEX); if (!this.owner) { - throw new Error(`Cannot find TabView`); + throw new Error(`Cannot find BottomNavigation`); } } @@ -93,10 +93,10 @@ function initializeNativeClasses() { } public onDestroyView() { - const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + const hasRemovingParent = this.getRemovingParentFragment(); // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. - // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + // TODO: Consider removing it when update to androidx.fragment:1.2.0 if (hasRemovingParent && this.owner.selectedIndex === this.index) { const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(appResources, this.backgroundBitmap); this.owner._originalBackground = this.owner.backgroundColor || new Color("White"); @@ -108,10 +108,10 @@ function initializeNativeClasses() { } public onPause(): void { - const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + const hasRemovingParent = this.getRemovingParentFragment(); // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. - // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + // TODO: Consider removing it when update to androidx.fragment:1.2.0 if (hasRemovingParent && this.owner.selectedIndex === this.index) { this.backgroundBitmap = this.loadBitmapFromView(this.owner.nativeViewProtected); } @@ -390,7 +390,7 @@ export class BottomNavigation extends TabNavigationBase { this._attachedToWindow = true; // _onAttachedToWindow called from OS again after it was detach - // TO DO: Consider testing and removing it when update to androidx.fragment:1.2.0 + // TODO: Consider testing and removing it when update to androidx.fragment:1.2.0 if (this._manager && this._manager.isDestroyed()) { return; } diff --git a/tns-core-modules/ui/frame/fragment.android.ts b/tns-core-modules/ui/frame/fragment.android.ts index a190b675e4..f24961613b 100644 --- a/tns-core-modules/ui/frame/fragment.android.ts +++ b/tns-core-modules/ui/frame/fragment.android.ts @@ -1,7 +1,7 @@ import { AndroidFragmentCallbacks, setFragmentCallbacks, setFragmentClass } from "./frame"; @JavaProxy("com.tns.FragmentClass") -class FragmentClass extends androidx.fragment.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; diff --git a/tns-core-modules/ui/frame/fragment.transitions.android.ts b/tns-core-modules/ui/frame/fragment.transitions.android.ts index d52b182e3a..266dea6a77 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -101,6 +101,9 @@ export function _setAndroidFragmentTransitions( if (name === "none") { const noTransition = new NoTransition(0, null); + // Setup empty/immediate animator when transitioning to nested frame for first time. + // Also setup empty/immediate transition to be executed when navigating back to this page. + // TODO: Consider removing empty/immediate animator when migrating to official androidx.fragment.app.Fragment:1.2. if (isNestedDefaultTransition) { fragmentTransaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out); setupAllAnimation(newEntry, noTransition); diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index ee75c6c3d6..b900c3519e 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -158,7 +158,7 @@ export class Frame extends FrameBase { this._attachedToWindow = true; // _onAttachedToWindow called from OS again after it was detach - // TO DO: Consider testing and removing it when update to androidx.fragment:1.2.0 + // TODO: Consider testing and removing it when update to androidx.fragment:1.2.0 if (this._manager && this._manager.isDestroyed()) { return; } @@ -265,6 +265,7 @@ export class Frame extends FrameBase { const fragment = this._currentEntry.fragment; const fragmentExitTransition = fragment.getExitTransition(); + // Reset animation to its initial state to prevent mirrorered effect when restore current fragment transitions if (fragmentExitTransition && fragmentExitTransition instanceof org.nativescript.widgets.CustomTransition) { fragmentExitTransition.setResetOnTransitionEnd(true); } @@ -432,7 +433,7 @@ export class Frame extends FrameBase { _setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction, isNestedDefaultTransition); if (currentEntry && animated && !navigationTransition) { - //TO DO: Check whether or not this is still necessary. For Modal views? + //TODO: Check whether or not this is still necessary. For Modal views? //transaction.setTransition(androidx.fragment.app.FragmentTransaction.TRANSIT_FRAGMENT_OPEN); } @@ -928,13 +929,14 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { } @profile - public onDestroyView(fragment: androidx.fragment.app.Fragment, superFunc: Function): void { + public onDestroyView(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void { if (traceEnabled()) { traceWrite(`${fragment}.onDestroyView()`, traceCategories.NativeLifecycle); } - const parentFragment = fragment.getParentFragment(); - if (parentFragment && (parentFragment.isRemoving())) { + const hasRemovingParent = fragment.getRemovingParentFragment(); + + if (hasRemovingParent) { const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(application.android.context.getResources(), this.backgroundBitmap); this.frame.nativeViewProtected.setBackgroundDrawable(bitmapDrawable); this.backgroundBitmap = null; @@ -972,12 +974,12 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { } @profile - public onPause(fragment: androidx.fragment.app.Fragment, superFunc: Function): void { + public onPause(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void { // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. - // TO DO: Consider removing it when update to androidx.fragment:1.2.0 - const parentFragment = fragment.getParentFragment(); + // TODO: Consider removing it when update to androidx.fragment:1.2.0 + const hasRemovingParent = fragment.getRemovingParentFragment(); - if (parentFragment && (parentFragment.isRemoving())) { + if (hasRemovingParent) { this.backgroundBitmap = this.loadBitmapFromView(this.frame.nativeViewProtected); } superFunc.call(fragment); 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 f8fa74a2fe..c8a75a816e 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -48,7 +48,7 @@ function initializeNativeClasses() { return; } - class TabFragmentImplementation extends androidx.fragment.app.Fragment { + class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { private owner: TabView; private index: number; private backgroundBitmap: android.graphics.Bitmap = null; @@ -86,10 +86,10 @@ function initializeNativeClasses() { } public onDestroyView() { - const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + const hasRemovingParent = this.getRemovingParentFragment(); // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. - // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + // TODO: Consider removing it when update to androidx.fragment:1.2.0 if (hasRemovingParent && this.owner.selectedIndex === this.index) { const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(appResources, this.backgroundBitmap); this.owner._originalBackground = this.owner.backgroundColor || new Color("White"); @@ -101,10 +101,10 @@ function initializeNativeClasses() { } public onPause(): void { - const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + const hasRemovingParent = this.getRemovingParentFragment(); // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. - // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + // TODO: Consider removing it when update to androidx.fragment:1.2.0 if (hasRemovingParent && this.owner.selectedIndex === this.index) { this.backgroundBitmap = this.loadBitmapFromView(this.owner.nativeViewProtected); } @@ -114,16 +114,16 @@ function initializeNativeClasses() { private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap { // Another way to get view bitmap. Test performance vs setDrawingCacheEnabled - const width = view.getWidth(); - const height = view.getHeight(); - const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888); - const canvas = new android.graphics.Canvas(bitmap); - view.layout(0, 0, width, height); - view.draw(canvas); - - // view.setDrawingCacheEnabled(true); - // const bitmap = android.graphics.Bitmap.createBitmap(view.getDrawingCache()); - // view.setDrawingCacheEnabled(false); + // const width = view.getWidth(); + // const height = view.getHeight(); + // const bitmap = android.graphics.Bitmap.createBitmap(width, height, android.graphics.Bitmap.Config.ARGB_8888); + // const canvas = new android.graphics.Canvas(bitmap); + // view.layout(0, 0, width, height); + // view.draw(canvas); + + view.setDrawingCacheEnabled(true); + const bitmap = android.graphics.Bitmap.createBitmap(view.getDrawingCache()); + view.setDrawingCacheEnabled(false); return bitmap; } diff --git a/tns-core-modules/ui/tabs/tabs.android.ts b/tns-core-modules/ui/tabs/tabs.android.ts index 085fc23736..d34bf105bd 100644 --- a/tns-core-modules/ui/tabs/tabs.android.ts +++ b/tns-core-modules/ui/tabs/tabs.android.ts @@ -53,7 +53,7 @@ function initializeNativeClasses() { return; } - class TabFragmentImplementation extends androidx.fragment.app.Fragment { + class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { private owner: Tabs; private index: number; private backgroundBitmap: android.graphics.Bitmap = null; @@ -91,10 +91,10 @@ function initializeNativeClasses() { } public onDestroyView() { - const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + const hasRemovingParent = this.getRemovingParentFragment(); // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. - // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + // TODO: Consider removing it when update to androidx.fragment:1.2.0 if (hasRemovingParent && this.owner.selectedIndex === this.index) { const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(appResources, this.backgroundBitmap); this.owner._originalBackground = this.owner.backgroundColor || new Color("White"); @@ -106,10 +106,10 @@ function initializeNativeClasses() { } public onPause(): void { - const hasRemovingParent = this.getParentFragment() && this.getParentFragment().isRemoving(); + const hasRemovingParent = this.getRemovingParentFragment(); // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. - // TO DO: Consider removing it when update to androidx.fragment:1.2.0 + // TODO: Consider removing it when update to androidx.fragment:1.2.0 if (hasRemovingParent && this.owner.selectedIndex === this.index) { this.backgroundBitmap = this.loadBitmapFromView(this.owner.nativeViewProtected); } diff --git a/tns-platform-declarations/android/org.nativescript.widgets.d.ts b/tns-platform-declarations/android/org.nativescript.widgets.d.ts index b26cdde22e..cf6873c3e1 100644 --- a/tns-platform-declarations/android/org.nativescript.widgets.d.ts +++ b/tns-platform-declarations/android/org.nativescript.widgets.d.ts @@ -62,6 +62,12 @@ } } + export class FragmentBase extends androidx.fragment.app.Fragment { + constructor(); + + public getRemovingParentFragment(): androidx.fragment.app.Fragment; + } + export class BorderDrawable extends android.graphics.drawable.ColorDrawable { constructor(density: number); constructor(density: number, id: string);