feat(core): ability to embed into platform host projects (#10465)

This commit is contained in:
Vladimir Mutafov
2024-07-02 02:47:33 +03:00
committed by GitHub
parent 9fd361c2e6
commit 779d79285d
19 changed files with 885 additions and 672 deletions

View File

@ -9,4 +9,5 @@ dist
css/parser.js* css/parser.js*
css/system-classes.js* css/system-classes.js*
!css-value/**/*.* !css-value/**/*.*
!fetch/**/*.* !fetch/**/*.*
/coverage

View File

@ -1,5 +1,6 @@
import { profile } from '../profiling'; import { profile } from '../profiling';
import { View } from '../ui'; import { View } from '../ui/core/view';
import { isEmbedded } from '../ui/embedding';
import { AndroidActivityCallbacks, NavigationEntry } from '../ui/frame/frame-common'; import { AndroidActivityCallbacks, NavigationEntry } from '../ui/frame/frame-common';
import type { AndroidApplication as IAndroidApplication } from './application'; import type { AndroidApplication as IAndroidApplication } from './application';
import { ApplicationCommon } from './application-common'; import { ApplicationCommon } from './application-common';
@ -10,6 +11,12 @@ declare namespace com {
class NativeScriptApplication extends android.app.Application { class NativeScriptApplication extends android.app.Application {
static getInstance(): NativeScriptApplication; static getInstance(): NativeScriptApplication;
} }
namespace embedding {
class ApplicationHolder {
static getInstance(): android.app.Application;
}
}
} }
} }
@ -358,6 +365,10 @@ export class AndroidApplication extends ApplicationCommon implements IAndroidApp
nativeApp = com.tns.NativeScriptApplication.getInstance(); nativeApp = com.tns.NativeScriptApplication.getInstance();
} }
if (!nativeApp && isEmbedded()) {
nativeApp = com.tns.embedding.ApplicationHolder.getInstance();
}
// the getInstance might return null if com.tns.NativeScriptApplication exists but is not the starting app type // the getInstance might return null if com.tns.NativeScriptApplication exists but is not the starting app type
if (!nativeApp) { if (!nativeApp) {
// TODO: Should we handle the case when a custom application type is provided and the user has not explicitly initialized the application module? // TODO: Should we handle the case when a custom application type is provided and the user has not explicitly initialized the application module?

View File

@ -1,5 +1,6 @@
import { profile } from '../profiling'; import { profile } from '../profiling';
import { View } from '../ui'; import { View } from '../ui/core/view';
import { isEmbedded } from '../ui/embedding';
import { IOSHelper } from '../ui/core/view/view-helper'; import { IOSHelper } from '../ui/core/view/view-helper';
import { NavigationEntry } from '../ui/frame/frame-interfaces'; import { NavigationEntry } from '../ui/frame/frame-interfaces';
import * as Utils from '../utils'; import * as Utils from '../utils';
@ -367,7 +368,7 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
}); });
if (this._window) { if (this._window) {
if (root !== null && !NativeScriptEmbedder.sharedInstance().delegate) { if (root !== null && !isEmbedded()) {
this.setWindowContent(root); this.setWindowContent(root);
} }
} else { } else {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nativescript/core", "name": "@nativescript/core",
"version": "8.8.0", "version": "8.8.0-embed.1",
"description": "A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.", "description": "A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.",
"main": "index", "main": "index",
"types": "index.d.ts", "types": "index.d.ts",

View File

@ -0,0 +1,26 @@
import type { View } from '../../ui/core/view';
declare namespace org {
namespace nativescript {
class Bootstrap {
static isEmbeddedNativeScript: boolean;
}
}
}
export function isEmbedded(): boolean {
return org.nativescript?.Bootstrap?.isEmbeddedNativeScript;
}
let embeddedView: View | undefined;
export function setEmbeddedView(view: View | undefined): void {
embeddedView = view;
}
export function getEmbeddedView(): View {
if (!embeddedView) {
throw new Error("{N} Core: Fragment content view not set or set to 'undefined'");
}
return embeddedView;
}

10
packages/core/ui/embedding/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import type { View } from '../../ui/core/view';
/**
* Whether the app is embedded into a host project or standalone project
*/
export function isEmbedded(): boolean;
export function setEmbeddedView(view: View | undefined): void;
export function getEmbeddedView(): View;

View File

@ -0,0 +1,3 @@
export function isEmbedded(): boolean {
return !!NativeScriptEmbedder.sharedInstance().delegate;
}

View File

@ -1,62 +1,135 @@
import '../../globals'; import '../../globals';
import { setActivityCallbacks, AndroidActivityCallbacks } from '.'; import { setActivityCallbacks } from '.';
import { Application } from '../../application'; import { Application } from '../../application';
import { isEmbedded } from '../embedding';
/** const EMPTY_FN = () => {};
* NOTE: We cannot use NativeClass here because this is used in appComponents in webpack.config declare const com: any;
* Whereby it bypasses the decorator transformation, hence pure es5 style written here
*/
const superProto = androidx.appcompat.app.AppCompatActivity.prototype;
(<any>androidx.appcompat.app.AppCompatActivity).extend('com.tns.NativeScriptActivity', {
init() {
// init must at least be defined
},
onCreate(savedInstanceState: android.os.Bundle): void {
Application.android.init(this.getApplication());
// Set isNativeScriptActivity in onCreate. if (!isEmbedded()) {
// The JS constructor might not be called because the activity is created from Android. /**
this.isNativeScriptActivity = true; * NOTE: We cannot use NativeClass here because this is used in appComponents in webpack.config
if (!this._callbacks) { * Whereby it bypasses the decorator transformation, hence pure es5 style written here
setActivityCallbacks(this); */
} const superProto = androidx.appcompat.app.AppCompatActivity.prototype;
(<any>androidx.appcompat.app.AppCompatActivity).extend('com.tns.NativeScriptActivity', {
init() {
// init must at least be defined
},
onCreate(savedInstanceState: android.os.Bundle): void {
Application.android.init(this.getApplication());
this._callbacks.onCreate(this, savedInstanceState, this.getIntent(), superProto.onCreate); // Set isNativeScriptActivity in onCreate.
}, // The JS constructor might not be called because the activity is created from Android.
this.isNativeScriptActivity = true;
if (!this._callbacks) {
setActivityCallbacks(this);
}
onNewIntent(intent: android.content.Intent): void { this._callbacks.onCreate(this, savedInstanceState, this.getIntent(), superProto.onCreate);
this._callbacks.onNewIntent(this, intent, superProto.setIntent, superProto.onNewIntent); },
},
onSaveInstanceState(outState: android.os.Bundle): void { onNewIntent(intent: android.content.Intent): void {
this._callbacks.onSaveInstanceState(this, outState, superProto.onSaveInstanceState); this._callbacks.onNewIntent(this, intent, superProto.setIntent, superProto.onNewIntent);
}, },
onStart(): void { onSaveInstanceState(outState: android.os.Bundle): void {
this._callbacks.onStart(this, superProto.onStart); this._callbacks.onSaveInstanceState(this, outState, superProto.onSaveInstanceState);
}, },
onStop(): void { onStart(): void {
this._callbacks.onStop(this, superProto.onStop); this._callbacks.onStart(this, superProto.onStart);
}, },
onDestroy(): void { onStop(): void {
this._callbacks.onDestroy(this, superProto.onDestroy); this._callbacks.onStop(this, superProto.onStop);
}, },
onPostResume(): void { onDestroy(): void {
this._callbacks.onPostResume(this, superProto.onPostResume); this._callbacks.onDestroy(this, superProto.onDestroy);
}, },
onBackPressed(): void { onPostResume(): void {
this._callbacks.onBackPressed(this, superProto.onBackPressed); this._callbacks.onPostResume(this, superProto.onPostResume);
}, },
onRequestPermissionsResult(requestCode: number, permissions: Array<string>, grantResults: Array<number>): void { onBackPressed(): void {
this._callbacks.onRequestPermissionsResult(this, requestCode, permissions, grantResults, undefined /*TODO: Enable if needed*/); this._callbacks.onBackPressed(this, superProto.onBackPressed);
}, },
onActivityResult(requestCode: number, resultCode: number, data: android.content.Intent): void { onRequestPermissionsResult(requestCode: number, permissions: Array<string>, grantResults: Array<number>): void {
this._callbacks.onActivityResult(this, requestCode, resultCode, data, superProto.onActivityResult); this._callbacks.onRequestPermissionsResult(this, requestCode, permissions, grantResults, undefined /*TODO: Enable if needed*/);
}, },
});
onActivityResult(requestCode: number, resultCode: number, data: android.content.Intent): void {
this._callbacks.onActivityResult(this, requestCode, resultCode, data, superProto.onActivityResult);
},
});
} else {
const Callbacks = com.tns.embedding.EmbeddableActivityCallbacks.extend({
init() {
// init must at least be defined
},
onCreate(savedInstanceState: android.os.Bundle): void {
const activity = this.getActivity();
Application.android.init(activity.getApplication());
// Set isNativeScriptActivity in onCreate.
// The JS constructor might not be called because the activity is created from Android.
activity.isNativeScriptActivity = true;
if (!activity._callbacks) {
setActivityCallbacks(activity);
}
activity._callbacks.onCreate(activity, savedInstanceState, activity.getIntent(), EMPTY_FN);
},
onNewIntent(intent: android.content.Intent): void {
const activity = this.getActivity();
activity._callbacks.onNewIntent(activity, intent, EMPTY_FN, EMPTY_FN);
},
onSaveInstanceState(outState: android.os.Bundle): void {
const activity = this.getActivity();
activity._callbacks.onSaveInstanceState(activity, outState, EMPTY_FN);
},
onStart(): void {
const activity = this.getActivity();
activity._callbacks.onStart(activity, EMPTY_FN);
},
onStop(): void {
const activity = this.getActivity();
activity._callbacks.onStop(activity, EMPTY_FN);
},
onDestroy(): void {
const activity = this.getActivity();
activity._callbacks.onDestroy(activity, EMPTY_FN);
},
onPostResume(): void {
const activity = this.getActivity();
activity._callbacks.onPostResume(activity, EMPTY_FN);
},
onBackPressed(): void {
const activity = this.getActivity();
activity._callbacks.onBackPressed(activity, EMPTY_FN);
},
onRequestPermissionsResult(requestCode: number, permissions: Array<string>, grantResults: Array<number>): void {
const activity = this.getActivity();
activity._callbacks.onRequestPermissionsResult(activity, requestCode, permissions, grantResults, undefined /*TODO: Enable if needed*/);
},
onActivityResult(requestCode: number, resultCode: number, data: android.content.Intent): void {
const activity = this.getActivity();
activity._callbacks.onActivityResult(activity, requestCode, resultCode, data, EMPTY_FN);
},
});
com.tns.embedding.CallbacksStore.setActivityCallbacks(new Callbacks());
}

View File

@ -0,0 +1,303 @@
import { AndroidActivityCallbacks, Frame } from '..';
import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData, Application } from '../../../application';
import { Trace } from '../../../trace';
import { View } from '../../core/view';
import { _clearEntry, _clearFragment, _getAnimatedEntries, _reverseTransitions, _setAndroidFragmentTransitions, _updateTransitions } from '../fragment.transitions';
import { profile } from '../../../profiling';
import { isEmbedded, setEmbeddedView } from '../../embedding';
const activityRootViewsMap = new Map<number, WeakRef<View>>();
const INTENT_EXTRA = 'com.tns.activity';
const ROOT_VIEW_ID_EXTRA = 'com.tns.activity.rootViewId';
export let moduleLoaded: boolean;
export class ActivityCallbacksImplementation implements AndroidActivityCallbacks {
private _rootView: View;
public getRootView(): View {
return this._rootView;
}
@profile
public onCreate(activity: androidx.appcompat.app.AppCompatActivity, savedInstanceState: android.os.Bundle, intentOrSuperFunc: android.content.Intent | Function, superFunc?: Function): void {
if (Trace.isEnabled()) {
Trace.write(`Activity.onCreate(${savedInstanceState})`, Trace.categories.NativeLifecycle);
}
const intent: android.content.Intent = superFunc ? <android.content.Intent>intentOrSuperFunc : undefined;
if (!superFunc) {
console.log('AndroidActivityCallbacks.onCreate(activity: any, savedInstanceState: any, superFunc: Function) ' + 'is deprecated. Use AndroidActivityCallbacks.onCreate(activity: any, savedInstanceState: any, intent: any, superFunc: Function) instead.');
superFunc = <Function>intentOrSuperFunc;
}
// If there is savedInstanceState this call will recreate all fragments that were previously in the navigation.
// We take care of associating them with a Page from our backstack in the onAttachFragment callback.
// If there is savedInstanceState and moduleLoaded is false we are restarted but process was killed.
// For now we treat it like first run (e.g. we are not passing savedInstanceState so no fragments are being restored).
// When we add support for application save/load state - revise this logic.
const isRestart = !!savedInstanceState && moduleLoaded;
superFunc.call(activity, isRestart ? savedInstanceState : null);
// Try to get the rootViewId form the saved state in case the activity
// was destroyed and we are now recreating it.
if (savedInstanceState) {
const rootViewId = savedInstanceState.getInt(ROOT_VIEW_ID_EXTRA, -1);
if (rootViewId !== -1 && activityRootViewsMap.has(rootViewId)) {
this._rootView = activityRootViewsMap.get(rootViewId)?.get();
}
}
if (intent && intent.getAction()) {
Application.android.notify(<AndroidActivityNewIntentEventData>{
eventName: Application.AndroidApplication.activityNewIntentEvent,
object: Application.android,
activity,
intent,
});
}
this.setActivityContent(activity, savedInstanceState, true);
moduleLoaded = true;
}
@profile
public onSaveInstanceState(activity: androidx.appcompat.app.AppCompatActivity, outState: android.os.Bundle, superFunc: Function): void {
superFunc.call(activity, outState);
const rootView = this._rootView;
if (rootView instanceof Frame) {
outState.putInt(INTENT_EXTRA, rootView.android.frameId);
rootView._saveFragmentsState();
}
if (rootView) {
outState.putInt(ROOT_VIEW_ID_EXTRA, rootView._domId);
}
}
@profile
public onNewIntent(activity: androidx.appcompat.app.AppCompatActivity, intent: android.content.Intent, superSetIntentFunc: Function, superFunc: Function): void {
superFunc.call(activity, intent);
superSetIntentFunc.call(activity, intent);
Application.android.notify(<AndroidActivityNewIntentEventData>{
eventName: Application.AndroidApplication.activityNewIntentEvent,
object: Application.android,
activity,
intent,
});
}
@profile
public onStart(activity: any, superFunc: Function): void {
superFunc.call(activity);
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onStart();', Trace.categories.NativeLifecycle);
}
const rootView = this._rootView;
if (rootView && !rootView.isLoaded && !isEmbedded()) {
rootView.callLoaded();
}
}
@profile
public onStop(activity: any, superFunc: Function): void {
superFunc.call(activity);
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onStop();', Trace.categories.NativeLifecycle);
}
const rootView = this._rootView;
if (rootView && rootView.isLoaded && !isEmbedded()) {
rootView.callUnloaded();
}
}
@profile
public onPostResume(activity: any, superFunc: Function): void {
superFunc.call(activity);
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onPostResume();', Trace.categories.NativeLifecycle);
}
// NOTE: activity.onPostResume() is called when activity resume is complete and we can
// safely raise the application resume event;
// onActivityResumed(...) lifecycle callback registered in application is called too early
// and raising the application resume event there causes issues like
// https://github.com/NativeScript/NativeScript/issues/6708
if ((<any>activity).isNativeScriptActivity) {
Application.setSuspended(false, {
// todo: deprecate in favor of using event.activity instead.
android: activity,
activity,
});
}
}
@profile
public onDestroy(activity: any, superFunc: Function): void {
try {
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onDestroy();', Trace.categories.NativeLifecycle);
}
const rootView = this._rootView;
if (rootView) {
rootView._tearDownUI(true);
}
// this may happen when the user changes the system theme
// In such case, isFinishing() is false (and isChangingConfigurations is true), and the app will start again (onCreate) with a savedInstanceState
// as a result, launchEvent will never be called
// possible alternative: always fire launchEvent and exitEvent, but pass extra flags to make it clear what kind of launch/destroy is happening
if (activity.isFinishing()) {
const exitArgs = {
eventName: Application.exitEvent,
object: Application.android,
android: activity,
};
Application.notify(exitArgs);
}
} finally {
superFunc.call(activity);
}
}
@profile
public onBackPressed(activity: any, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onBackPressed;', Trace.categories.NativeLifecycle);
}
const args = <AndroidActivityBackPressedEventData>{
eventName: 'activityBackPressed',
object: Application,
android: Application.android,
activity: activity,
cancel: false,
};
Application.android.notify(args);
if (args.cancel) {
return;
}
const view = this._rootView;
let callSuper = false;
const viewArgs = <AndroidActivityBackPressedEventData>{
eventName: 'activityBackPressed',
object: view,
activity: activity,
cancel: false,
};
view.notify(viewArgs);
// In the case of Frame, use this callback only if it was overridden, since the original will cause navigation issues
if (!viewArgs.cancel && (view.onBackPressed === Frame.prototype.onBackPressed || !view.onBackPressed())) {
callSuper = view instanceof Frame ? !Frame.goBack() : true;
}
if (callSuper) {
superFunc.call(activity);
}
}
@profile
public onRequestPermissionsResult(activity: any, requestCode: number, permissions: Array<string>, grantResults: Array<number>, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onRequestPermissionsResult;', Trace.categories.NativeLifecycle);
}
Application.android.notify(<AndroidActivityRequestPermissionsEventData>{
eventName: 'activityRequestPermissions',
object: Application,
android: Application.android,
activity: activity,
requestCode: requestCode,
permissions: permissions,
grantResults: grantResults,
});
}
@profile
public onActivityResult(activity: any, requestCode: number, resultCode: number, data: android.content.Intent, superFunc: Function): void {
superFunc.call(activity, requestCode, resultCode, data);
if (Trace.isEnabled()) {
Trace.write(`NativeScriptActivity.onActivityResult(${requestCode}, ${resultCode}, ${data})`, Trace.categories.NativeLifecycle);
}
Application.android.notify(<AndroidActivityResultEventData>{
eventName: 'activityResult',
object: Application,
android: Application.android,
activity: activity,
requestCode: requestCode,
resultCode: resultCode,
intent: data,
});
}
public resetActivityContent(activity: androidx.appcompat.app.AppCompatActivity): void {
if (this._rootView) {
const manager = this._rootView._getFragmentManager();
manager.executePendingTransactions();
this._rootView._onRootViewReset();
}
// Delete previously cached root view in order to recreate it.
this._rootView = null;
this.setActivityContent(activity, null, false);
this._rootView.callLoaded();
}
// Paths that go trough this method:
// 1. Application initial start - there is no rootView in callbacks.
// 2. Application revived after Activity is destroyed. this._rootView should have been restored by id in onCreate.
// 3. Livesync if rootView has no custom _onLivesync. this._rootView should have been cleared upfront. Launch event should not fired
// 4. resetRootView method. this._rootView should have been cleared upfront. Launch event should not fired
private setActivityContent(activity: androidx.appcompat.app.AppCompatActivity, savedInstanceState: android.os.Bundle, fireLaunchEvent: boolean): void {
let rootView = this._rootView;
if (Trace.isEnabled()) {
Trace.write(`Frame.setActivityContent rootView: ${rootView} shouldCreateRootFrame: false fireLaunchEvent: ${fireLaunchEvent}`, Trace.categories.NativeLifecycle);
}
const intent = activity.getIntent();
rootView = Application.createRootView(rootView, fireLaunchEvent, {
// todo: deprecate in favor of args.intent?
android: intent,
intent,
savedInstanceState,
});
if (!rootView) {
// no root view created
return;
}
activityRootViewsMap.set(rootView._domId, new WeakRef(rootView));
// setup view as styleScopeHost
rootView._setupAsRootView(activity);
if (isEmbedded()) {
setEmbeddedView(rootView);
} else {
activity.setContentView(rootView.nativeViewProtected, new org.nativescript.widgets.CommonLayoutParams());
}
this._rootView = rootView;
// sets root classes once rootView is ready...
Application.initRootView(rootView);
}
}

