@nativescript/core
Version:
A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.
641 lines • 27.6 kB
JavaScript
// Types.
import { Application } from '../../application';
import { Observable } from '../../data/observable';
import { Trace } from '../../trace';
import { _stack, FrameBase, NavigationType } from './frame-common';
import { _clearEntry, _clearFragment, _getAnimatedEntries, _reverseTransitions, _setAndroidFragmentTransitions, _updateTransitions, addNativeTransitionListener } from './fragment.transitions';
import { profile } from '../../profiling';
import { android as androidUtils } from '../../utils/native-helper';
import { ensureFragmentClass, fragmentClass } from './fragment';
import { FragmentCallbacksImplementation } from './callbacks/fragment-callbacks';
import { ActivityCallbacksImplementation } from './callbacks/activity-callbacks';
export * from './frame-common';
export { setFragmentClass } from './fragment';
const INTENT_EXTRA = 'com.tns.activity';
const FRAMEID = '_frameId';
const CALLBACKS = '_callbacks';
const ownerSymbol = Symbol('_owner');
let navDepth = -1;
let fragmentId = -1;
export { moduleLoaded } from './callbacks/activity-callbacks';
export let attachStateChangeListener;
function getAttachListener() {
if (!attachStateChangeListener) {
/**
* NOTE: We cannot use NativeClass here because this is used in appComponents in webpack.config
* Whereby it bypasses the decorator transformation, hence pure es5 style written here
*/
const AttachListener = java.lang.Object.extend({
interfaces: [android.view.View.OnAttachStateChangeListener],
init() {
// init must be defined at least
},
onViewAttachedToWindow(view) {
const owner = view[ownerSymbol];
if (owner) {
owner._onAttachedToWindow();
}
},
onViewDetachedFromWindow(view) {
const owner = view[ownerSymbol];
if (owner) {
owner._onDetachedFromWindow();
}
},
});
attachStateChangeListener = new AttachListener();
}
return attachStateChangeListener;
}
export class Frame extends FrameBase {
constructor() {
super();
this._containerViewId = -1;
this._tearDownPending = false;
this._attachedToWindow = false;
this._wasReset = false;
this._android = new AndroidFrame(this);
}
static reloadPage(context) {
const activity = androidUtils.getCurrentActivity();
const callbacks = activity[CALLBACKS];
if (callbacks) {
const rootView = callbacks.getRootView();
// Handle application root module
const isAppRootModuleChanged = context && context.path && context.path.includes(Application.getMainEntry().moduleName) && context.type !== 'style';
// Reset activity content when:
// + Application root module is changed
// + View did not handle the change
// Note:
// The case when neither app root module is changed, neighter livesync is handled on View,
// then changes will not apply until navigate forward to the module.
if (isAppRootModuleChanged || !rootView || !rootView._onLivesync(context)) {
callbacks.resetActivityContent(activity);
}
}
else {
Trace.error(`${activity}[CALLBACKS] is null or undefined`);
}
}
static get defaultAnimatedNavigation() {
return FrameBase.defaultAnimatedNavigation;
}
static set defaultAnimatedNavigation(value) {
FrameBase.defaultAnimatedNavigation = value;
}
static get defaultTransition() {
return FrameBase.defaultTransition;
}
static set defaultTransition(value) {
FrameBase.defaultTransition = value;
}
get containerViewId() {
return this._containerViewId;
}
// @ts-ignore
get android() {
return this._android;
}
get _hasFragments() {
return true;
}
_onAttachedToWindow() {
super._onAttachedToWindow();
// _onAttachedToWindow called from OS again after it was detach
// still happens with androidx.fragment:1.3.2
const activity = androidUtils.getCurrentActivity();
const lifecycleState = activity?.getLifecycle?.()?.getCurrentState() || androidx.lifecycle.Lifecycle.State.CREATED;
if ((this._manager && this._manager.isDestroyed()) || !lifecycleState.isAtLeast(androidx.lifecycle.Lifecycle.State.CREATED)) {
return;
}
this._attachedToWindow = true;
this._wasReset = false;
this._processNextNavigationEntry();
}
_onDetachedFromWindow() {
super._onDetachedFromWindow();
this._attachedToWindow = false;
}
_processNextNavigationEntry() {
// In case activity was destroyed because of back button pressed (e.g. app exit)
// and application is restored from recent apps, current fragment isn't recreated.
// In this case call _navigateCore in order to recreate the current fragment.
// Don't call navigate because it will fire navigation events.
// As JS instances are alive it is already done for the current page.
if (!this.isLoaded || this._executingContext) {
return;
}
// in case the activity is "reset" using resetRootView we must wait for
// the attachedToWindow event to make the first navigation or it will crash
// https://github.com/NativeScript/NativeScript/commit/9dd3e1a8076e5022e411f2f2eeba34aabc68d112
// though we should not do it on app "start"
// or it will create a "flash" to activity background color
if (this._wasReset && !this._attachedToWindow) {
return;
}
const animatedEntries = _getAnimatedEntries(this._android.frameId);
if (animatedEntries) {
// Wait until animations are completed.
if (animatedEntries.size > 0) {
return;
}
}
const manager = this._getFragmentManager();
const entry = this._currentEntry;
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
// is broken when transaction.setCustomAnimations(...) is used in a scenario with:
// 1) forward navigation
// 2) suspend / resume app
// 3) back navigation -- the exiting fragment is erroneously animated with the exit animator from the
// simulated navigation (NoTransition, zero duration animator) and thus the fragment immediately disappears;
// the user only sees the animation of the entering fragment as per its specific enter animation settings.
// NOTE: we are restoring the animation settings in Frame.setCurrent(...) as navigation completes asynchronously
const 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();
}
}
_getChildFragmentManager() {
let backstackEntry;
if (this._executingContext && this._executingContext.entry) {
backstackEntry = this._executingContext.entry;
}
else {
backstackEntry = this._currentEntry;
}
if (backstackEntry && backstackEntry.fragment && backstackEntry.fragment.isAdded()) {
return backstackEntry.fragment.getChildFragmentManager();
}
return null;
}
_onRootViewReset() {
super._onRootViewReset();
// used to handle the "first" navigate differently on first run and on reset
this._wasReset = true;
// call this AFTER the super call to ensure descendants apply their rootview-reset logic first
// i.e. in a scenario with nested frames / frame with tabview let the descendandt cleanup the inner
// fragments first, and then cleanup the parent fragments
this.disposeCurrentFragment();
}
onLoaded() {
if (this._originalBackground) {
this.backgroundColor = null;
this.backgroundColor = this._originalBackground;
this._originalBackground = null;
}
this._frameCreateTimeout = setTimeout(() => {
// there's a bug with nested frames where sometimes the nested fragment is not recreated at all
// so we manually check on loaded event if the fragment is not recreated and recreate it
const currentEntry = this._currentEntry || this._executingContext?.entry;
if (currentEntry) {
if (!currentEntry.fragment) {
const manager = this._getFragmentManager();
const transaction = manager.beginTransaction();
currentEntry.fragment = this.createFragment(currentEntry, currentEntry.fragmentTag);
_updateTransitions(currentEntry);
transaction.replace(this.containerViewId, currentEntry.fragment, currentEntry.fragmentTag);
transaction.commitAllowingStateLoss();
}
}
}, 0);
super.onLoaded();
}
onUnloaded() {
super.onUnloaded();
if (typeof this._frameCreateTimeout === 'number') {
clearTimeout(this._frameCreateTimeout);
this._frameCreateTimeout = null;
}
}
disposeCurrentFragment() {
if (!this._currentEntry || !this._currentEntry.fragment || !this._currentEntry.fragment.isAdded()) {
return;
}
const fragment = this._currentEntry.fragment;
const fragmentManager = fragment.getFragmentManager();
const transaction = fragmentManager.beginTransaction();
const fragmentExitTransition = fragment.getExitTransition();
// Reset animation to its initial state to prevent mirrored effect when restore current fragment transitions
if (fragmentExitTransition && fragmentExitTransition instanceof org.nativescript.widgets.CustomTransition) {
fragmentExitTransition.setResetOnTransitionEnd(true);
}
transaction.remove(fragment);
transaction.commitNowAllowingStateLoss();
}
createFragment(backstackEntry, fragmentTag) {
ensureFragmentClass();
const newFragment = new fragmentClass();
const args = new android.os.Bundle();
args.putInt(FRAMEID, this._android.frameId);
newFragment.setArguments(args);
setFragmentCallbacks(newFragment);
const callbacks = newFragment[CALLBACKS];
callbacks.frame = this;
callbacks.entry = backstackEntry;
// backstackEntry
backstackEntry.fragment = newFragment;
backstackEntry.fragmentTag = fragmentTag;
backstackEntry.navDepth = navDepth;
return newFragment;
}
setCurrent(entry, navigationType) {
const current = this._currentEntry;
const currentEntryChanged = current !== entry;
if (currentEntryChanged) {
this._updateBackstack(entry, navigationType);
// If activity was destroyed we need to destroy fragment and UI
// of current and new entries.
if (this._tearDownPending) {
this._tearDownPending = false;
if (!entry.recreated) {
this._disposeBackstackEntry(entry);
}
if (current && !current.recreated) {
this._disposeBackstackEntry(current);
}
// If we have context activity was recreated. Create new fragment
// and UI for the new current page.
const context = this._context;
if (context && !entry.recreated) {
entry.fragment = this.createFragment(entry, entry.fragmentTag);
entry.resolvedPage._setupUI(context);
}
entry.recreated = false;
if (current) {
current.recreated = false;
}
}
super.setCurrent(entry, navigationType);
// If we had real navigation process queue.
this._processNavigationQueue(entry.resolvedPage);
}
else {
// Otherwise currentPage was recreated so this wasn't real navigation.
// Continue with next item in the queue.
this._processNextNavigationEntry();
}
// restore cached animation settings if we just completed simulated first navigation (no animation)
if (this._cachedTransitionState) {
restoreTransitionState(this._currentEntry, this._cachedTransitionState);
this._cachedTransitionState = null;
}
// restore original fragment transitions if we just completed replace navigation (hmr)
if (navigationType === NavigationType.replace) {
_clearEntry(entry);
const animated = this._getIsAnimatedNavigation(entry.entry);
const navigationTransition = this._getNavigationTransition(entry.entry);
const currentEntry = null;
const newEntry = entry;
const transaction = null;
_setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction);
}
}
onBackPressed() {
if (this.canGoBack()) {
this.goBack();
return true;
}
if (!this.navigationQueueIsEmpty()) {
const manager = this._getFragmentManager();
if (manager) {
manager.executePendingTransactions();
return true;
}
}
return false;
}
// HACK: This @profile decorator creates a circular dependency
// HACK: because the function parameter type is evaluated with 'typeof'
_navigateCore(newEntry) {
// should be (newEntry: BackstackEntry)
super._navigateCore(newEntry);
// set frameId here so that we could use it in fragment.transitions
newEntry.frameId = this._android.frameId;
const activity = this._android.activity;
if (!activity) {
// Activity not associated. In this case we have two execution paths:
// 1. This is the main frame for the application
// 2. This is an inner frame which requires a new Activity
const currentActivity = this._android.currentActivity;
if (currentActivity) {
startActivity(currentActivity, this._android.frameId);
}
return;
}
const manager = this._getFragmentManager();
const clearHistory = newEntry.entry.clearHistory;
const currentEntry = this._currentEntry;
// New Fragment
if (clearHistory) {
navDepth = -1;
}
const isReplace = this._executingContext && this._executingContext.navigationType === NavigationType.replace;
if (!isReplace) {
navDepth++;
}
fragmentId++;
const newFragmentTag = `fragment${fragmentId}[${navDepth}]`;
const newFragment = this.createFragment(newEntry, newFragmentTag);
const transaction = manager.beginTransaction();
const animated = currentEntry ? this._getIsAnimatedNavigation(newEntry.entry) : false;
// NOTE: Don't use transition for the initial navigation (same as on iOS)
// On API 21+ transition won't be triggered unless there was at least one
// layout pass so we will wait forever for transitionCompleted handler...
// https://github.com/NativeScript/NativeScript/issues/4895
let navigationTransition;
if (this._currentEntry) {
navigationTransition = this._getNavigationTransition(newEntry.entry);
}
else {
navigationTransition = null;
}
const isNestedDefaultTransition = !currentEntry;
_setAndroidFragmentTransitions(animated, navigationTransition, currentEntry, newEntry, this._android.frameId, transaction, isNestedDefaultTransition);
if (currentEntry && animated && !navigationTransition) {
//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);
navigationTransition?.instance?.androidFragmentTransactionCallback?.(transaction, currentEntry, newEntry);
transaction.commitAllowingStateLoss();
}
_goBackCore(backstackEntry) {
super._goBackCore(backstackEntry);
navDepth = backstackEntry.navDepth;
const manager = this._getFragmentManager();
const transaction = manager.beginTransaction();
if (!backstackEntry.fragment) {
// Happens on newer API levels. On older all fragments
// are recreated once activity is created.
// This entry fragment was destroyed by app suspend.
// We need to recreate its animations and then reverse it.
backstackEntry.fragment = this.createFragment(backstackEntry, backstackEntry.fragmentTag);
_updateTransitions(backstackEntry);
}
_reverseTransitions(backstackEntry, this._currentEntry);
transaction.replace(this.containerViewId, backstackEntry.fragment, backstackEntry.fragmentTag);
backstackEntry.transition?.androidFragmentTransactionCallback?.(transaction, this._currentEntry, backstackEntry);
transaction.commitAllowingStateLoss();
}
_removeEntry(removed) {
super._removeEntry(removed);
if (removed.fragment) {
_clearEntry(removed);
}
removed.fragment = null;
removed.viewSavedState = null;
}
_disposeBackstackEntry(entry) {
if (entry.fragment) {
_clearFragment(entry);
}
entry.recreated = false;
entry.fragment = null;
super._disposeBackstackEntry(entry);
}
createNativeView() {
// Create native view with available _currentEntry occur in Don't Keep Activities
// scenario when Activity is recreated on app suspend/resume. Push frame back in frame stack
// since it was removed in disposeNativeView() method.
if (this._currentEntry) {
this._pushInFrameStack();
}
return new org.nativescript.widgets.ContentLayout(this._context);
}
initNativeView() {
super.initNativeView();
const listener = getAttachListener();
this.nativeViewProtected.addOnAttachStateChangeListener(listener);
this.nativeViewProtected[ownerSymbol] = this;
this._android.rootViewGroup = this.nativeViewProtected;
if (this._containerViewId < 0) {
this._containerViewId = android.view.View.generateViewId();
}
this._android.rootViewGroup.setId(this._containerViewId);
}
disposeNativeView() {
const listener = getAttachListener();
this.nativeViewProtected.removeOnAttachStateChangeListener(listener);
this.nativeViewProtected[ownerSymbol] = null;
this._tearDownPending = !!this._executingContext;
const current = this._currentEntry;
const executingEntry = this._executingContext ? this._executingContext.entry : null;
this.backStack.forEach((entry) => {
// Don't destroy current and executing entries or UI will look blank.
// We will do it in setCurrent.
if (entry !== executingEntry) {
this._disposeBackstackEntry(entry);
}
});
if (current && !executingEntry) {
this._disposeBackstackEntry(current);
}
this._android.rootViewGroup = null;
this._removeFromFrameStack();
super.disposeNativeView();
}
_popFromFrameStack() {
if (!this._isInFrameStack) {
return;
}
super._popFromFrameStack();
}
_getNavBarVisible(page) {
switch (this.actionBarVisibility) {
case 'never':
return false;
case 'always':
return true;
default:
if (page.actionBarHidden !== undefined) {
return !page.actionBarHidden;
}
if (this._android && this._android.showActionBar !== undefined) {
return this._android.showActionBar;
}
return true;
}
}
_saveFragmentsState() {
// We save only fragments in backstack.
// Current fragment is saved by FragmentManager.
this.backStack.forEach((entry) => {
const view = entry.resolvedPage.nativeViewProtected;
if (!entry.viewSavedState && view) {
const viewState = new android.util.SparseArray();
view.saveHierarchyState(viewState);
entry.viewSavedState = viewState;
}
});
}
}
__decorate([
profile,
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", void 0)
], Frame.prototype, "_navigateCore", null);
export function reloadPage(context) {
console.warn('reloadPage() is deprecated. Use Frame.reloadPage() instead.');
return Frame.reloadPage(context);
}
// attach on global, so it can be overwritten in NativeScript Angular
global.__onLiveSyncCore = Frame.reloadPage;
function cloneExpandedTransitionListener(expandedTransitionListener) {
if (!expandedTransitionListener) {
return null;
}
const cloneTransition = expandedTransitionListener.transition.clone();
return addNativeTransitionListener(expandedTransitionListener.entry, cloneTransition);
}
function getTransitionState(entry) {
const expandedEntry = entry;
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 transitionState;
}
function restoreTransitionState(entry, snapshot) {
const expandedEntry = entry;
if (snapshot.enterTransitionListener) {
expandedEntry.enterTransitionListener = snapshot.enterTransitionListener;
}
if (snapshot.exitTransitionListener) {
expandedEntry.exitTransitionListener = snapshot.exitTransitionListener;
}
if (snapshot.reenterTransitionListener) {
expandedEntry.reenterTransitionListener = snapshot.reenterTransitionListener;
}
if (snapshot.returnTransitionListener) {
expandedEntry.returnTransitionListener = snapshot.returnTransitionListener;
}
expandedEntry.transitionName = snapshot.transitionName;
}
let framesCounter = 0;
const framesCache = new Array();
class AndroidFrame extends Observable {
constructor(owner) {
super();
this._showActionBar = true;
this._owner = owner;
this.frameId = framesCounter++;
framesCache.push(new WeakRef(this));
}
get showActionBar() {
return this._showActionBar;
}
set showActionBar(value) {
if (this._showActionBar !== value) {
this._showActionBar = value;
if (this.owner.currentPage) {
this.owner.currentPage.actionBar.update();
}
}
}
get activity() {
const activity = this.owner._context;
if (activity) {
return activity;
}
// traverse the parent chain for an ancestor Frame
let currView = this._owner.parent;
while (currView) {
if (currView instanceof Frame) {
return currView.android.activity;
}
currView = currView.parent;
}
return undefined;
}
get actionBar() {
const activity = this.currentActivity;
if (!activity) {
return undefined;
}
const bar = activity.getActionBar();
if (!bar) {
return undefined;
}
return bar;
}
get currentActivity() {
let activity = this.activity;
if (activity) {
return activity;
}
const frames = _stack();
for (let length = frames.length, i = length - 1; i >= 0; i--) {
activity = frames[i].android.activity;
if (activity) {
return activity;
}
}
return undefined;
}
get owner() {
return this._owner;
}
canGoBack() {
if (!this.activity) {
return false;
}
// can go back only if it is not the main one.
return this.activity.getIntent().getAction() !== android.content.Intent.ACTION_MAIN;
}
fragmentForPage(entry) {
const tag = entry && entry.fragmentTag;
if (tag) {
return this.owner._getFragmentManager().findFragmentByTag(tag);
}
return undefined;
}
}
function startActivity(activity, frameId) {
// TODO: Implicitly, we will open the same activity type as the current one
const intent = new android.content.Intent(activity, activity.getClass());
intent.setAction(android.content.Intent.ACTION_DEFAULT);
intent.putExtra(INTENT_EXTRA, frameId);
// TODO: Put the navigation context (if any) in the intent
activity.startActivity(intent);
}
export function getFrameByNumberId(frameId) {
// Find the frame for this activity.
for (let i = 0; i < framesCache.length; i++) {
const aliveFrame = framesCache[i].get();
if (aliveFrame && aliveFrame.frameId === frameId) {
return aliveFrame.owner;
}
}
return null;
}
export function setActivityCallbacks(activity) {
activity[CALLBACKS] = new ActivityCallbacksImplementation();
}
export function setFragmentCallbacks(fragment) {
fragment[CALLBACKS] = new FragmentCallbacksImplementation();
}
//# sourceMappingURL=index.android.js.map