feat: overhaul and streamline Android page navigation transitions (#7925)

* feat: remove Animators and replace with Transitions

* fix: handle disappearing nested fragments for tabs.

Extract TabFragmentImplementation in tab-navigation base for both tabs and bottom navigation

* chore: bump webpack cycles counter

* feat(android-widgets): add androidx.transition:transition as dependency

* chore: fix typescript errors

* fix(frame-android): child already has a parent. Replace removeView with removeAllViews

* fix(tests): wait for fragment before isAdded() check

* fix(bottom-navigation): prevent changeTab logic when fragment manager is destroyed

* chore: apply PR comments changes
This commit is contained in:
Alexander Djenkov
2019-10-15 18:19:47 +03:00
committed by GitHub
parent dc6540269f
commit 08e23bcc3b
20 changed files with 834 additions and 574 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -4,27 +4,6 @@ import android.animation.Animator;
import androidx.fragment.app.Fragment;
public abstract class FragmentBase extends Fragment {
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
// [nested frames / fragments] apply dummy animator to the nested fragment with
// the same duration as the exit animator of the removing parent fragment to work around
// https://code.google.com/p/android/issues/detail?id=55228 (child fragments disappear
// when parent fragment is removed as all children are first removed from parent)
if (!enter) {
Fragment removingParentFragment = this.getRemovingParentFragment();
if (removingParentFragment != null) {
Animator parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimatorHelper.exitFakeResourceId);
if (parentAnimator != null) {
long duration = AnimatorHelper.getTotalDuration(parentAnimator);
return AnimatorHelper.createDummyAnimator(duration);
}
}
}
return super.onCreateAnimator(transit, enter, nextAnim);
}
public Fragment getRemovingParentFragment() {
Fragment parentFragment = this.getParentFragment();
while (parentFragment != null && !parentFragment.isRemoving()) {
@ -33,4 +12,4 @@ public abstract class FragmentBase extends Fragment {
return parentFragment;
}
}
}

View File

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

View File

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

View File

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

View File