View File

@ -0,0 +1,310 @@
import { profile } from '../../../profiling';
import { AndroidFragmentCallbacks, BackstackEntry, Frame, getFrameByNumberId } from '..';
import { Trace } from '../../../trace';
import { Application } from '../../../application';
import { Color } from '../../../color';
import { _updateTransitions } from '../fragment.transitions';
import { Page } from '../../page';
const FRAMEID = '_frameId';
const CALLBACKS = '_callbacks';
export 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 {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onHiddenChanged(${hidden})`, Trace.categories.NativeLifecycle);
}
superFunc.call(fragment, hidden);
}
@profile
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;
}
return animator || superFunc.call(fragment, transit, enter, nextAnim);
}
@profile
public onCreate(fragment: androidx.fragment.app.Fragment, savedInstanceState: android.os.Bundle, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onCreate(${savedInstanceState})`, Trace.categories.NativeLifecycle);
}
superFunc.call(fragment, savedInstanceState);
// There is no entry set to the fragment, so this must be destroyed fragment that was recreated by Android.
// We should find its corresponding page in our backstack and set it manually.
if (!this.entry) {
const args = fragment.getArguments();
const frameId = args.getInt(FRAMEID);
const frame = getFrameByNumberId(frameId);
if (!frame) {
throw new Error(`Cannot find Frame for ${fragment}`);
}
findPageForFragment(fragment, frame);
}
}
@profile
public onCreateView(fragment: androidx.fragment.app.Fragment, inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle, superFunc: Function): android.view.View {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onCreateView(inflater, container, ${savedInstanceState})`, Trace.categories.NativeLifecycle);
}
const entry = this.entry;
if (!entry) {
Trace.error(`${fragment}.onCreateView: entry is null or undefined`);
return null;
}
const page = entry.resolvedPage;
if (!page) {
Trace.error(`${fragment}.onCreateView: entry has no resolvedPage`);
return null;
}
const frame = this.frame;
if (!frame) {
Trace.error(`${fragment}.onCreateView: this.frame is null or undefined`);
return null;
}
frame._resolvedPage = page;
if (page.parent === frame) {
frame._inheritStyles(page);
// If we are navigating to a page that was destroyed
// reinitialize its UI.
if (!page._context) {
const context = (container && container.getContext()) || (inflater && inflater.getContext());
page._setupUI(context);
}
if (frame.isLoaded && !page.isLoaded) {
page.callLoaded();
}
} else {
if (!page.parent) {
if (!frame._styleScope) {
// Make sure page will have styleScope even if parents don't.
page._updateStyleScope();
}
frame._addView(page);
} else {
throw new Error('Page is already shown on another frame.');
}
}
const savedState = entry.viewSavedState;
if (savedState) {
(<android.view.View>page.nativeViewProtected).restoreHierarchyState(savedState);
entry.viewSavedState = null;
}
// fixes 'java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first'.
// on app resume in nested frame scenarios with support library version greater than 26.0.0
// HACK: this whole code block shouldn't be necessary as the native view is supposedly removed from its parent
// right after onDestroyView(...) is called but for some reason the fragment view (page) still thinks it has a
// parent while its supposed parent believes it properly removed its children; in order to "force" the child to
// lose its parent we temporarily add it to the parent, and then remove it (addViewInLayout doesn't trigger layout pass)
const nativeView = page.nativeViewProtected;
if (nativeView != null) {
const parentView = nativeView.getParent();
if (parentView instanceof android.view.ViewGroup) {
if (parentView.getChildCount() === 0) {
parentView.addViewInLayout(nativeView, -1, new org.nativescript.widgets.CommonLayoutParams());
}
parentView.removeAllViews();
}
}
return page.nativeViewProtected;
}
@profile
public onSaveInstanceState(fragment: androidx.fragment.app.Fragment, outState: android.os.Bundle, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onSaveInstanceState(${outState})`, Trace.categories.NativeLifecycle);
}
superFunc.call(fragment, outState);
}
@profile
public onDestroyView(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void {
try {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onDestroyView()`, Trace.categories.NativeLifecycle);
}
const hasRemovingParent = fragment.getRemovingParentFragment();
if (hasRemovingParent) {
const nativeFrameView = this.frame.nativeViewProtected;
if (nativeFrameView) {
const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(Application.android.context.getResources(), this.backgroundBitmap);
this.frame._originalBackground = this.frame.backgroundColor || new Color('White');
nativeFrameView.setBackgroundDrawable(bitmapDrawable);
this.backgroundBitmap = null;
}
}
} finally {
superFunc.call(fragment);
}
}
@profile
public onDestroy(fragment: androidx.fragment.app.Fragment, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onDestroy()`, Trace.categories.NativeLifecycle);
}
superFunc.call(fragment);
const entry = this.entry;
if (!entry) {
Trace.error(`${fragment}.onDestroy: entry is null or undefined`);
return null;
}
// [nested frames / fragments] see https://github.com/NativeScript/NativeScript/issues/6629
// retaining reference to a destroyed fragment here somehow causes a cryptic
// "IllegalStateException: Failure saving state: active fragment has cleared index: -1"
// in a specific mixed parent / nested frame navigation scenario
entry.fragment = null;
const page = entry.resolvedPage;
if (!page) {
// todo: check why this happens when using shared element transition!!!
// commented out the Trace.error to prevent a crash (the app will still work interestingly)
console.log(`${fragment}.onDestroy: entry has no resolvedPage`);
// Trace.error(`${fragment}.onDestroy: entry has no resolvedPage`);
return null;
}
}
@profile
public onPause(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void {
try {
// 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);
}
} finally {
superFunc.call(fragment);
}
}
@profile
public onResume(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void {
const frame = this.entry.resolvedPage.frame;
// on some cases during the first navigation on nested frames the animation doesn't trigger
// we depend on the animation (even None animation) to set the entry as the current entry
// animation should start between start and resume, so if we have an executing navigation here it probably means the animation was skipped
// so we manually set the entry
// also, to be compatible with fragments 1.2.x we need this setTimeout as animations haven't run on onResume yet
const weakRef = new WeakRef(this);
setTimeout(() => {
const owner = weakRef.get();
if (!owner) {
return;
}
if (frame._executingContext && !(<any>owner.entry).isAnimationRunning) {
frame.setCurrent(owner.entry, frame._executingContext.navigationType);
}
}, 0);
superFunc.call(fragment);
}
@profile
public onStop(fragment: androidx.fragment.app.Fragment, superFunc: Function): void {
superFunc.call(fragment);
}
@profile
public toStringOverride(fragment: androidx.fragment.app.Fragment, superFunc: Function): string {
const entry = this.entry;
if (entry) {
return `${entry.fragmentTag}<${entry.resolvedPage}>`;
} else {
return 'NO ENTRY, ' + superFunc.call(fragment);
}
}
private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap {
// Don't try to create bitmaps with no dimensions as this causes a crash
// This might happen when showing and closing dialogs fast.
if (!(view && view.getWidth() > 0 && view.getHeight() > 0)) {
return undefined;
}
// 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 drawCache = view.getDrawingCache();
// const bitmap = android.graphics.Bitmap.createBitmap(drawCache);
// view.setDrawingCacheEnabled(false);
return org.nativescript.widgets.Utils.getBitmapFromView(view);
}
}
function findPageForFragment(fragment: androidx.fragment.app.Fragment, frame: Frame) {
const fragmentTag = fragment.getTag();
if (Trace.isEnabled()) {
Trace.write(`Finding page for ${fragmentTag}.`, Trace.categories.NativeLifecycle);
}
let entry: BackstackEntry;
const current = frame._currentEntry;
const executingContext = frame._executingContext;
if (current && current.fragmentTag === fragmentTag) {
entry = current;
} else if (executingContext && executingContext.entry && executingContext.entry.fragmentTag === fragmentTag) {
entry = executingContext.entry;
}
let page: Page;
if (entry) {
entry.recreated = true;
page = entry.resolvedPage;
}
if (page) {
const callbacks: FragmentCallbacksImplementation = fragment[CALLBACKS];
callbacks.frame = frame;
callbacks.entry = entry;
entry.fragment = fragment;
_updateTransitions(entry);
} else {
throw new Error(`Could not find a page for ${fragmentTag}.`);
}
}

