mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-17 04:41:36 +08:00

Switch Activity / Fragment / FragmentManager implementation from native framework to support library APIs BREAKING CHANGE: NativeScript core framework now extends support library APIs versus native framework classes as per Google's latest guidelines: - NativeScript activities now extend `android.support.v7.app.AppCompatActivity` (vs android.app.Activity) - NativeScript fragments now extend `android.support.v4.app.Fragment` (vs android.app.Fragment) - NativeScript now works internally with `android.support.v4.app.FragmentManager` (vs android.app.FragmentManager) The implications of these changes should be mostly transparent to the developer except for the fact that the support library Fragment / FragmentManager work with Animation APIs versus Animator APIs. For Android API Levels lower than 28 the new Fragment API uses a different fragment enter animation by default. You can customise the transition per navigation entry or globally via the [navigation transitions API](https://docs.nativescript.org/core-concepts/navigation#navigation-transitions) Before: Default fragment enter animation was fade animation After: Default fragment enter animation for API levels lower than 28 is now a fast "push fade" animation; default fragment enter animation for API levels equal to or greater than 28 remains fade animation Before: AndroidFragmentCallbacks interface exposed the following `onCreateAnimator(...)` method ``` ts export interface AndroidFragmentCallbacks { onCreateAnimator(fragment: any, transit: number, enter: boolean, nextAnim: number, superFunc: Function): any; // ... } ``` After: AndroidFragmentCallbacks interface now exposes the following `onCreateAnimation(...)` method instead (and `onCreateAnimator(...)` is now removed) ``` ts export interface AndroidFragmentCallbacks { onCreateAnimation(fragment: any, transit: number, enter: boolean, nextAnim: number, superFunc: Function): any; // ... } ``` Before: Transition class exposed the following abstract `createAndroidAnimator(...)` method ``` ts export class Transition { public createAndroidAnimator(transitionType: string): any; // ... } ``` After: Transition class now exposes the following abstract `createAndroidAnimation(...)` method instead (and `createAndroidAnimation(...) is now removed) ``` ts export class Transition { public createAndroidAnimation(transitionType: string): any; // ... } ``` To migrate the code of your custom transitions follow the example below: Before: ``` ts import * as transition from "tns-core-modules/ui/transition"; export class CustomTransition extends transition.Transition { constructor(duration: number, curve: any) { super(duration, curve); } public createAndroidAnimator(transitionType: string): android.animation.Animator { var scaleValues = Array.create("float", 2); switch (transitionType) { case transition.AndroidTransitionType.enter: case transition.AndroidTransitionType.popEnter: scaleValues[0] = 0; scaleValues[1] = 1; break; case transition.AndroidTransitionType.exit: case transition.AndroidTransitionType.popExit: scaleValues[0] = 1; scaleValues[1] = 0; break; } var objectAnimators = Array.create(android.animation.Animator, 2); objectAnimators[0] = android.animation.ObjectAnimator.ofFloat(null, "scaleX", scaleValues); objectAnimators[1] = android.animation.ObjectAnimator.ofFloat(null, "scaleY", scaleValues); var animatorSet = new android.animation.AnimatorSet(); animatorSet.playTogether(objectAnimators); var duration = this.getDuration(); if (duration !== undefined) { animatorSet.setDuration(duration); } animatorSet.setInterpolator(this.getCurve()); return animatorSet; } } ``` After: ``` ts import * as transition from "tns-core-modules/ui/transition"; export class CustomTransition extends transition.Transition { constructor(duration: number, curve: any) { super(duration, curve); } public createAndroidAnimation(transitionType: string): android.view.animation.Animation { const scaleValues = []; switch (transitionType) { case transition.AndroidTransitionType.enter: case transition.AndroidTransitionType.popEnter: scaleValues[0] = 0; scaleValues[1] = 1; break; case transition.AndroidTransitionType.exit: case transition.AndroidTransitionType.popExit: scaleValues[0] = 1; scaleValues[1] = 0; break; } const animationSet = new android.view.animation.AnimationSet(false); const duration = this.getDuration(); if (duration !== undefined) { animationSet.setDuration(duration); } animationSet.setInterpolator(this.getCurve()); animationSet.addAnimation( new android.view.animation.ScaleAnimation( scaleValues[0], scaleValues[1], scaleValues[0], scaleValues[1] )); return animationSet; } } ```
394 lines
17 KiB
TypeScript
394 lines
17 KiB
TypeScript
import {
|
|
AndroidActivityBundleEventData, AndroidActivityEventData, ApplicationEventData, OrientationChangedEventData,
|
|
AndroidApplication as AndroidApplicationDefinition,
|
|
AndroidActivityResultEventData, AndroidActivityBackPressedEventData, AndroidActivityRequestPermissionsEventData
|
|
} from ".";
|
|
|
|
import {
|
|
notify, hasListeners, lowMemoryEvent, orientationChangedEvent, suspendEvent, resumeEvent, displayedEvent,
|
|
setApplication, livesync, Observable
|
|
} from "./application-common";
|
|
import { profile } from "../profiling";
|
|
|
|
// First reexport so that app module is initialized.
|
|
export * from "./application-common";
|
|
|
|
// types
|
|
import { NavigationEntry, View, AndroidActivityCallbacks } from "../ui/frame";
|
|
|
|
const ActivityCreated = "activityCreated";
|
|
const ActivityDestroyed = "activityDestroyed";
|
|
const ActivityStarted = "activityStarted";
|
|
const ActivityPaused = "activityPaused";
|
|
const ActivityResumed = "activityResumed";
|
|
const ActivityStopped = "activityStopped";
|
|
const SaveActivityState = "saveActivityState";
|
|
const ActivityResult = "activityResult";
|
|
const ActivityBackPressed = "activityBackPressed";
|
|
const ActivityRequestPermissions = "activityRequestPermissions";
|
|
|
|
export class AndroidApplication extends Observable implements AndroidApplicationDefinition {
|
|
public static activityCreatedEvent = ActivityCreated;
|
|
public static activityDestroyedEvent = ActivityDestroyed;
|
|
public static activityStartedEvent = ActivityStarted;
|
|
public static activityPausedEvent = ActivityPaused;
|
|
public static activityResumedEvent = ActivityResumed;
|
|
public static activityStoppedEvent = ActivityStopped;
|
|
public static saveActivityStateEvent = SaveActivityState;
|
|
public static activityResultEvent = ActivityResult;
|
|
public static activityBackPressedEvent = ActivityBackPressed;
|
|
public static activityRequestPermissionsEvent = ActivityRequestPermissions;
|
|
|
|
public paused: boolean;
|
|
public nativeApp: android.app.Application;
|
|
public context: android.content.Context;
|
|
public foregroundActivity: android.support.v7.app.AppCompatActivity;
|
|
public startActivity: android.support.v7.app.AppCompatActivity;
|
|
public packageName: string;
|
|
// we are using these property to store the callbacks to avoid early GC collection which would trigger MarkReachableObjects
|
|
private callbacks: any = {};
|
|
|
|
public get currentContext(): android.content.Context {
|
|
return this.foregroundActivity;
|
|
}
|
|
|
|
public init(nativeApp: android.app.Application) {
|
|
if (this.nativeApp === nativeApp) {
|
|
return;
|
|
}
|
|
|
|
if (this.nativeApp) {
|
|
throw new Error("application.android already initialized.");
|
|
}
|
|
|
|
this.nativeApp = nativeApp;
|
|
this.packageName = nativeApp.getPackageName();
|
|
this.context = nativeApp.getApplicationContext();
|
|
|
|
// we store those callbacks and add a function for clearing them later so that the objects will be eligable for GC
|
|
this.callbacks.lifecycleCallbacks = initLifecycleCallbacks();
|
|
this.callbacks.componentCallbacks = initComponentCallbacks();
|
|
this.nativeApp.registerActivityLifecycleCallbacks(this.callbacks.lifecycleCallbacks);
|
|
this.nativeApp.registerComponentCallbacks(this.callbacks.componentCallbacks);
|
|
|
|
this._registerPendingReceivers();
|
|
}
|
|
|
|
private _registeredReceivers = {};
|
|
private _pendingReceiverRegistrations = new Array<(context: android.content.Context) => void>();
|
|
private _registerPendingReceivers() {
|
|
this._pendingReceiverRegistrations.forEach(func => func(this.context));
|
|
this._pendingReceiverRegistrations.length = 0;
|
|
}
|
|
|
|
public registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) {
|
|
ensureBroadCastReceiverClass();
|
|
const that = this;
|
|
const registerFunc = function (context: android.content.Context) {
|
|
const receiver: android.content.BroadcastReceiver = new BroadcastReceiverClass(onReceiveCallback);
|
|
context.registerReceiver(receiver, new android.content.IntentFilter(intentFilter));
|
|
that._registeredReceivers[intentFilter] = receiver;
|
|
};
|
|
|
|
if (this.context) {
|
|
registerFunc(this.context);
|
|
}
|
|
else {
|
|
this._pendingReceiverRegistrations.push(registerFunc);
|
|
}
|
|
}
|
|
|
|
public unregisterBroadcastReceiver(intentFilter: string) {
|
|
const receiver = this._registeredReceivers[intentFilter];
|
|
if (receiver) {
|
|
this.context.unregisterReceiver(receiver);
|
|
this._registeredReceivers[intentFilter] = undefined;
|
|
delete this._registeredReceivers[intentFilter];
|
|
}
|
|
}
|
|
}
|
|
export interface AndroidApplication {
|
|
on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any);
|
|
on(event: "activityCreated", callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any);
|
|
on(event: "activityDestroyed", callback: (args: AndroidActivityEventData) => void, thisArg?: any);
|
|
on(event: "activityStarted", callback: (args: AndroidActivityEventData) => void, thisArg?: any);
|
|
on(event: "activityPaused", callback: (args: AndroidActivityEventData) => void, thisArg?: any);
|
|
on(event: "activityResumed", callback: (args: AndroidActivityEventData) => void, thisArg?: any);
|
|
on(event: "activityStopped", callback: (args: AndroidActivityEventData) => void, thisArg?: any);
|
|
on(event: "saveActivityState", callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any);
|
|
on(event: "activityResult", callback: (args: AndroidActivityResultEventData) => void, thisArg?: any);
|
|
on(event: "activityBackPressed", callback: (args: AndroidActivityBackPressedEventData) => void, thisArg?: any);
|
|
on(event: "activityRequestPermissions", callback: (args: AndroidActivityRequestPermissionsEventData) => void, thisArg?: any);
|
|
}
|
|
|
|
const androidApp = new AndroidApplication();
|
|
// use the exports object instead of 'export var' due to global namespace collision
|
|
exports.android = androidApp;
|
|
setApplication(androidApp);
|
|
|
|
let mainEntry: NavigationEntry;
|
|
let started = false;
|
|
// NOTE: for backwards compatibility. Remove for 4.0.0.
|
|
const createRootFrame = { value: true };
|
|
export function start(entry?: NavigationEntry | string) {
|
|
if (started) {
|
|
throw new Error("Application is already started.");
|
|
}
|
|
|
|
started = true;
|
|
mainEntry = typeof entry === "string" ? { moduleName: entry } : entry;
|
|
if (!androidApp.nativeApp) {
|
|
const nativeApp = getNativeApplication();
|
|
androidApp.init(nativeApp);
|
|
}
|
|
}
|
|
|
|
export function shouldCreateRootFrame(): boolean {
|
|
return createRootFrame.value;
|
|
}
|
|
|
|
export function run(entry?: NavigationEntry | string) {
|
|
createRootFrame.value = false;
|
|
start(entry);
|
|
}
|
|
|
|
const CALLBACKS = "_callbacks";
|
|
|
|
export function _resetRootView(entry?: NavigationEntry | string) {
|
|
const activity = androidApp.foregroundActivity;
|
|
if (!activity) {
|
|
throw new Error("Cannot find android activity.");
|
|
}
|
|
|
|
createRootFrame.value = false;
|
|
mainEntry = typeof entry === "string" ? { moduleName: entry } : entry;
|
|
const callbacks: AndroidActivityCallbacks = activity[CALLBACKS];
|
|
callbacks.resetActivityContent(activity);
|
|
}
|
|
|
|
export function getMainEntry() {
|
|
return mainEntry;
|
|
}
|
|
|
|
export function getRootView(): View {
|
|
// Use start activity as a backup when foregroundActivity is still not set
|
|
// in cases when we are getting the root view before activity.onResumed event is fired
|
|
const activity = androidApp.foregroundActivity || androidApp.startActivity;
|
|
if (!activity) {
|
|
return undefined;
|
|
}
|
|
const callbacks: AndroidActivityCallbacks = activity[CALLBACKS];
|
|
|
|
return callbacks ? callbacks.getRootView() : undefined;
|
|
}
|
|
|
|
export function getNativeApplication(): android.app.Application {
|
|
// Try getting it from module - check whether application.android.init has been explicitly called
|
|
let nativeApp = androidApp.nativeApp;
|
|
if (!nativeApp) {
|
|
// check whether the com.tns.NativeScriptApplication type exists
|
|
if (!nativeApp && com.tns.NativeScriptApplication) {
|
|
nativeApp = com.tns.NativeScriptApplication.getInstance();
|
|
}
|
|
|
|
// the getInstance might return null if com.tns.NativeScriptApplication exists but is not the starting app type
|
|
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?
|
|
const clazz = java.lang.Class.forName("android.app.ActivityThread");
|
|
if (clazz) {
|
|
const method = clazz.getMethod("currentApplication", null);
|
|
if (method) {
|
|
nativeApp = method.invoke(null, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
// we cannot work without having the app instance
|
|
if (!nativeApp) {
|
|
throw new Error("Failed to retrieve native Android Application object. If you have a custom android.app.Application type implemented make sure that you've called the '<application-module>.android.init' method.")
|
|
}
|
|
}
|
|
|
|
return nativeApp;
|
|
}
|
|
|
|
global.__onLiveSync = function () {
|
|
if (androidApp && androidApp.paused) {
|
|
return;
|
|
}
|
|
|
|
livesync();
|
|
};
|
|
|
|
function initLifecycleCallbacks() {
|
|
const setThemeOnLaunch = profile("setThemeOnLaunch", (activity: android.support.v7.app.AppCompatActivity) => {
|
|
// Set app theme after launch screen was used during startup
|
|
const activityInfo = activity.getPackageManager().getActivityInfo(activity.getComponentName(), android.content.pm.PackageManager.GET_META_DATA);
|
|
if (activityInfo.metaData) {
|
|
const setThemeOnLaunch = activityInfo.metaData.getInt("SET_THEME_ON_LAUNCH", -1);
|
|
if (setThemeOnLaunch !== -1) {
|
|
activity.setTheme(setThemeOnLaunch);
|
|
}
|
|
}
|
|
});
|
|
|
|
const notifyActivityCreated = profile("notifyActivityCreated", function (activity: android.support.v7.app.AppCompatActivity, savedInstanceState: android.os.Bundle) {
|
|
androidApp.notify(<AndroidActivityBundleEventData>{ eventName: ActivityCreated, object: androidApp, activity, bundle: savedInstanceState });
|
|
});
|
|
|
|
const subscribeForGlobalLayout = profile("subscribeForGlobalLayout", function (activity: android.support.v7.app.AppCompatActivity) {
|
|
const rootView = activity.getWindow().getDecorView().getRootView();
|
|
// store the listener not to trigger GC collection before collecting the method
|
|
this.onGlobalLayoutListener = new android.view.ViewTreeObserver.OnGlobalLayoutListener({
|
|
onGlobalLayout() {
|
|
notify({ eventName: displayedEvent, object: androidApp, activity });
|
|
let viewTreeObserver = rootView.getViewTreeObserver();
|
|
viewTreeObserver.removeOnGlobalLayoutListener(this.onGlobalLayoutListener);
|
|
}
|
|
});
|
|
rootView.getViewTreeObserver().addOnGlobalLayoutListener(this.onGlobalLayoutListener);
|
|
});
|
|
|
|
const lifecycleCallbacks = new android.app.Application.ActivityLifecycleCallbacks({
|
|
onActivityCreated: profile("onActivityCreated", function (activity: android.support.v7.app.AppCompatActivity, savedInstanceState: android.os.Bundle) {
|
|
setThemeOnLaunch(activity);
|
|
|
|
if (!androidApp.startActivity) {
|
|
androidApp.startActivity = activity;
|
|
}
|
|
|
|
notifyActivityCreated(activity, savedInstanceState);
|
|
|
|
if (hasListeners(displayedEvent)) {
|
|
subscribeForGlobalLayout(activity);
|
|
}
|
|
}),
|
|
|
|
onActivityDestroyed: profile("onActivityDestroyed", function (activity: android.support.v7.app.AppCompatActivity) {
|
|
if (activity === androidApp.foregroundActivity) {
|
|
androidApp.foregroundActivity = undefined;
|
|
}
|
|
|
|
if (activity === androidApp.startActivity) {
|
|
androidApp.startActivity = undefined;
|
|
}
|
|
|
|
androidApp.notify(<AndroidActivityEventData>{ eventName: ActivityDestroyed, object: androidApp, activity: activity });
|
|
// TODO: This is a temporary workaround to force the V8's Garbage Collector, which will force the related Java Object to be collected.
|
|
gc();
|
|
}),
|
|
|
|
onActivityPaused: profile("onActivityPaused", function (activity: android.support.v7.app.AppCompatActivity) {
|
|
if ((<any>activity).isNativeScriptActivity) {
|
|
androidApp.paused = true;
|
|
notify(<ApplicationEventData>{ eventName: suspendEvent, object: androidApp, android: activity });
|
|
}
|
|
|
|
androidApp.notify(<AndroidActivityEventData>{ eventName: ActivityPaused, object: androidApp, activity: activity });
|
|
}),
|
|
|
|
onActivityResumed: profile("onActivityResumed", function (activity: android.support.v7.app.AppCompatActivity) {
|
|
androidApp.foregroundActivity = activity;
|
|
|
|
if ((<any>activity).isNativeScriptActivity) {
|
|
notify(<ApplicationEventData>{ eventName: resumeEvent, object: androidApp, android: activity });
|
|
androidApp.paused = false;
|
|
}
|
|
|
|
androidApp.notify(<AndroidActivityEventData>{ eventName: ActivityResumed, object: androidApp, activity: activity });
|
|
}),
|
|
|
|
onActivitySaveInstanceState: profile("onActivityResumed", function (activity: android.support.v7.app.AppCompatActivity, outState: android.os.Bundle) {
|
|
androidApp.notify(<AndroidActivityBundleEventData>{ eventName: SaveActivityState, object: androidApp, activity: activity, bundle: outState });
|
|
}),
|
|
|
|
onActivityStarted: profile("onActivityStarted", function (activity: android.support.v7.app.AppCompatActivity) {
|
|
androidApp.notify(<AndroidActivityEventData>{ eventName: ActivityStarted, object: androidApp, activity: activity });
|
|
}),
|
|
|
|
onActivityStopped: profile("onActivityStopped", function (activity: android.support.v7.app.AppCompatActivity) {
|
|
androidApp.notify(<AndroidActivityEventData>{ eventName: ActivityStopped, object: androidApp, activity: activity });
|
|
})
|
|
});
|
|
|
|
return lifecycleCallbacks;
|
|
}
|
|
|
|
let currentOrientation: number;
|
|
function initComponentCallbacks() {
|
|
let componentCallbacks = new android.content.ComponentCallbacks2({
|
|
onLowMemory: profile("onLowMemory", function () {
|
|
gc();
|
|
java.lang.System.gc();
|
|
notify(<ApplicationEventData>{ eventName: lowMemoryEvent, object: this, android: this });
|
|
}),
|
|
|
|
onTrimMemory: profile("onTrimMemory", function (level: number) {
|
|
// TODO: This is skipped for now, test carefully for OutOfMemory exceptions
|
|
}),
|
|
|
|
onConfigurationChanged: profile("onConfigurationChanged", function (newConfig: android.content.res.Configuration) {
|
|
const newOrientation = newConfig.orientation;
|
|
if (newOrientation === currentOrientation) {
|
|
return;
|
|
}
|
|
|
|
currentOrientation = newOrientation;
|
|
let newValue;
|
|
|
|
switch (newOrientation) {
|
|
case android.content.res.Configuration.ORIENTATION_LANDSCAPE:
|
|
newValue = "landscape";
|
|
break;
|
|
case android.content.res.Configuration.ORIENTATION_PORTRAIT:
|
|
newValue = "portrait";
|
|
break;
|
|
default:
|
|
newValue = "unknown";
|
|
break;
|
|
}
|
|
|
|
notify(<OrientationChangedEventData>{
|
|
eventName: orientationChangedEvent,
|
|
android: androidApp.nativeApp,
|
|
newValue: newValue,
|
|
object: androidApp
|
|
});
|
|
})
|
|
});
|
|
|
|
return componentCallbacks;
|
|
}
|
|
|
|
let BroadcastReceiverClass;
|
|
function ensureBroadCastReceiverClass() {
|
|
if (BroadcastReceiverClass) {
|
|
return;
|
|
}
|
|
|
|
class BroadcastReceiver extends android.content.BroadcastReceiver {
|
|
private _onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void;
|
|
|
|
constructor(onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) {
|
|
super();
|
|
this._onReceiveCallback = onReceiveCallback;
|
|
return global.__native(this);
|
|
}
|
|
|
|
public onReceive(context: android.content.Context, intent: android.content.Intent) {
|
|
if (this._onReceiveCallback) {
|
|
this._onReceiveCallback(context, intent);
|
|
}
|
|
}
|
|
}
|
|
|
|
BroadcastReceiverClass = BroadcastReceiver;
|
|
}
|
|
|
|
declare namespace com {
|
|
namespace tns {
|
|
class NativeScriptApplication extends android.app.Application {
|
|
static getInstance(): NativeScriptApplication;
|
|
}
|
|
}
|
|
} |