@ -643,6 +643,11 @@ export abstract class View extends ViewBase {
* @private
*/
_gestureObservers: any;
/**
* @private
* androidx.fragment.app.FragmentManager
*/
_manager: any;
/**
* @private
*/

View File

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

View File

@ -3,23 +3,29 @@
// 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;
}
const defaultInterpolator = lazy(() => new android.view.animation.AccelerateDecelerateInterpolator());
export const waitingQueue = new Map<number, Set<ExpandedEntry>>();
export const completedEntries = new Map<number, ExpandedEntry>();
let TransitionListener: TransitionListener;
let AnimationListener: android.animation.Animator.AnimatorListener;
interface ExpandedTransitionListener extends androidx.transition.Transition.TransitionListener {
entry: ExpandedEntry;
transition: androidx.transition.Transition;
}
interface ExpandedAnimator extends android.animation.Animator {
@ -27,12 +33,8 @@ interface ExpandedAnimator extends android.animation.Animator {
transitionType?: string;
}
interface ExpandedTransitionListener extends android.transition.Transition.TransitionListener {
entry: ExpandedEntry;
transition: android.transition.Transition;
}
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;
isNestedDefaultTransition: 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<number, Set<ExpandedEntry>>();
export const completedEntries = new Map<number, ExpandedEntry>();
let TransitionListener: TransitionListener;
let AnimationListener: android.animation.Animator.AnimatorListener;
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,67 +92,67 @@ 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);
} 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
const noTransition = new NoTransition(0, null);
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);
}
// 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") {
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) {
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
transition = new FlipTransition(direction, navigationTransition.duration, navigationTransition.curve);
const flipTransition = new FlipTransition(direction, navigationTransition.duration, navigationTransition.curve);
setupNewFragmentCustomTransition(navigationTransition, newEntry, flipTransition);
if (currentFragmentNeedsDifferentAnimation) {
setupCurrentFragmentCustomTransition(navigationTransition, currentEntry, flipTransition);
}
}
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);
}
setupAllAnimation(newEntry, transition);
if (currentFragmentNeedsDifferentAnimation) {
setupExitAndPopEnterAnimation(currentEntry, transition);
}
}
if (currentEntry) {
currentEntry.transitionName = name;
@ -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 = <ExpandedAnimator>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 = <ExpandedAnimator>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 && !(<any>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 = <ExpandedAnimator>transition.createAndroidAnimator(AndroidTransitionType.exit);
exitAnimator.transitionType = AndroidTransitionType.exit;
exitAnimator.entry = entry;
exitAnimator.addListener(listener);
entry.exitAnimator = exitAnimator;
const popEnterAnimator = <ExpandedAnimator>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<BackstackEntry> {
@ -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([(<any>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([(<any>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,60 +380,43 @@ 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);
}
fragment.setReenterTransition(null);
if (traceEnabled()) {
traceWrite(`Cleared Reenter ${reenterTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition);
}
}
if (removeListener) {
entry.exitTransitionListener = null;
}
}
const reenterListener = entry.reenterTransitionListener;
if (reenterListener) {
const reenterTransition = fragment.getReenterTransition();
if (reenterTransition) {
if (removeListener) {
entry.reenterTransitionListener = null;
reenterTransition.removeListener(reenterListener);
}
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);
}
fragment.setReturnTransition(null);
if (traceEnabled()) {
traceWrite(`Cleared Return ${returnTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition);
}
}
if (removeListener) {
entry.returnTransitionListener = null;
}
if (removeListener) {
entry.enterTransitionListener = null;
}
}
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);
const returnListener = entry.returnTransitionListener;
if (returnListener) {
const returnTransition = fragment.getReturnTransition();
if (returnTransition) {
if (removeListener) {
returnTransition.removeListener(returnListener);
}
fragment.setReturnTransition(null);
if (traceEnabled()) {
traceWrite(`Cleared Return ${returnTransition.getClass().getSimpleName()} transition for ${fragment}`, traceCategories.Transition);
}
}
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 = <ExpandedAnimator>transition.createAndroidAnimator(AndroidTransitionType.exit);
exitAnimator.transitionType = AndroidTransitionType.exit;
exitAnimator.entry = entry;
exitAnimator.addListener(listener);
entry.exitAnimator = exitAnimator;
const popEnterAnimator = <ExpandedAnimator>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 = <ExpandedAnimator>transition.createAndroidAnimator(AndroidTransitionType.enter);
enterAnimator.transitionType = AndroidTransitionType.enter;
enterAnimator.entry = entry;
enterAnimator.addListener(listener);
entry.enterAnimator = enterAnimator;
const popExitAnimator = <ExpandedAnimator>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 = <ExpandedAnimator>transition.createAndroidAnimator(AndroidTransitionType.enter);
enterAnimator.transitionType = AndroidTransitionType.enter;
enterAnimator.entry = entry;
enterAnimator.addListener(listener);
entry.defaultEnterAnimator = enterAnimator;
const exitAnimator = <ExpandedAnimator>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);
return animator;
const values = Array.create("float", 2);
values[0] = 0.0;
values[1] = 1.0;
const animator = <android.animation.Animator>android.animation.ObjectAnimator.ofFloat(null, "alpha", values);
animator.setDuration(duration);
objectAnimators[0] = animator;
animatorSet.playTogether(objectAnimators);
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());
}
}

View File

@ -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

View File

@ -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<number, WeakRef<View>>();
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);
let cachedTransitionState = getTransitionState(this._currentEntry);
this._currentEntry = null;
// NavigateCore will eventually call _processNextNavigationEntry again.
this._navigateCore(entry);
this._currentEntry = entry;
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 = <any>entry;
const animatorState = <AnimatorState>{};
const transitionState = <TransitionState>{};
animatorState.enterAnimator = cloneExpandedAnimator(expandedEntry.enterAnimator);
animatorState.exitAnimator = cloneExpandedAnimator(expandedEntry.exitAnimator);
animatorState.popEnterAnimator = cloneExpandedAnimator(expandedEntry.popEnterAnimator);
animatorState.popExitAnimator = cloneExpandedAnimator(expandedEntry.popExitAnimator);
animatorState.transitionName = expandedEntry.transitionName;
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 = <any>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);
public onCreateAnimator(fragment: androidx.fragment.app.Fragment, transit: number, enter: boolean, nextAnim: number, superFunc: Function): android.animation.Animator {
let animator = null;
const entry = <any>this.entry;
// Return enterAnimator only when new (no current entry) nested transition.
if (enter && entry.isNestedDefaultTransition) {
animator = entry.enterAnimator;
entry.isNestedDefaultTransition = false;
}
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;
}
let animator = _onFragmentCreateAnimator(this.entry, fragment, nextAnim, enter);
if (!animator) {
animator = superFunc.call(fragment, transit, enter, nextAnim);
}
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 {

View File

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

View File

@ -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<TabNavigationBase, nu
});
selectedIndexProperty.register(TabNavigationBase);
export const _tabs = new Array<WeakRef<TabNavigationBase>>();
export const itemsProperty = new Property<TabNavigationBase, TabContentItem[]>({
name: "items", valueChanged: (target, oldValue, newValue) => {
target.onItemsChanged(oldValue, newValue);

View File

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

View File

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

View File

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

View File

@ -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.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.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.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.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.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.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.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.Animator>android.animation.ObjectAnimator.ofFloat(null, "alpha", values);
animator.setDuration(fullDuration / 2);
objectAnimators[1] = animator;
break;
}

View File

@ -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,