View File

@ -1,4 +1,7 @@
import { AndroidFragmentCallbacks, setFragmentCallbacks, setFragmentClass } from '.'; import { isEmbedded, getEmbeddedView } from '../embedding';
import { setFragmentCallbacks } from '.';
declare const com: any;
const superProto = org.nativescript.widgets.FragmentBase.prototype; const superProto = org.nativescript.widgets.FragmentBase.prototype;
const FragmentClass = (<any>org.nativescript.widgets.FragmentBase).extend('com.tns.FragmentClass', { const FragmentClass = (<any>org.nativescript.widgets.FragmentBase).extend('com.tns.FragmentClass', {
@ -33,9 +36,7 @@ const FragmentClass = (<any>org.nativescript.widgets.FragmentBase).extend('com.t
}, },
onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle) { onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle) {
const result = this._callbacks.onCreateView(this, inflater, container, savedInstanceState, superProto.onCreateView); return this._callbacks.onCreateView(this, inflater, container, savedInstanceState, superProto.onCreateView);
return result;
}, },
onSaveInstanceState(outState: android.os.Bundle) { onSaveInstanceState(outState: android.os.Bundle) {
@ -61,4 +62,49 @@ const FragmentClass = (<any>org.nativescript.widgets.FragmentBase).extend('com.t
}, },
}); });
export let fragmentClass: any;
export function ensureFragmentClass() {
if (fragmentClass) {
return;
}
// this require will apply the FragmentClass implementation
require('./fragment');
if (!fragmentClass) {
throw new Error('Failed to initialize the extended androidx.fragment.app.Fragment class');
}
}
export function setFragmentClass(clazz: any) {
if (fragmentClass) {
throw new Error('Fragment class already initialized');
}
if (isEmbedded()) {
attachEmbeddableFragmentCallbacks();
}
fragmentClass = clazz;
}
function attachEmbeddableFragmentCallbacks() {
const Callbacks = com.tns.embedding.EmbeddableFragmentCallbacks.extend({
init() {
// init must at least be defined
},
onCreateView() {
return getEmbeddedView().nativeViewProtected;
},
onResume() {
getEmbeddedView().callLoaded();
},
onPause() {
getEmbeddedView().callUnloaded();
},
});
com.tns.embedding.CallbacksStore.setFragmentCallbacks(new Callbacks());
}
setFragmentClass(FragmentClass); setFragmentClass(FragmentClass);

