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/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; 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() 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..66bccc2768 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTransition.java @@ -0,0 +1,135 @@ +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); + } + + // Reset animation to its initial state to prevent mirrorered effect + if (this.resetOnTransitionEnd) { + this.immediateAnimatorSet = this.animatorSet.clone(); + } + + // Switching to hardware layer during transition to improve animation performance + CustomAnimatorListener listener = new CustomAnimatorListener(view); + animatorSet.addListener(listener); + this.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 index 88a2217196..1270ca0380 100644 --- 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 @@ -4,27 +4,6 @@ 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()) { @@ -33,4 +12,4 @@ public Fragment getRemovingParentFragment() { 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 0fafaf048a..8353f38c54 100644 --- a/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts +++ b/tns-core-modules/ui/bottom-navigation/bottom-navigation.android.ts @@ -34,6 +34,7 @@ const ownerSymbol = Symbol("_owner"); let TabFragment: any; let BottomNavigationBar: any; let AttachStateChangeListener: any; +let appResources: android.content.res.Resources; function makeFragmentName(viewId: number, id: number): string { return "android:bottomnavigation:" + viewId + ":" + id; @@ -55,8 +56,9 @@ function initializeNativeClasses() { } class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { - private tab: BottomNavigation; + private owner: BottomNavigation; private index: number; + private backgroundBitmap: android.graphics.Bitmap = null; constructor() { super(); @@ -77,18 +79,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 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]; + const tabItem = this.owner.items[this.index]; return tabItem.nativeViewProtected; } + + public onDestroyView() { + const hasRemovingParent = this.getRemovingParentFragment(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // 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"); + this.owner.nativeViewProtected.setBackgroundDrawable(bitmapDrawable); + this.backgroundBitmap = null; + } + + super.onDestroyView(); + } + + public onPause(): void { + const hasRemovingParent = this.getRemovingParentFragment(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // 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); + } + + 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; + } } class BottomNavigationBarImplementation extends org.nativescript.widgets.BottomNavigationBar { @@ -168,6 +213,7 @@ function initializeNativeClasses() { TabFragment = TabFragmentImplementation; BottomNavigationBar = BottomNavigationBarImplementation; AttachStateChangeListener = new AttachListener(); + appResources = application.android.context.getResources(); } function setElevation(bottomNavigationBar: org.nativescript.widgets.BottomNavigationBar) { @@ -196,6 +242,7 @@ export class BottomNavigation extends TabNavigationBase { private _currentFragment: androidx.fragment.app.Fragment; private _currentTransaction: androidx.fragment.app.FragmentTransaction; private _attachedToWindow = false; + public _originalBackground: any; constructor() { super(); @@ -320,6 +367,12 @@ export class BottomNavigation extends TabNavigationBase { public onLoaded(): void { super.onLoaded(); + if (this._originalBackground) { + this.backgroundColor = null; + this.backgroundColor = this._originalBackground; + this._originalBackground = null; + } + if (this.tabStrip) { this.setTabStripItems(this.tabStrip.items); } else { @@ -334,8 +387,14 @@ export class BottomNavigation extends TabNavigationBase { _onAttachedToWindow(): void { super._onAttachedToWindow(); - this._attachedToWindow = true; + + // _onAttachedToWindow called from OS again after it was detach + // TODO: 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/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 be60affc39..d0543484a1 100644 --- a/tns-core-modules/ui/core/view/view.d.ts +++ b/tns-core-modules/ui/core/view/view.d.ts @@ -643,6 +643,11 @@ export abstract class View extends ViewBase { * @private */ _gestureObservers: any; + /** + * @private + * androidx.fragment.app.FragmentManager + */ + _manager: any; /** * @private */ diff --git a/tns-core-modules/ui/frame/fragment.android.ts b/tns-core-modules/ui/frame/fragment.android.ts index e6cb0ec83a..f24961613b 100644 --- a/tns-core-modules/ui/frame/fragment.android.ts +++ b/tns-core-modules/ui/frame/fragment.android.ts @@ -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 85e905b800..266dea6a77 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.android.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.android.ts @@ -3,36 +3,38 @@ // Definitions. import { NavigationType } from "./frame-common"; import { NavigationTransition, BackstackEntry } from "../frame"; -import { AnimationType } from "./fragment.transitions.types"; // 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; + new(entry: ExpandedEntry, transition: androidx.transition.Transition): ExpandedTransitionListener; } -interface ExpandedAnimator extends android.animation.Animator { +const defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator()); + +export const waitingQueue = new Map>(); +export const completedEntries = new Map(); + +let TransitionListener: TransitionListener; +let AnimationListener: android.animation.Animator.AnimatorListener; + +interface ExpandedTransitionListener extends androidx.transition.Transition.TransitionListener { entry: ExpandedEntry; - transitionType?: string; + transition: androidx.transition.Transition; } -interface ExpandedTransitionListener extends android.transition.Transition.TransitionListener { +interface ExpandedAnimator extends android.animation.Animator { entry: ExpandedEntry; - transition: android.transition.Transition; + transitionType?: string; } interface ExpandedEntry extends BackstackEntry { + enterTransitionListener: ExpandedTransitionListener; exitTransitionListener: ExpandedTransitionListener; reenterTransitionListener: ExpandedTransitionListener; @@ -43,32 +45,21 @@ interface ExpandedEntry extends BackstackEntry { 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>(); -export const completedEntries = new Map(); -let TransitionListener: TransitionListener; -let AnimationListener: android.animation.Animator.AnimatorListener; + isNestedDefaultTransition: boolean; +} export function _setAndroidFragmentTransitions( animated: boolean, 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 +68,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 +79,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 +92,68 @@ 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); + + // 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); + 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") { + 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 +161,106 @@ 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(); - 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; + // 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; +} + +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 animator; + 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); + } + + animator.entry = null; } export function _getAnimatedEntries(frameId: number): Set { @@ -262,22 +294,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 +316,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 +334,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 +343,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 +369,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 +380,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 +432,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 +479,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 +489,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 +499,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 +509,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 +524,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 +549,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()); + setEnterTransition(navTransition, entry, new androidx.transition.Explode()); + setReturnTransition(navTransition, entry, new androidx.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; -} - -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 +636,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 +677,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 +688,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 +705,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..d4e06f1e73 100644 --- a/tns-core-modules/ui/frame/fragment.transitions.d.ts +++ b/tns-core-modules/ui/frame/fragment.transitions.d.ts @@ -3,11 +3,8 @@ */ /** */ import { NavigationTransition, BackstackEntry } from "../frame"; - -/** - * @private - */ -export { AnimationType } from "./fragment.transitions.types"; +// Types. +import { Transition, AndroidTransitionType } from "../transition/transition"; /** * @private @@ -17,12 +14,9 @@ export function _setAndroidFragmentTransitions( navigationTransition: NavigationTransition, currentEntry: BackstackEntry, newEntry: BackstackEntry, + frameId: number, fragmentTransaction: any, - frameId: number): void; -/** - * @private - */ -export function _onFragmentCreateAnimator(entry: BackstackEntry, fragment: any, nextAnim: number, enter: boolean): any; + isNestedDefaultTransition?: boolean): void; /** * @private */ @@ -57,4 +51,10 @@ export function _clearFragment(entry: BackstackEntry): void; * @private */ export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: any, operation: number, fromVC: any, toVC: any): any; -//@endprivate + +/** + * @private + * nativeTransition: androidx.transition.Transition + */ +export function addNativeTransitionListener(entry: any, nativeTransition: any): any; + //@endprivate diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index b35a2e8448..b900c3519e 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, + _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, addNativeTransitionListener } 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"; @@ -47,6 +48,7 @@ const activityRootViewsMap = new Map>(); let navDepth = -1; let fragmentId = -1; + export let moduleLoaded: boolean; if (global && global.__inspector) { @@ -118,7 +120,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 +156,13 @@ export class Frame extends FrameBase { _onAttachedToWindow(): void { super._onAttachedToWindow(); this._attachedToWindow = true; + + // _onAttachedToWindow called from OS again after it was detach + // TODO: Consider testing and removing it when update to androidx.fragment:1.2.0 + if (this._manager && this._manager.isDestroyed()) { + return; + } + this._processNextNavigationEntry(); } @@ -182,7 +191,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 +204,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 +262,15 @@ 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(); + + // 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); + } + + transaction.remove(fragment); transaction.commitNowAllowingStateLoss(); } @@ -314,9 +338,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 +352,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 +428,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); + //TODO: 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 +457,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 +556,50 @@ export class Frame extends FrameBase { }); } } - -function cloneExpandedAnimator(expandedAnimator: any) { - if (!expandedAnimator) { +function cloneExpandedTransitionListener(expandedTransitionListener: any) { + 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 +801,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 +812,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 @@ -902,7 +913,7 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { parentView.addViewInLayout(nativeView, -1, new org.nativescript.widgets.CommonLayoutParams()); } - parentView.removeView(nativeView); + parentView.removeAllViews(); } } @@ -918,11 +929,18 @@ 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 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; + } superFunc.call(fragment); } @@ -955,6 +973,18 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks { } } + @profile + 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. + // TODO: Consider removing it when update to androidx.fragment:1.2.0 + const hasRemovingParent = fragment.getRemovingParentFragment(); + + if (hasRemovingParent) { + 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 +999,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/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.ts b/tns-core-modules/ui/tab-navigation-base/tab-navigation-base/tab-navigation-base.ts index bc0cb9d249..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 @@ -6,7 +6,7 @@ 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"; @@ -265,6 +265,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..c8a75a816e 100644 --- a/tns-core-modules/ui/tab-view/tab-view.android.ts +++ b/tns-core-modules/ui/tab-view/tab-view.android.ts @@ -27,6 +27,7 @@ interface PagerAdapter { const TABID = "_tabId"; const INDEX = "_index"; let PagerAdapter: PagerAdapter; +let appResources: android.content.res.Resources; function makeFragmentName(viewId: number, id: number): string { return "android:viewpager:" + viewId + ":" + id; @@ -48,8 +49,9 @@ function initializeNativeClasses() { } class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { - private tab: TabView; + private owner: TabView; private index: number; + private backgroundBitmap: android.graphics.Bitmap = null; constructor() { super(); @@ -70,18 +72,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.getRemovingParentFragment(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // 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"); + this.owner.nativeViewProtected.setBackground(bitmapDrawable); + this.backgroundBitmap = null; + } + + super.onDestroyView(); + } + + public onPause(): void { + const hasRemovingParent = this.getRemovingParentFragment(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // 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); + } + + 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; @@ -233,6 +278,7 @@ function initializeNativeClasses() { } PagerAdapter = FragmentPagerAdapter; + appResources = application.android.context.getResources(); } function createTabItemSpec(item: TabViewItem): org.nativescript.widgets.TabItemSpec { @@ -249,7 +295,7 @@ function createTabItemSpec(item: TabViewItem): org.nativescript.widgets.TabItemS const is = fromFileOrResource(item.iconSource); if (is) { // TODO: Make this native call that accepts string so that we don't load Bitmap in JS. - result.iconDrawable = new android.graphics.drawable.BitmapDrawable(application.android.context.getResources(), is.android); + result.iconDrawable = new android.graphics.drawable.BitmapDrawable(appResources, is.android); } else { traceMissingIcon(item.iconSource); } @@ -397,6 +443,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(); @@ -534,6 +581,12 @@ export class TabView extends TabViewBase { public onLoaded(): void { super.onLoaded(); + if (this._originalBackground) { + this.backgroundColor = null; + this.backgroundColor = this._originalBackground; + this._originalBackground = null; + } + 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..d34bf105bd 100644 --- a/tns-core-modules/ui/tabs/tabs.android.ts +++ b/tns-core-modules/ui/tabs/tabs.android.ts @@ -23,14 +23,16 @@ const ACCENT_COLOR = "colorAccent"; const PRIMARY_COLOR = "colorPrimary"; const DEFAULT_ELEVATION = 4; +const TABID = "_tabId"; +const INDEX = "_index"; + interface PagerAdapter { new(owner: Tabs): androidx.viewpager.widget.PagerAdapter; } -const TABID = "_tabId"; -const INDEX = "_index"; let PagerAdapter: PagerAdapter; let TabsBar: any; +let appResources: android.content.res.Resources; function makeFragmentName(viewId: number, id: number): string { return "android:viewpager:" + viewId + ":" + id; @@ -52,8 +54,9 @@ function initializeNativeClasses() { } class TabFragmentImplementation extends org.nativescript.widgets.FragmentBase { - private tab: Tabs; + private owner: Tabs; private index: number; + private backgroundBitmap: android.graphics.Bitmap = null; constructor() { super(); @@ -74,18 +77,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.nativeViewProtected; } + + public onDestroyView() { + const hasRemovingParent = this.getRemovingParentFragment(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // 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"); + this.owner.nativeViewProtected.setBackgroundDrawable(bitmapDrawable); + this.backgroundBitmap = null; + } + + super.onDestroyView(); + } + + public onPause(): void { + const hasRemovingParent = this.getRemovingParentFragment(); + + // Get view as bitmap and set it as background. This is workaround for the disapearing nested fragments. + // 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); + } + + 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; @@ -285,6 +331,7 @@ function initializeNativeClasses() { PagerAdapter = FragmentPagerAdapter; TabsBar = TabsBarImplementation; + appResources = application.android.context.getResources(); } let defaultAccentColor: number = undefined; @@ -325,6 +372,7 @@ export class Tabs extends TabsBase { private _viewPager: androidx.viewpager.widget.ViewPager; private _pagerAdapter: androidx.viewpager.widget.PagerAdapter; private _androidViewId: number = -1; + public _originalBackground: any; constructor() { super(); @@ -456,6 +504,12 @@ export class Tabs extends TabsBase { public onLoaded(): void { super.onLoaded(); + if (this._originalBackground) { + this.backgroundColor = null; + this.backgroundColor = this._originalBackground; + this._originalBackground = null; + } + this.setItems((this.items)); if (this.tabStrip) { @@ -653,7 +707,7 @@ export class Tabs extends TabsBase { image = this.getFixedSizeIcon(image); } - imageDrawable = new android.graphics.drawable.BitmapDrawable(application.android.context.getResources(), image); + imageDrawable = new android.graphics.drawable.BitmapDrawable(appResources, image); } else { // TODO // traceMissingIcon(iconSource); 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 af1127ef9c..cf6873c3e1 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); @@ -57,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); @@ -173,12 +184,6 @@ public verticalAlignment: VerticalAlignment; } - export class FragmentBase extends androidx.fragment.app.Fragment { - constructor(); - - public getRemovingParentFragment(): androidx.fragment.app.Fragment; - } - export enum Stretch { none, aspectFill,