5
packages/core/ui/frame/fragment.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
export function ensureFragmentClass(): void;
export function setFragmentClass(clazz: any): void;
export let fragmentClass: any;

View File

@ -0,0 +1,5 @@
export function ensureFragmentClass(): void {}
export function setFragmentClass(clazz: any): void {}
export let fragmentClass: any;

View File

@ -1,12 +1,11 @@
// Definitions. // Definitions.
import { AndroidActivityCallbacks, AndroidFragmentCallbacks, AndroidFrame as AndroidFrameDefinition, BackstackEntry, NavigationTransition } from '.'; import { AndroidActivityCallbacks, AndroidFrame as AndroidFrameDefinition, BackstackEntry, NavigationTransition } from '.';
import { Page } from '../page'; import { Page } from '../page';
import { TransitionState } from './frame-common'; import { TransitionState } from './frame-common';
// Types. // Types.
import { AndroidActivityBackPressedEventData, AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData, Application } from '../../application'; import { Application } from '../../application';
import { Color } from '../../color';
import { Observable } from '../../data/observable'; import { Observable } from '../../data/observable';
import { Trace } from '../../trace'; import { Trace } from '../../trace';
import { View } from '../core/view'; import { View } from '../core/view';
@ -17,21 +16,23 @@ import { _clearEntry, _clearFragment, _getAnimatedEntries, _reverseTransitions,
import { profile } from '../../profiling'; import { profile } from '../../profiling';
import { android as androidUtils } from '../../utils/native-helper'; import { android as androidUtils } from '../../utils/native-helper';
import type { ExpandedEntry } from './fragment.transitions.android'; import type { ExpandedEntry } from './fragment.transitions.android';
import { ensureFragmentClass, fragmentClass } from './fragment';
import { FragmentCallbacksImplementation } from './callbacks/fragment-callbacks';
import { ActivityCallbacksImplementation } from './callbacks/activity-callbacks';
export * from './frame-common'; export * from './frame-common';
export { setFragmentClass } from './fragment';
const INTENT_EXTRA = 'com.tns.activity'; const INTENT_EXTRA = 'com.tns.activity';
const ROOT_VIEW_ID_EXTRA = 'com.tns.activity.rootViewId';
const FRAMEID = '_frameId'; const FRAMEID = '_frameId';
const CALLBACKS = '_callbacks'; const CALLBACKS = '_callbacks';
const ownerSymbol = Symbol('_owner'); const ownerSymbol = Symbol('_owner');
const activityRootViewsMap = new Map<number, WeakRef<View>>();
let navDepth = -1; let navDepth = -1;
let fragmentId = -1; let fragmentId = -1;
export let moduleLoaded: boolean; export { moduleLoaded } from './callbacks/activity-callbacks';
export let attachStateChangeListener: android.view.View.OnAttachStateChangeListener; export let attachStateChangeListener: android.view.View.OnAttachStateChangeListener;
@ -756,38 +757,6 @@ class AndroidFrame extends Observable implements AndroidFrameDefinition {
} }
} }
function findPageForFragment(fragment: androidx.fragment.app.Fragment, frame: Frame) {
const fragmentTag = fragment.getTag();
if (Trace.isEnabled()) {
Trace.write(`Finding page for ${fragmentTag}.`, Trace.categories.NativeLifecycle);
}
let entry: BackstackEntry;
const current = frame._currentEntry;
const executingContext = frame._executingContext;
if (current && current.fragmentTag === fragmentTag) {
entry = current;
} else if (executingContext && executingContext.entry && executingContext.entry.fragmentTag === fragmentTag) {
entry = executingContext.entry;
}
let page: Page;
if (entry) {
entry.recreated = true;
page = entry.resolvedPage;
}
if (page) {
const callbacks: FragmentCallbacksImplementation = fragment[CALLBACKS];
callbacks.frame = frame;
callbacks.entry = entry;
entry.fragment = fragment;
_updateTransitions(entry);
} else {
throw new Error(`Could not find a page for ${fragmentTag}.`);
}
}
function startActivity(activity: androidx.appcompat.app.AppCompatActivity, frameId: number) { function startActivity(activity: androidx.appcompat.app.AppCompatActivity, frameId: number) {
// TODO: Implicitly, we will open the same activity type as the current one // TODO: Implicitly, we will open the same activity type as the current one
const intent = new android.content.Intent(activity, activity.getClass()); const intent = new android.content.Intent(activity, activity.getClass());
@ -798,7 +767,7 @@ function startActivity(activity: androidx.appcompat.app.AppCompatActivity, frame
activity.startActivity(intent); activity.startActivity(intent);
} }
function getFrameByNumberId(frameId: number): Frame { export function getFrameByNumberId(frameId: number): Frame {
// Find the frame for this activity. // Find the frame for this activity.
for (let i = 0; i < framesCache.length; i++) { for (let i = 0; i < framesCache.length; i++) {
const aliveFrame = framesCache[i].get(); const aliveFrame = framesCache[i].get();
@ -810,578 +779,6 @@ function getFrameByNumberId(frameId: number): Frame {
return null; return null;
} }
function ensureFragmentClass() {
if (fragmentClass) {
return;
}
// this require will apply the FragmentClass implementation
require('./fragment');
if (!fragmentClass) {
throw new Error('Failed to initialize the extended androidx.fragment.app.Fragment class');
}
}
let fragmentClass: any;
export function setFragmentClass(clazz: any) {
if (fragmentClass) {
throw new Error('Fragment class already initialized');
}
fragmentClass = clazz;
}
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 {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onHiddenChanged(${hidden})`, Trace.categories.NativeLifecycle);
}
superFunc.call(fragment, hidden);
}
@profile
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;
}
return animator || superFunc.call(fragment, transit, enter, nextAnim);
}
@profile
public onCreate(fragment: androidx.fragment.app.Fragment, savedInstanceState: android.os.Bundle, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onCreate(${savedInstanceState})`, Trace.categories.NativeLifecycle);
}
superFunc.call(fragment, savedInstanceState);
// There is no entry set to the fragment, so this must be destroyed fragment that was recreated by Android.
// We should find its corresponding page in our backstack and set it manually.
if (!this.entry) {
const args = fragment.getArguments();
const frameId = args.getInt(FRAMEID);
const frame = getFrameByNumberId(frameId);
if (!frame) {
throw new Error(`Cannot find Frame for ${fragment}`);
}
findPageForFragment(fragment, frame);
}
}
@profile
public onCreateView(fragment: androidx.fragment.app.Fragment, inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle, superFunc: Function): android.view.View {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onCreateView(inflater, container, ${savedInstanceState})`, Trace.categories.NativeLifecycle);
}
const entry = this.entry;
if (!entry) {
Trace.error(`${fragment}.onCreateView: entry is null or undefined`);
return null;
}
const page = entry.resolvedPage;
if (!page) {
Trace.error(`${fragment}.onCreateView: entry has no resolvedPage`);
return null;
}
const frame = this.frame;
if (!frame) {
Trace.error(`${fragment}.onCreateView: this.frame is null or undefined`);
return null;
}
frame._resolvedPage = page;
if (page.parent === frame) {
frame._inheritStyles(page);
// If we are navigating to a page that was destroyed
// reinitialize its UI.
if (!page._context) {
const context = (container && container.getContext()) || (inflater && inflater.getContext());
page._setupUI(context);
}
if (frame.isLoaded && !page.isLoaded) {
page.callLoaded();
}
} else {
if (!page.parent) {
if (!frame._styleScope) {
// Make sure page will have styleScope even if parents don't.
page._updateStyleScope();
}
frame._addView(page);
} else {
throw new Error('Page is already shown on another frame.');
}
}
const savedState = entry.viewSavedState;
if (savedState) {
(<android.view.View>page.nativeViewProtected).restoreHierarchyState(savedState);
entry.viewSavedState = null;
}
// fixes 'java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first'.
// on app resume in nested frame scenarios with support library version greater than 26.0.0
// HACK: this whole code block shouldn't be necessary as the native view is supposedly removed from its parent
// right after onDestroyView(...) is called but for some reason the fragment view (page) still thinks it has a
// parent while its supposed parent believes it properly removed its children; in order to "force" the child to
// lose its parent we temporarily add it to the parent, and then remove it (addViewInLayout doesn't trigger layout pass)
const nativeView = page.nativeViewProtected;
if (nativeView != null) {
const parentView = nativeView.getParent();
if (parentView instanceof android.view.ViewGroup) {
if (parentView.getChildCount() === 0) {
parentView.addViewInLayout(nativeView, -1, new org.nativescript.widgets.CommonLayoutParams());
}
parentView.removeAllViews();
}
}
return page.nativeViewProtected;
}
@profile
public onSaveInstanceState(fragment: androidx.fragment.app.Fragment, outState: android.os.Bundle, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onSaveInstanceState(${outState})`, Trace.categories.NativeLifecycle);
}
superFunc.call(fragment, outState);
}
@profile
public onDestroyView(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void {
try {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onDestroyView()`, Trace.categories.NativeLifecycle);
}
const hasRemovingParent = fragment.getRemovingParentFragment();
if (hasRemovingParent) {
const nativeFrameView = this.frame.nativeViewProtected;
if (nativeFrameView) {
const bitmapDrawable = new android.graphics.drawable.BitmapDrawable(Application.android.context.getResources(), this.backgroundBitmap);
this.frame._originalBackground = this.frame.backgroundColor || new Color('White');
nativeFrameView.setBackgroundDrawable(bitmapDrawable);
this.backgroundBitmap = null;
}
}
} finally {
superFunc.call(fragment);
}
}
@profile
public onDestroy(fragment: androidx.fragment.app.Fragment, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write(`${fragment}.onDestroy()`, Trace.categories.NativeLifecycle);
}
superFunc.call(fragment);
const entry = this.entry;
if (!entry) {
Trace.error(`${fragment}.onDestroy: entry is null or undefined`);
return null;
}
// [nested frames / fragments] see https://github.com/NativeScript/NativeScript/issues/6629
// retaining reference to a destroyed fragment here somehow causes a cryptic
// "IllegalStateException: Failure saving state: active fragment has cleared index: -1"
// in a specific mixed parent / nested frame navigation scenario
entry.fragment = null;
const page = entry.resolvedPage;
if (!page) {
// todo: check why this happens when using shared element transition!!!
// commented out the Trace.error to prevent a crash (the app will still work interestingly)
console.log(`${fragment}.onDestroy: entry has no resolvedPage`);
// Trace.error(`${fragment}.onDestroy: entry has no resolvedPage`);
return null;
}
}
@profile
public onPause(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void {
try {
// 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);
}
} finally {
superFunc.call(fragment);
}
}
@profile
public onResume(fragment: org.nativescript.widgets.FragmentBase, superFunc: Function): void {
const frame = this.entry.resolvedPage.frame;
// on some cases during the first navigation on nested frames the animation doesn't trigger
// we depend on the animation (even None animation) to set the entry as the current entry
// animation should start between start and resume, so if we have an executing navigation here it probably means the animation was skipped
// so we manually set the entry
// also, to be compatible with fragments 1.2.x we need this setTimeout as animations haven't run on onResume yet
const weakRef = new WeakRef(this);
setTimeout(() => {
const owner = weakRef.get();
if (!owner) {
return;
}
if (frame._executingContext && !(<any>owner.entry).isAnimationRunning) {
frame.setCurrent(owner.entry, frame._executingContext.navigationType);
}
}, 0);
superFunc.call(fragment);
}
@profile
public onStop(fragment: androidx.fragment.app.Fragment, superFunc: Function): void {
superFunc.call(fragment);
}
@profile
public toStringOverride(fragment: androidx.fragment.app.Fragment, superFunc: Function): string {
const entry = this.entry;
if (entry) {
return `${entry.fragmentTag}<${entry.resolvedPage}>`;
} else {
return 'NO ENTRY, ' + superFunc.call(fragment);
}
}
private loadBitmapFromView(view: android.view.View): android.graphics.Bitmap {
// Don't try to create bitmaps with no dimensions as this causes a crash
// This might happen when showing and closing dialogs fast.
if (!(view && view.getWidth() > 0 && view.getHeight() > 0)) {
return undefined;
}
// 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 drawCache = view.getDrawingCache();
// const bitmap = android.graphics.Bitmap.createBitmap(drawCache);
// view.setDrawingCacheEnabled(false);
return org.nativescript.widgets.Utils.getBitmapFromView(view);
}
}
class ActivityCallbacksImplementation implements AndroidActivityCallbacks {
private _rootView: View;
public getRootView(): View {
return this._rootView;
}
@profile
public onCreate(activity: androidx.appcompat.app.AppCompatActivity, savedInstanceState: android.os.Bundle, intentOrSuperFunc: android.content.Intent | Function, superFunc?: Function): void {
if (Trace.isEnabled()) {
Trace.write(`Activity.onCreate(${savedInstanceState})`, Trace.categories.NativeLifecycle);
}
const intent: android.content.Intent = superFunc ? <android.content.Intent>intentOrSuperFunc : undefined;
if (!superFunc) {
console.log('AndroidActivityCallbacks.onCreate(activity: any, savedInstanceState: any, superFunc: Function) ' + 'is deprecated. Use AndroidActivityCallbacks.onCreate(activity: any, savedInstanceState: any, intent: any, superFunc: Function) instead.');
superFunc = <Function>intentOrSuperFunc;
}
// If there is savedInstanceState this call will recreate all fragments that were previously in the navigation.
// We take care of associating them with a Page from our backstack in the onAttachFragment callback.
// If there is savedInstanceState and moduleLoaded is false we are restarted but process was killed.
// For now we treat it like first run (e.g. we are not passing savedInstanceState so no fragments are being restored).
// When we add support for application save/load state - revise this logic.
const isRestart = !!savedInstanceState && moduleLoaded;
superFunc.call(activity, isRestart ? savedInstanceState : null);
// Try to get the rootViewId form the saved state in case the activity
// was destroyed and we are now recreating it.
if (savedInstanceState) {
const rootViewId = savedInstanceState.getInt(ROOT_VIEW_ID_EXTRA, -1);
if (rootViewId !== -1 && activityRootViewsMap.has(rootViewId)) {
this._rootView = activityRootViewsMap.get(rootViewId)?.get();
}
}
if (intent && intent.getAction()) {
Application.android.notify(<AndroidActivityNewIntentEventData>{
eventName: Application.AndroidApplication.activityNewIntentEvent,
object: Application.android,
activity,
intent,
});
}
this.setActivityContent(activity, savedInstanceState, true);
moduleLoaded = true;
}
@profile
public onSaveInstanceState(activity: androidx.appcompat.app.AppCompatActivity, outState: android.os.Bundle, superFunc: Function): void {
superFunc.call(activity, outState);
const rootView = this._rootView;
if (rootView instanceof Frame) {
outState.putInt(INTENT_EXTRA, rootView.android.frameId);
rootView._saveFragmentsState();
}
if (rootView) {
outState.putInt(ROOT_VIEW_ID_EXTRA, rootView._domId);
}
}
@profile
public onNewIntent(activity: androidx.appcompat.app.AppCompatActivity, intent: android.content.Intent, superSetIntentFunc: Function, superFunc: Function): void {
superFunc.call(activity, intent);
superSetIntentFunc.call(activity, intent);
Application.android.notify(<AndroidActivityNewIntentEventData>{
eventName: Application.AndroidApplication.activityNewIntentEvent,
object: Application.android,
activity,
intent,
});
}
@profile
public onStart(activity: any, superFunc: Function): void {
superFunc.call(activity);
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onStart();', Trace.categories.NativeLifecycle);
}
const rootView = this._rootView;
if (rootView && !rootView.isLoaded) {
rootView.callLoaded();
}
}
@profile
public onStop(activity: any, superFunc: Function): void {
superFunc.call(activity);
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onStop();', Trace.categories.NativeLifecycle);
}
const rootView = this._rootView;
if (rootView && rootView.isLoaded) {
rootView.callUnloaded();
}
}
@profile
public onPostResume(activity: any, superFunc: Function): void {
superFunc.call(activity);
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onPostResume();', Trace.categories.NativeLifecycle);
}
// NOTE: activity.onPostResume() is called when activity resume is complete and we can
// safely raise the application resume event;
// onActivityResumed(...) lifecycle callback registered in application is called too early
// and raising the application resume event there causes issues like
// https://github.com/NativeScript/NativeScript/issues/6708
if ((<any>activity).isNativeScriptActivity) {
Application.setSuspended(false, {
// todo: deprecate in favor of using event.activity instead.
android: activity,
activity,
});
}
}
@profile
public onDestroy(activity: any, superFunc: Function): void {
try {
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onDestroy();', Trace.categories.NativeLifecycle);
}
const rootView = this._rootView;
if (rootView) {
rootView._tearDownUI(true);
}
// this may happen when the user changes the system theme
// In such case, isFinishing() is false (and isChangingConfigurations is true), and the app will start again (onCreate) with a savedInstanceState
// as a result, launchEvent will never be called
// possible alternative: always fire launchEvent and exitEvent, but pass extra flags to make it clear what kind of launch/destroy is happening
if (activity.isFinishing()) {
const exitArgs = {
eventName: Application.exitEvent,
object: Application.android,
android: activity,
};
Application.notify(exitArgs);
}
} finally {
superFunc.call(activity);
}
}
@profile
public onBackPressed(activity: any, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onBackPressed;', Trace.categories.NativeLifecycle);
}
const args = <AndroidActivityBackPressedEventData>{
eventName: 'activityBackPressed',
object: Application,
android: Application.android,
activity: activity,
cancel: false,
};
Application.android.notify(args);
if (args.cancel) {
return;
}
const view = this._rootView;
let callSuper = false;
const viewArgs = <AndroidActivityBackPressedEventData>{
eventName: 'activityBackPressed',
object: view,
activity: activity,
cancel: false,
};
view.notify(viewArgs);
// In the case of Frame, use this callback only if it was overridden, since the original will cause navigation issues
if (!viewArgs.cancel && (view.onBackPressed === Frame.prototype.onBackPressed || !view.onBackPressed())) {
callSuper = view instanceof Frame ? !FrameBase.goBack() : true;
}
if (callSuper) {
superFunc.call(activity);
}
}
@profile
public onRequestPermissionsResult(activity: any, requestCode: number, permissions: Array<string>, grantResults: Array<number>, superFunc: Function): void {
if (Trace.isEnabled()) {
Trace.write('NativeScriptActivity.onRequestPermissionsResult;', Trace.categories.NativeLifecycle);
}
Application.android.notify(<AndroidActivityRequestPermissionsEventData>{
eventName: 'activityRequestPermissions',
object: Application,
android: Application.android,
activity: activity,
requestCode: requestCode,
permissions: permissions,
grantResults: grantResults,
});
}
@profile
public onActivityResult(activity: any, requestCode: number, resultCode: number, data: android.content.Intent, superFunc: Function): void {
superFunc.call(activity, requestCode, resultCode, data);
if (Trace.isEnabled()) {
Trace.write(`NativeScriptActivity.onActivityResult(${requestCode}, ${resultCode}, ${data})`, Trace.categories.NativeLifecycle);
}
Application.android.notify(<AndroidActivityResultEventData>{
eventName: 'activityResult',
object: Application,
android: Application.android,
activity: activity,
requestCode: requestCode,
resultCode: resultCode,
intent: data,
});
}
public resetActivityContent(activity: androidx.appcompat.app.AppCompatActivity): void {
if (this._rootView) {
const manager = this._rootView._getFragmentManager();
manager.executePendingTransactions();
this._rootView._onRootViewReset();
}
// Delete previously cached root view in order to recreate it.
this._rootView = null;
this.setActivityContent(activity, null, false);
this._rootView.callLoaded();
}
// Paths that go trough this method:
// 1. Application initial start - there is no rootView in callbacks.
// 2. Application revived after Activity is destroyed. this._rootView should have been restored by id in onCreate.
// 3. Livesync if rootView has no custom _onLivesync. this._rootView should have been cleared upfront. Launch event should not fired
// 4. resetRootView method. this._rootView should have been cleared upfront. Launch event should not fired
private setActivityContent(activity: androidx.appcompat.app.AppCompatActivity, savedInstanceState: android.os.Bundle, fireLaunchEvent: boolean): void {
let rootView = this._rootView;
if (Trace.isEnabled()) {
Trace.write(`Frame.setActivityContent rootView: ${rootView} shouldCreateRootFrame: false fireLaunchEvent: ${fireLaunchEvent}`, Trace.categories.NativeLifecycle);
}
const intent = activity.getIntent();
rootView = Application.createRootView(rootView, fireLaunchEvent, {
// todo: deprecate in favor of args.intent?
android: intent,
intent,
savedInstanceState,
});
if (!rootView) {
// no root view created
return;
}
activityRootViewsMap.set(rootView._domId, new WeakRef(rootView));
// setup view as styleScopeHost
rootView._setupAsRootView(activity);
activity.setContentView(rootView.nativeViewProtected, new org.nativescript.widgets.CommonLayoutParams());
this._rootView = rootView;
// sets root classes once rootView is ready...
Application.initRootView(rootView);
}
}
export function setActivityCallbacks(activity: androidx.appcompat.app.AppCompatActivity): void { export function setActivityCallbacks(activity: androidx.appcompat.app.AppCompatActivity): void {
activity[CALLBACKS] = new ActivityCallbacksImplementation(); activity[CALLBACKS] = new ActivityCallbacksImplementation();
} }

View File

@ -17,6 +17,16 @@ export interface NavigationData extends EventData {
* Nested frames are supported, enabling hierarchical navigation scenarios. * Nested frames are supported, enabling hierarchical navigation scenarios.
*/ */
export class Frame extends FrameBase { export class Frame extends FrameBase {
/**
* @private
*/
_originalBackground?: any;
/**
* @private
*/
_saveFragmentsState?();
/** /**
* Gets a frame by id. * Gets a frame by id.
*/ */
@ -259,6 +269,12 @@ export function setFragmentClass(clazz: any): void;
*/ */
export function getFrameById(id: string): Frame; export function getFrameById(id: string): Frame;
/**
*
* (Android Only) Gets a frame by internally tracked id.
*/
export function getFrameByNumberId(frameId: number): Frame;
/** /**
* @deprecated Use Frame.topmost() instead. * @deprecated Use Frame.topmost() instead.
* *
@ -432,6 +448,8 @@ export interface BackstackEntry {
* To start a new Activity, a new Frame instance should be created and navigated to the desired Page. * To start a new Activity, a new Frame instance should be created and navigated to the desired Page.
*/ */
export interface AndroidFrame extends Observable { export interface AndroidFrame extends Observable {
frameId?: any;
/** /**
* Gets the native [android ViewGroup](http://developer.android.com/reference/android/view/ViewGroup.html) instance that represents the root layout part of the Frame. * Gets the native [android ViewGroup](http://developer.android.com/reference/android/view/ViewGroup.html) instance that represents the root layout part of the Frame.
*/ */

View File

@ -24,6 +24,7 @@ export { DialogStrings, action, alert, confirm, login, prompt, getCurrentPage, D
export type { DialogOptions, CancelableOptions, AlertOptions, PromptResult, PromptOptions, ActionOptions, ConfirmOptions, LoginResult, LoginOptions } from './dialogs'; export type { DialogOptions, CancelableOptions, AlertOptions, PromptResult, PromptOptions, ActionOptions, ConfirmOptions, LoginResult, LoginOptions } from './dialogs';
export * from './editable-text-base'; export * from './editable-text-base';
export { isEmbedded } from './embedding';
export { Frame, setActivityCallbacks } from './frame'; export { Frame, setActivityCallbacks } from './frame';
export type { NavigationEntry, NavigationContext, NavigationTransition, BackstackEntry, ViewEntry, AndroidActivityCallbacks } from './frame'; export type { NavigationEntry, NavigationContext, NavigationTransition, BackstackEntry, ViewEntry, AndroidActivityCallbacks } from './frame';

View File

@ -1,5 +1,4 @@
import { parseCSSStroke } from './css-stroke'; import { parseCSSStroke } from './css-stroke';
import { CoreTypes } from '../../core-types';
import { Length } from './style-properties'; import { Length } from './style-properties';
import { Color } from '../../color'; import { Color } from '../../color';

View File

@ -2,6 +2,9 @@ import { INativeScriptPlatform } from "../helpers/platform";
import { env } from '../'; import { env } from '../';
function getDistPath() { function getDistPath() {
if (process.env.USER_PROJECT_PLATFORMS_ANDROID) {
return `${process.env.USER_PROJECT_PLATFORMS_ANDROID}/${process.env.USER_PROJECT_PLATFORMS_ANDROID_MODULE}/src/nativescript/assets/app`;
}
return `${env.buildPath ?? "platforms"}/android/app/src/main/assets/app`; return `${env.buildPath ?? "platforms"}/android/app/src/main/assets/app`;
} }

View File

@ -11,10 +11,11 @@ function sanitizeName(appName: string): string {
).join(""); ).join("");
} }
function getDistPath() { function getDistPath() {
// try projectName from nativescript.config.ts, if not set, use original method // if nativescript.config projectName is defined, use that custom name
// otherwise, default to base project directory name for project name
const appName = getValue('projectName') ?? sanitizeName(basename(getProjectRootPath())); const appName = getValue('projectName') ?? sanitizeName(basename(getProjectRootPath()));
const platform = process.env.USER_PROJECT_PLATFORMS_IOS ? process.env.USER_PROJECT_PLATFORMS_IOS : `${env.buildPath ?? "platforms"}/ios`;
return `${env.buildPath ?? "platforms"}/ios/${appName}/app`; return `${platform}/${appName}/app`;
} }
const iOSPlatform: INativeScriptPlatform = { const iOSPlatform: INativeScriptPlatform = {