Merge branch 'main' into feat/simpler-image-scale-ios

This commit is contained in:
Nathan Walker
2023-03-23 13:20:47 -07:00
committed by GitHub
80 changed files with 2387 additions and 835 deletions

View File

@@ -31,20 +31,11 @@ export const accessibilityEnabledProperty = new CssProperty<Style, boolean>({
});
accessibilityEnabledProperty.register(Style);
const accessibilityHiddenPropertyName = 'accessibilityHidden';
const accessibilityHiddenCssName = 'a11y-hidden';
export const accessibilityHiddenProperty = global.isIOS
? new InheritedCssProperty({
name: accessibilityHiddenPropertyName,
cssName: accessibilityHiddenCssName,
valueConverter: booleanConverter,
})
: new CssProperty({
name: accessibilityHiddenPropertyName,
cssName: accessibilityHiddenCssName,
valueConverter: booleanConverter,
});
export const accessibilityHiddenProperty = new (global.isIOS ? InheritedCssProperty : CssProperty)({
name: 'accessibilityHidden',
cssName: 'a11y-hidden',
valueConverter: booleanConverter,
});
accessibilityHiddenProperty.register(Style);
export const accessibilityIdentifierProperty = new Property<View, string>({

View File

@@ -206,6 +206,19 @@ export class iOSApplication implements iOSApplicationDefinition {
return this._rootView;
}
public setSystemAppearance(value: 'light' | 'dark' | null) {
if (this.systemAppearance !== value) {
this._systemAppearance = value;
systemAppearanceChanged(this.rootView, value);
notify(<SystemAppearanceChangedEventData>{
eventName: systemAppearanceChangedEvent,
ios: this,
newValue: iosApp.systemAppearance,
object: this,
});
}
}
public addNotificationObserver(notificationName: string, onReceiveCallback: (notification: NSNotification) => void): NotificationObserver {
const observer = NotificationObserver.initWithCallback(onReceiveCallback);
NSNotificationCenter.defaultCenter.addObserverSelectorNameObject(observer, 'onReceive', notificationName, null);
@@ -376,17 +389,7 @@ export class iOSApplication implements iOSApplicationDefinition {
const userInterfaceStyle = controller.traitCollection.userInterfaceStyle;
const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle);
if (this._systemAppearance !== newSystemAppearance) {
this._systemAppearance = newSystemAppearance;
systemAppearanceChanged(rootView, newSystemAppearance);
notify(<SystemAppearanceChangedEventData>{
eventName: systemAppearanceChangedEvent,
ios: this,
newValue: this._systemAppearance,
object: this,
});
}
this.setSystemAppearance(newSystemAppearance);
});
}
}
@@ -404,7 +407,7 @@ export function ensureNativeApplication() {
}
// attach on global, so it can be overwritten in NativeScript Angular
(<any>global).__onLiveSyncCore = function (context?: ModuleContext) {
global.__onLiveSyncCore = function (context?: ModuleContext) {
ensureNativeApplication();
iosApp._onLivesync(context);
};
@@ -461,6 +464,7 @@ export function run(entry?: string | NavigationEntry) {
rootView._setupAsRootView({});
const embedderDelegate = NativeScriptEmbedder.sharedInstance().delegate;
if (embedderDelegate) {
setViewControllerView(rootView);
embedderDelegate.presentNativeScriptApp(controller);
} else {
const visibleVC = getVisibleViewController(rootController);
@@ -474,16 +478,7 @@ export function run(entry?: string | NavigationEntry) {
const userInterfaceStyle = controller.traitCollection.userInterfaceStyle;
const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle);
if (this._systemAppearance !== newSystemAppearance) {
this._systemAppearance = newSystemAppearance;
notify(<SystemAppearanceChangedEventData>{
eventName: systemAppearanceChangedEvent,
ios: this,
newValue: this._systemAppearance,
object: this,
});
}
iosApp.setSystemAppearance(newSystemAppearance);
});
iosApp.notifyAppStarted();
}

View File

@@ -254,6 +254,44 @@ export class FileSystemAccess implements IFileSystemAccess {
return this.getLogicalRootPath() + '/app';
}
public readBuffer = this.readBufferSync.bind(this);
public readBufferAsync(path: string): Promise<ArrayBuffer> {
return new Promise<ArrayBuffer>((resolve, reject) => {
try {
org.nativescript.widgets.Async.File.readBuffer(
path,
new org.nativescript.widgets.Async.CompleteCallback({
onComplete: (result: java.nio.ByteBuffer) => {
resolve((ArrayBuffer as any).from(result));
},
onError: (err) => {
reject(new Error(err));
},
}),
null
);
} catch (ex) {
reject(ex);
}
});
}
public readBufferSync(path: string, onError?: (error: any) => any) {
try {
const javaFile = new java.io.File(path);
const stream = new java.io.FileInputStream(javaFile);
const channel = stream.getChannel();
const buffer = new ArrayBuffer(javaFile.length());
channel.read(buffer as any);
return buffer;
} catch (exception) {
if (onError) {
onError(exception);
}
}
}
public read = this.readSync.bind(this);
public readAsync(path: string): Promise<number[]> {
@@ -293,6 +331,52 @@ export class FileSystemAccess implements IFileSystemAccess {
}
}
static getBuffer(buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): any {
if (buffer instanceof ArrayBuffer) {
return (buffer as any).nativeObject || buffer;
} else {
return (buffer?.buffer as any)?.nativeObject || buffer;
}
}
public writeBuffer = this.writeBufferSync.bind(this);
public writeBufferAsync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
org.nativescript.widgets.Async.File.writeBuffer(
path,
FileSystemAccess.getBuffer(buffer),
new org.nativescript.widgets.Async.CompleteCallback({
onComplete: () => {
resolve();
},
onError: (err) => {
reject(new Error(err));
},
}),
null
);
} catch (ex) {
reject(ex);
}
});
}
public writeBufferSync(path: string, buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
try {
const javaFile = new java.io.File(path);
const stream = new java.io.FileOutputStream(javaFile);
const channel = stream.getChannel();
channel.write(FileSystemAccess.getBuffer(buffer));
stream.close();
} catch (exception) {
if (onError) {
onError(exception);
}
}
}
public write = this.writeSync.bind(this);
public writeAsync(path: string, bytes: androidNative.Array<number>): Promise<void> {
@@ -757,6 +841,7 @@ export class FileSystemAccess29 extends FileSystemAccess {
getCurrentAppPath(): string {
return super.getCurrentAppPath();
}
public readText = this.readTextSync.bind(this);
readTextAsync(path: string, encoding?: any): Promise<string> {
@@ -795,6 +880,47 @@ export class FileSystemAccess29 extends FileSystemAccess {
}
}
readBuffer = this.readBufferSync.bind(this);
readBufferAsync(path: string): Promise<any> {
if (isContentUri(path)) {
return new Promise((resolve, reject) => {
getOrSetHelper(path).readBuffer(
applicationContext,
new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {
resolve(result);
},
onError(error) {
reject(error);
},
})
);
});
}
return super.readBufferAsync(path);
}
readBufferSync(path: string, onError?: (error: any) => any) {
if (isContentUri(path)) {
let callback = null;
if (typeof onError === 'function') {
callback = new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {},
onError(error) {
onError(error);
},
});
}
const ret = getOrSetHelper(path).readBufferSync(applicationContext, callback);
if (ret) {
return null;
}
return (ArrayBuffer as any).from(ret);
}
return super.readBufferSync(path, onError);
}
read = this.readSync.bind(this);
readAsync(path: string): Promise<any> {
@@ -872,6 +998,45 @@ export class FileSystemAccess29 extends FileSystemAccess {
}
}
writeBuffer = this.writeBufferSync.bind(this);
writeBufferAsync(path: string, content: any): Promise<void> {
if (isContentUri(path)) {
return new Promise<void>((resolve, reject) => {
getOrSetHelper(path).writeBuffer(
applicationContext,
FileSystemAccess.getBuffer(content),
new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {
resolve();
},
onError(error) {
reject(error);
},
})
);
});
}
return super.writeAsync(path, content);
}
writeBufferSync(path: string, content: any, onError?: (error: any) => any) {
if (isContentUri(path)) {
let callback = null;
if (typeof onError === 'function') {
callback = new org.nativescript.widgets.FileHelper.Callback({
onSuccess(result) {},
onError(error) {
onError(error);
},
});
}
getOrSetHelper(path).writeSync(applicationContext, FileSystemAccess.getBuffer(content), callback);
} else {
super.writeSync(path, content, onError);
}
}
write = this.writeSync.bind(this);
writeAsync(path: string, content: any): Promise<void> {

View File

@@ -298,6 +298,12 @@ export class FileSystemAccess implements IFileSystemAccess {
readTextSync(path: string, onError?: (error: any) => any, encoding?: any): string;
readBuffer(path: string, onError?: (error: any) => any): ArrayBuffer;
readBufferAsync(path: string): Promise<ArrayBuffer>;
readBufferSync(path: string, onError?: (error: any) => any): ArrayBuffer;
read(path: string, onError?: (error: any) => any): any;
readAsync(path: string): Promise<any>;
@@ -310,6 +316,12 @@ export class FileSystemAccess implements IFileSystemAccess {
writeTextSync(path: string, content: string, onError?: (error: any) => any, encoding?: any);
writeBuffer(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any);
writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void>;
writeBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any);
write(path: string, content: any, onError?: (error: any) => any);
writeAsync(path: string, content: any): Promise<void>;

View File

@@ -294,6 +294,30 @@ export class FileSystemAccess {
}
}
public readBuffer = this.readBufferSync.bind(this);
public readBufferAsync(path: string): Promise<ArrayBuffer> {
return new Promise<ArrayBuffer>((resolve, reject) => {
try {
(NSData as any).dataWithContentsOfFileCompletion(path, (data) => {
resolve(interop.bufferFromData(data));
});
} catch (ex) {
reject(new Error("Failed to read file at path '" + path + "': " + ex));
}
});
}
public readBufferSync(path: string, onError?: (error: any) => any): ArrayBuffer {
try {
return interop.bufferFromData(NSData.dataWithContentsOfFile(path));
} catch (ex) {
if (onError) {
onError(new Error("Failed to read file at path '" + path + "': " + ex));
}
}
}
public read = this.readSync.bind(this);
public readAsync(path: string): Promise<NSData> {
@@ -352,6 +376,40 @@ export class FileSystemAccess {
}
}
static getBuffer(buffer: ArrayBuffer | Uint8Array | Uint8ClampedArray): NSData {
if (buffer instanceof ArrayBuffer) {
return NSData.dataWithData(buffer as any);
} else {
const buf = NSData.dataWithData(buffer?.buffer as any);
const len = buffer.byteLength;
return NSData.dataWithBytesNoCopyLength((buf.bytes as interop.Pointer).add(buffer?.byteOffset ?? 0), len);
}
}
public writeBuffer = this.writeBufferSync.bind(this);
public writeBufferAsync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
FileSystemAccess.getBuffer(content).writeToFileAtomicallyCompletion(path, true, () => {
resolve();
});
} catch (ex) {
reject(new Error("Failed to write file at path '" + path + "': " + ex));
}
});
}
public writeBufferSync(path: string, content: ArrayBuffer | Uint8Array | Uint8ClampedArray, onError?: (error: any) => any) {
try {
FileSystemAccess.getBuffer(content).writeToFileAtomically(path, true);
} catch (ex) {
if (onError) {
onError(new Error("Failed to write to file '" + path + "': " + ex));
}
}
}
public write = this.writeSync.bind(this);
public writeAsync(path: string, content: NSData): Promise<void> {

View File

@@ -27,7 +27,7 @@ export class FPSCallback implements definition.FPSCallback {
}
private _isNativeFramesSupported() {
return typeof (<any>global).__postFrameCallback === 'function' && typeof (<any>global).__removeFrameCallback === 'function';
return typeof (global as any).__postFrameCallback === 'function' && typeof global.__removeFrameCallback === 'function';
}
public start() {

View File

@@ -17,106 +17,106 @@ declare interface NativeScriptError extends Error {
}
//Augment the NodeJS global type with our own extensions
declare namespace NodeJS {
interface Global {
NativeScriptHasInitGlobal?: boolean;
NativeScriptGlobals?: {
/**
* Global framework event handling
*/
events: {
[Key in keyof import('data/observable').Observable]: import('data/observable').Observable[Key];
};
launched: boolean;
// used by various classes to setup callbacks to wire up global app event handling when the app instance is ready
appEventWiring: Array<any>;
// determines if the app instance is ready upon bootstrap
appInstanceReady: boolean;
/**
* Ability for classes to initialize app event handling early even before the app instance is ready during boot cycle avoiding boot race conditions
* @param callback wire up any global event handling inside the callback
*/
addEventWiring(callback: () => void): void;
declare module globalThis {
var NativeScriptHasInitGlobal: boolean;
var NativeScriptGlobals: {
/**
* Global framework event handling
*/
events: {
[Key in keyof import('data/observable').Observable]: import('data/observable').Observable[Key];
};
android?: any;
require(id: string): any;
moduleMerge(sourceExports: any, destExports: any): void;
registerModule(name: string, loader: (name: string) => any): void;
/**
* Register all modules from a webpack context.
* The context is one created using the following webpack utility:
* https://webpack.js.org/guides/dependency-management/#requirecontext
*
* The extension map is optional, modules in the webpack context will have their original file extension (e.g. may be ".ts" or ".scss" etc.),
* while the built-in module builders in {N} will look for ".js", ".css" or ".xml" files. Adding a map such as:
* ```
* { ".ts": ".js" }
* ```
* Will resolve lookups for .js to the .ts file.
* By default scss and ts files are mapped.
*/
registerWebpackModules(context: { keys(): string[]; (key: string): any }, extensionMap?: { [originalFileExtension: string]: string });
launched: boolean;
// used by various classes to setup callbacks to wire up global app event handling when the app instance is ready
appEventWiring: Array<any>;
// determines if the app instance is ready upon bootstrap
appInstanceReady: boolean;
/**
* The NativeScript XML builder, style-scope, application modules use various resources such as:
* app.css, page.xml files and modules during the application life-cycle.
* The moduleResolvers can be used to provide additional mechanisms to locate such resources.
* For example:
* ```
* global.moduleResolvers.unshift(uri => uri === "main-page" ? require("main-page") : null);
* ```
* More advanced scenarios will allow for specific bundlers to integrate their module resolving mechanisms.
* When adding resolvers at the start of the array, avoid throwing and return null instead so subsequent resolvers may try to resolve the resource.
* By default the only member of the array is global.require, as last resort - if it fails to find a module it will throw.
* Ability for classes to initialize app event handling early even before the app instance is ready during boot cycle avoiding boot race conditions
* @param callback wire up any global event handling inside the callback
*/
readonly moduleResolvers: ModuleResolver[];
addEventWiring(callback: () => void): void;
};
// var android: any;
function require(id: string): any;
/**
*
* @param name Name of the module to be loaded
* @param loadForUI Is this UI module is being loaded for UI from @nativescript/core/ui/builder.
* Xml, css/scss and js/ts modules for pages and custom-components should load with loadForUI=true.
* Passing "true" will enable the HMR mechanics this module. Default value is false.
*/
loadModule(name: string, loadForUI?: boolean): any;
function moduleMerge(sourceExports: any, destExports: any): void;
/**
* Checks if the module has been registered with `registerModule` or in `registerWebpackModules`
* @param name Name of the module
*/
moduleExists(name: string): boolean;
function registerModule(name: string, loader: (name: string) => any): void;
/**
* Register all modules from a webpack context.
* The context is one created using the following webpack utility:
* https://webpack.js.org/guides/dependency-management/#requirecontext
*
* The extension map is optional, modules in the webpack context will have their original file extension (e.g. may be ".ts" or ".scss" etc.),
* while the built-in module builders in {N} will look for ".js", ".css" or ".xml" files. Adding a map such as:
* ```
* { ".ts": ".js" }
* ```
* Will resolve lookups for .js to the .ts file.
* By default scss and ts files are mapped.
*/
function registerWebpackModules(context: { keys(): string[]; (key: string): any }, extensionMap?: { [originalFileExtension: string]: string });
getRegisteredModules(): string[];
/**
* The NativeScript XML builder, style-scope, application modules use various resources such as:
* app.css, page.xml files and modules during the application life-cycle.
* The moduleResolvers can be used to provide additional mechanisms to locate such resources.
* For example:
* ```
* global.moduleResolvers.unshift(uri => uri === "main-page" ? require("main-page") : null);
* ```
* More advanced scenarios will allow for specific bundlers to integrate their module resolving mechanisms.
* When adding resolvers at the start of the array, avoid throwing and return null instead so subsequent resolvers may try to resolve the resource.
* By default the only member of the array is global.require, as last resort - if it fails to find a module it will throw.
*/
var moduleResolvers: ModuleResolver[];
_unregisterModule(name: string): void;
/**
*
* @param name Name of the module to be loaded
* @param loadForUI Is this UI module is being loaded for UI from @nativescript/core/ui/builder.
* Xml, css/scss and js/ts modules for pages and custom-components should load with loadForUI=true.
* Passing "true" will enable the HMR mechanics this module. Default value is false.
*/
function loadModule(name: string, loadForUI?: boolean): any;
_isModuleLoadedForUI(moduleName: string): boolean;
/**
* Checks if the module has been registered with `registerModule` or in `registerWebpackModules`
* @param name Name of the module
*/
function moduleExists(name: string): boolean;
onGlobalLayoutListener: any;
zonedCallback(callback: Function): Function;
Reflect?: any;
Deprecated(target: Object, key?: string | symbol, descriptor?: any): any;
Experimental(target: Object, key?: string | symbol, descriptor?: any): any;
function getRegisteredModules(): string[];
__native?: any;
__inspector?: any;
__extends: any;
__onLiveSync: (context?: { type: string; path: string }) => void;
__onLiveSyncCore: (context?: { type: string; path: string }) => void;
__onUncaughtError: (error: NativeScriptError) => void;
__onDiscardedError: (error: NativeScriptError) => void;
__snapshot?: boolean;
TNS_WEBPACK?: boolean;
isIOS?: boolean;
isAndroid?: boolean;
__requireOverride?: (name: string, dir: string) => any;
function _unregisterModule(name: string): void;
// used to get the rootlayout instance to add/remove childviews
rootLayout: any;
}
function _isModuleLoadedForUI(moduleName: string): boolean;
var onGlobalLayoutListener: any;
function zonedCallback(callback: Function): Function;
var Reflect: any;
function Deprecated(target: Object, key?: string | symbol, descriptor?: any): any;
function Experimental(target: Object, key?: string | symbol, descriptor?: any): any;
var __native: any;
var __inspector: any;
var __extends: any;
var __onLiveSync: (context?: { type: string; path: string }) => void;
var __onLiveSyncCore: (context?: { type: string; path: string }) => void;
var __onUncaughtError: (error: NativeScriptError) => void;
var __onDiscardedError: (error: NativeScriptError) => void;
var __snapshot: boolean;
var TNS_WEBPACK: boolean;
var isIOS: boolean;
var isAndroid: boolean;
var isDisplayedEventFired: boolean;
var autoLoadPolyfills: boolean;
var __requireOverride: (name: string, dir: string) => any;
// used to get the rootlayout instance to add/remove childviews
var rootLayout: any;
}
declare const __DEV__: boolean;
declare const __CSS_PARSER__: string;

View File

@@ -115,8 +115,8 @@ export function initGlobal() {
// ts-helpers
// Required by V8 snapshot generator
if (!(<any>global).__extends) {
(<any>global).__extends = function (d, b) {
if (!global.__extends) {
global.__extends = function (d, b) {
for (const p in b) {
if (b.hasOwnProperty(p)) {
d[p] = b[p];
@@ -160,7 +160,7 @@ export function initGlobal() {
};
// Cast to <any> because moduleResolvers is read-only in definitions
(<any>global).moduleResolvers = [global.require];
global.moduleResolvers = [global.require];
global.registerModule = function (name: string, loader: ModuleLoader): void {
modules.set(name, { loader, moduleId: name });
@@ -250,7 +250,7 @@ export function initGlobal() {
return result;
}
for (const resolver of (<any>global).moduleResolvers) {
for (const resolver of global.moduleResolvers) {
const result = resolver(name);
if (result) {
modules.set(name, { moduleId: name, loader: () => result });
@@ -276,19 +276,19 @@ export function initGlobal() {
};
global.zonedCallback = function (callback: Function): Function {
if ((<any>global).zone) {
if (global.zone) {
// Zone v0.5.* style callback wrapping
return (<any>global).zone.bind(callback);
return global.zone.bind(callback);
}
if ((<any>global).Zone) {
if (global.Zone) {
// Zone v0.6.* style callback wrapping
return (<any>global).Zone.current.wrap(callback);
return global.Zone.current.wrap(callback);
} else {
return callback;
}
};
(<any>global).System = {
global.System = {
import(path) {
return new Promise((resolve, reject) => {
try {

View File

@@ -17,6 +17,8 @@ export declare const Application: {
suspendEvent: string;
resumeEvent: string;
exitEvent: string;
foregroundEvent: string;
backgroundEvent: string;
lowMemoryEvent: string;
orientationChangedEvent: string;
systemAppearanceChangedEvent: string;

View File

@@ -6,7 +6,7 @@ export { iOSApplication, AndroidApplication } from './application';
export type { ApplicationEventData, LaunchEventData, OrientationChangedEventData, UnhandledErrorEventData, DiscardedErrorEventData, CssChangedEventData, LoadAppCSSEventData, AndroidActivityEventData, AndroidActivityBundleEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData, AndroidActivityNewIntentEventData, AndroidActivityBackPressedEventData, SystemAppearanceChangedEventData } from './application';
import { fontScaleChangedEvent, launchEvent, displayedEvent, uncaughtErrorEvent, discardedErrorEvent, suspendEvent, resumeEvent, exitEvent, lowMemoryEvent, orientationChangedEvent, systemAppearanceChanged, systemAppearanceChangedEvent, getMainEntry, getRootView, _resetRootView, getResources, setResources, setCssFileName, getCssFileName, loadAppCss, addCss, on, off, notify, hasListeners, run, orientation, getNativeApplication, hasLaunched, android as appAndroid, ios as iosApp, systemAppearance, setAutoSystemAppearanceChanged, ensureNativeApplication, setMaxRefreshRate } from './application';
import { inBackground, suspended } from './application/application-common';
import { inBackground, suspended, foregroundEvent, backgroundEvent } from './application/application-common';
export const Application = {
launchEvent,
@@ -16,6 +16,8 @@ export const Application = {
suspendEvent,
resumeEvent,
exitEvent,
foregroundEvent,
backgroundEvent,
lowMemoryEvent,
orientationChangedEvent,
systemAppearanceChangedEvent,

View File

@@ -1,5 +1,7 @@
// @ts-nocheck
global.WeakRef.prototype.get = global.WeakRef.prototype.deref;
global.NativeClass = function () {};
global.NSObject = class NSObject {};
global.NSString = {
stringWithString() {
return {

View File

Binary file not shown.

View File

@@ -2,13 +2,13 @@
declare let __startCPUProfiler: any;
declare let __stopCPUProfiler: any;
export function uptime() {
return global.android ? (<any>org).nativescript.Process.getUpTime() : (<any>global).__tns_uptime();
export function uptime(): number {
return global.android ? (<any>org).nativescript.Process.getUpTime() : global.__tns_uptime();
}
export function log(message: string, ...optionalParams: any[]): void {
if ((<any>global).__nslog) {
(<any>global).__nslog('CONSOLE LOG: ' + message);
if (global.__nslog) {
global.__nslog('CONSOLE LOG: ' + message);
}
console.log(message, ...optionalParams);
}
@@ -31,7 +31,7 @@ const timers: { [index: string]: TimerInfo } = {};
const anyGlobal = <any>global;
const profileNames: string[] = [];
export const time = (<any>global).__time || Date.now;
export const time = (global.__time || Date.now) as () => number;
export function start(name: string): void {
let info = timers[name];

View File

@@ -4,7 +4,6 @@
"noEmitOnError": true,
"noEmitHelpers": true,
"declaration": true,
"noImplicitAny": false,
"noImplicitUseStrict": true,
"removeComments": false,
"emitDecoratorMetadata": true,

View File

@@ -34,7 +34,7 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate {
public nextAnimation: Function;
// The CAAnimationDelegate protocol has been introduced in the iOS 10 SDK
static ObjCProtocols = (<any>global).CAAnimationDelegate ? [(<any>global).CAAnimationDelegate] : [];
static ObjCProtocols = global.CAAnimationDelegate ? [global.CAAnimationDelegate] : [];
private _finishedCallback: Function;
private _propertyAnimation: PropertyAnimationInfo;

View File

@@ -211,7 +211,6 @@ export class View extends ViewCommon implements ViewDefinition {
const boundsOrigin = nativeView.bounds.origin;
const boundsFrame = adjustedFrame || frame;
nativeView.bounds = CGRectMake(boundsOrigin.x, boundsOrigin.y, boundsFrame.size.width, boundsFrame.size.height);
nativeView.layoutIfNeeded();
this._raiseLayoutChangedEvent();
this._isLaidOut = true;
@@ -889,6 +888,9 @@ export class View extends ViewCommon implements ViewDefinition {
}
_setNativeClipToBounds() {
if (!this.nativeViewProtected) {
return;
}
const backgroundInternal = this.style.backgroundInternal;
this.nativeViewProtected.clipsToBounds = (this.nativeViewProtected instanceof UIScrollView || backgroundInternal.hasBorderWidth() || backgroundInternal.hasBorderRadius()) && !backgroundInternal.hasBoxShadow();
}

View File

@@ -14,9 +14,51 @@ const majorVersion = iOSNativeHelper.MajorVersion;
class UILayoutViewController extends UIViewController {
public owner: WeakRef<View>;
private _isRunningLayout: number;
private get isRunningLayout() {
return this._isRunningLayout !== 0;
}
private startRunningLayout() {
this._isRunningLayout++;
}
private finishRunningLayout() {
this._isRunningLayout--;
this.clearScheduledLayout();
}
private runLayout(cb: () => void) {
try {
this.startRunningLayout();
cb();
} finally {
this.finishRunningLayout();
}
}
layoutTimer: number;
private clearScheduledLayout() {
if (this.layoutTimer) {
clearTimeout(this.layoutTimer);
this.layoutTimer = null;
}
}
private scheduleLayout() {
if (this.layoutTimer) {
return;
}
setTimeout(() => {
this.layoutTimer = null;
if (!this.isRunningLayout) {
this.runLayout(() => this.layoutOwner());
}
});
}
public static initWithOwner(owner: WeakRef<View>): UILayoutViewController {
const controller = <UILayoutViewController>UILayoutViewController.new();
controller.owner = owner;
controller._isRunningLayout = 0;
return controller;
}
@@ -29,6 +71,11 @@ class UILayoutViewController extends UIViewController {
this.extendedLayoutIncludesOpaqueBars = true;
}
public viewSafeAreaInsetsDidChange(): void {
super.viewSafeAreaInsetsDidChange();
this.scheduleLayout();
}
public viewWillLayoutSubviews(): void {
super.viewWillLayoutSubviews();
const owner = this.owner?.deref();
@@ -38,56 +85,69 @@ class UILayoutViewController extends UIViewController {
}
public viewDidLayoutSubviews(): void {
this.startRunningLayout();
super.viewDidLayoutSubviews();
this.layoutOwner();
this.finishRunningLayout();
}
layoutOwner(force = false) {
const owner = this.owner?.deref();
if (owner) {
if (majorVersion >= 11) {
// Handle nested UILayoutViewController safe area application.
// Currently, UILayoutViewController can be nested only in a TabView.
// The TabView itself is handled by the OS, so we check the TabView's parent (usually a Page, but can be a Layout).
const tabViewItem = owner.parent;
const tabView = tabViewItem && tabViewItem.parent;
let parent = tabView && tabView.parent;
if (!owner) {
return;
}
if (!force && owner.isLayoutValid && !owner.nativeViewProtected?.layer.needsLayout?.()) {
// we skip layout if the view is not yet laid out yet
// this usually means that viewDidLayoutSubviews will be called again
// so doing a layout pass now will layout with the wrong parameters
return;
}
// Handle Angular scenario where TabView is in a ProxyViewContainer
// It is possible to wrap components in ProxyViewContainers indefinitely
// Not using instanceof ProxyViewContainer to avoid circular dependency
// TODO: Try moving UILayoutViewController out of view module
while (parent && !parent.nativeViewProtected) {
parent = parent.parent;
}
if (majorVersion >= 11) {
// Handle nested UILayoutViewController safe area application.
// Currently, UILayoutViewController can be nested only in a TabView.
// The TabView itself is handled by the OS, so we check the TabView's parent (usually a Page, but can be a Layout).
const tabViewItem = owner.parent;
const tabView = tabViewItem && tabViewItem.parent;
let parent = tabView && tabView.parent;
if (parent) {
const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top;
const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom;
let currentInsetsTop = this.view.safeAreaInsets.top;
let currentInsetsBottom = this.view.safeAreaInsets.bottom;
// Safe area insets include additional safe area insets too, so subtract old values
if (this.additionalSafeAreaInsets) {
currentInsetsTop -= this.additionalSafeAreaInsets.top;
currentInsetsBottom -= this.additionalSafeAreaInsets.bottom;
}
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0);
if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) {
const additionalInsets = new UIEdgeInsets({
top: additionalInsetsTop,
left: 0,
bottom: additionalInsetsBottom,
right: 0,
});
this.additionalSafeAreaInsets = additionalInsets;
} else {
this.additionalSafeAreaInsets = null;
}
}
// Handle Angular scenario where TabView is in a ProxyViewContainer
// It is possible to wrap components in ProxyViewContainers indefinitely
// Not using instanceof ProxyViewContainer to avoid circular dependency
// TODO: Try moving UILayoutViewController out of view module
while (parent && !parent.nativeViewProtected) {
parent = parent.parent;
}
IOSHelper.layoutView(this, owner);
if (parent) {
const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top;
const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom;
let currentInsetsTop = this.view.safeAreaInsets.top;
let currentInsetsBottom = this.view.safeAreaInsets.bottom;
// Safe area insets include additional safe area insets too, so subtract old values
if (this.additionalSafeAreaInsets) {
currentInsetsTop -= this.additionalSafeAreaInsets.top;
currentInsetsBottom -= this.additionalSafeAreaInsets.bottom;
}
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0);
if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) {
const additionalInsets = new UIEdgeInsets({
top: additionalInsetsTop,
left: 0,
bottom: additionalInsetsBottom,
right: 0,
});
this.additionalSafeAreaInsets = additionalInsets;
} else {
this.additionalSafeAreaInsets = null;
}
}
}
IOSHelper.layoutView(this, owner);
}
public viewWillAppear(animated: boolean): void {

View File

@@ -254,6 +254,7 @@ function applySelectors<T extends View>(view: T, callback: (view: T) => void) {
if (currentPage) {
const styleScope = currentPage._styleScope;
if (styleScope) {
view.parent = currentPage;
view._inheritStyleScope(styleScope);
view.onLoaded();
callback(view);

View File

@@ -3,7 +3,7 @@
*/
import { DialogOptions, ConfirmOptions, PromptOptions, PromptResult, LoginOptions, LoginResult, ActionOptions } from './dialogs-common';
import { getLabelColor, getButtonColors, isDialogOptions, inputType, capitalizationType, DialogStrings, parseLoginOptions } from './dialogs-common';
import { android as androidApp } from '../../application';
import { ad } from '../../utils/native-helper';
export * from './dialogs-common';
@@ -12,7 +12,7 @@ function isString(value): value is string {
}
function createAlertDialog(options?: DialogOptions): android.app.AlertDialog.Builder {
const alert = new android.app.AlertDialog.Builder(androidApp.foregroundActivity, options.theme ? options.theme : -1);
const alert = new android.app.AlertDialog.Builder(ad.getCurrentActivity(), options.theme ? options.theme : -1);
alert.setTitle(options && isString(options.title) ? options.title : '');
alert.setMessage(options && isString(options.message) ? options.message : '');
if (options && options.cancelable === false) {
@@ -202,7 +202,7 @@ export function prompt(...args): Promise<PromptResult> {
try {
const alert = createAlertDialog(options);
const input = new android.widget.EditText(androidApp.foregroundActivity);
const input = new android.widget.EditText(ad.getCurrentActivity());
if (options) {
if (options.inputType === inputType.password) {
@@ -257,23 +257,21 @@ export function login(...args: any[]): Promise<LoginResult> {
return new Promise<LoginResult>((resolve, reject) => {
try {
const context = androidApp.foregroundActivity;
const alert = createAlertDialog(options);
const userNameInput = new android.widget.EditText(context);
const userNameInput = new android.widget.EditText(ad.getApplicationContext());
userNameInput.setHint(options.userNameHint ? options.userNameHint : '');
userNameInput.setText(options.userName ? options.userName : '');
const passwordInput = new android.widget.EditText(context);
const passwordInput = new android.widget.EditText(ad.getApplicationContext());
passwordInput.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
passwordInput.setTypeface(android.graphics.Typeface.DEFAULT);
passwordInput.setHint(options.passwordHint ? options.passwordHint : '');
passwordInput.setText(options.password ? options.password : '');
const layout = new android.widget.LinearLayout(context);
const layout = new android.widget.LinearLayout(ad.getApplicationContext());
layout.setOrientation(1);
layout.addView(userNameInput);
layout.addView(passwordInput);
@@ -324,8 +322,7 @@ export function action(...args): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
const activity = androidApp.foregroundActivity || androidApp.startActivity;
const alert = new android.app.AlertDialog.Builder(activity, options.theme ? options.theme : -1);
const alert = new android.app.AlertDialog.Builder(ad.getCurrentActivity(), options.theme ? options.theme : -1);
const message = options && isString(options.message) ? options.message : '';
const title = options && isString(options.title) ? options.title : '';
if (options && options.cancelable === false) {

View File

@@ -51,9 +51,7 @@ placeholderColorProperty.register(Style);
const keyboardTypeConverter = makeParser<CoreTypes.KeyboardInputType>(makeValidator<CoreTypes.KeyboardInputType>(CoreTypes.KeyboardType.datetime, CoreTypes.KeyboardType.phone, CoreTypes.KeyboardType.number, CoreTypes.KeyboardType.url, CoreTypes.KeyboardType.email, CoreTypes.KeyboardType.integer), true);
const autofillTypeConverter = makeParser<CoreTypes.AutofillType>(makeValidator<CoreTypes.AutofillType>(CoreTypes.AutofillType.username, CoreTypes.AutofillType.password, CoreTypes.AutofillType.none), true);
export const autofillTypeProperty = new Property<EditableTextBase, CoreTypes.AutofillType>({ name: 'autofillType', valueConverter: autofillTypeConverter });
export const autofillTypeProperty = new Property<EditableTextBase, CoreTypes.AutofillType>({ name: 'autofillType' });
autofillTypeProperty.register(EditableTextBase);
export const keyboardTypeProperty = new Property<EditableTextBase, CoreTypes.KeyboardInputType>({ name: 'keyboardType', valueConverter: keyboardTypeConverter });

View File

@@ -43,6 +43,14 @@ export class FrameBase extends CustomLayoutView {
public actionBarVisibility: 'auto' | 'never' | 'always';
public _currentEntry: BackstackEntry;
/**
* A reference of current page that is set earlier than current entry.
* Using this property, methods like 'eachChildView' and '_childrenCount' gain access to page view
* just in time for calls like '_addView' to perform view-tree iterations.
*/
public _resolvedPage: Page;
public _animationInProgress = false;
public _executingContext: NavigationContext;
public _isInFrameStack = false;
@@ -238,6 +246,8 @@ export class FrameBase extends CustomLayoutView {
// In case we navigated forward to a page that was in the backstack
// with clearHistory: true
if (!newPage.frame) {
this._resolvedPage = newPage;
this._addView(newPage);
newPage._frame = this;
}
@@ -456,7 +466,6 @@ export class FrameBase extends CustomLayoutView {
if (this._currentEntry) {
return this._currentEntry.resolvedPage;
}
return null;
}
@@ -508,7 +517,7 @@ export class FrameBase extends CustomLayoutView {
}
get _childrenCount(): number {
if (this.currentPage) {
if (this._resolvedPage) {
return 1;
}
@@ -516,7 +525,7 @@ export class FrameBase extends CustomLayoutView {
}
public eachChildView(callback: (child: View) => boolean) {
const page = this.currentPage;
const page = this._resolvedPage;
if (page) {
callback(page);
}

View File

@@ -21,6 +21,7 @@ import { Device } from '../../platform';
import { profile } from '../../profiling';
import { android as androidApplication } from '../../application';
import { setSuspended } from '../../application/application-common';
import { ad } from '../../utils/native-helper';
export * from './frame-common';
@@ -94,7 +95,7 @@ export class Frame extends FrameBase {
}
public static reloadPage(context?: ModuleContext): void {
const activity = application.android.foregroundActivity;
const activity = ad.getCurrentActivity();
const callbacks: AndroidActivityCallbacks = activity[CALLBACKS];
if (callbacks) {
const rootView: View = callbacks.getRootView();
@@ -147,7 +148,8 @@ export class Frame extends FrameBase {
// _onAttachedToWindow called from OS again after it was detach
// still happens with androidx.fragment:1.3.2
const lifecycleState = (androidApplication.foregroundActivity?.getLifecycle?.() || androidApplication.startActivity?.getLifecycle?.())?.getCurrentState() || androidx.lifecycle.Lifecycle.State.CREATED;
const activity = ad.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;
}
@@ -595,7 +597,7 @@ export function reloadPage(context?: ModuleContext): void {
}
// attach on global, so it can be overwritten in NativeScript Angular
(<any>global).__onLiveSyncCore = Frame.reloadPage;
global.__onLiveSyncCore = Frame.reloadPage;
function cloneExpandedTransitionListener(expandedTransitionListener: any) {
if (!expandedTransitionListener) {
@@ -913,6 +915,8 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
return null;
}
frame._resolvedPage = page;
if (page.parent === frame) {
// If we are navigating to a page that was destroyed
// reinitialize its UI.
@@ -920,6 +924,10 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
const context = (container && container.getContext()) || (inflater && inflater.getContext());
page._setupUI(context);
}
if (frame.isLoaded && !page.isLoaded) {
page.callLoaded();
}
} else {
if (!frame._styleScope) {
// Make sure page will have styleScope even if parents don't.
@@ -929,10 +937,6 @@ class FragmentCallbacksImplementation implements AndroidFragmentCallbacks {
frame._addView(page);
}
if (frame.isLoaded && !page.isLoaded) {
page.callLoaded();
}
const savedState = entry.viewSavedState;
if (savedState) {
(<android.view.View>page.nativeViewProtected).restoreHierarchyState(savedState);

View File

@@ -39,6 +39,7 @@ export class Label extends TextBase implements LabelDefinition {
const textView = this.nativeTextViewProtected;
textView.setSingleLine(true);
textView.setEllipsize(android.text.TextUtils.TruncateAt.END);
textView.setGravity(android.view.Gravity.CENTER_VERTICAL);
}
[whiteSpaceProperty.setNative](value: CoreTypes.WhiteSpaceType) {

View File

@@ -42,7 +42,9 @@ export class RootLayoutBase extends GridLayout {
// ability to add any view instance to composite views like layers
open(view: View, options: RootLayoutOptions = {}): Promise<void> {
return new Promise((resolve, reject) => {
const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
return new Promise<void>((resolve, reject) => {
if (!(view instanceof View)) {
return reject(new Error(`Invalid open view: ${view}`));
}
@@ -51,46 +53,57 @@ export class RootLayoutBase extends GridLayout {
return reject(new Error(`${view} has already been added`));
}
const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
resolve();
})
.then(() => {
// keep track of the views locally to be able to use their options later
this.popupViews.push({ view: view, options: options });
// keep track of the views locally to be able to use their options later
this.popupViews.push({ view: view, options: options });
if (options.shadeCover) {
// perf optimization note: we only need 1 layer of shade cover
// we just update properties if needed by additional overlaid views
if (this.shadeCover) {
// overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
this.updateShadeCover(this.shadeCover, options.shadeCover);
} else {
this.openShadeCover(options.shadeCover);
if (options.shadeCover) {
// perf optimization note: we only need 1 layer of shade cover
// we just update properties if needed by additional overlaid views
if (this.shadeCover) {
// overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
return this.updateShadeCover(this.shadeCover, options.shadeCover);
}
return this.openShadeCover(options.shadeCover);
}
}
})
.then(() => {
view.opacity = 0; // always begin with view invisible when adding dynamically
this.insertChild(view, this.getChildrenCount() + 1);
view.opacity = 0; // always begin with view invisible when adding dynamically
this.insertChild(view, this.getChildrenCount() + 1);
setTimeout(() => {
// only apply initial state and animate after the first tick - ensures safe areas and other measurements apply correctly
this.applyInitialState(view, enterAnimationDefinition);
this.getEnterAnimation(view, enterAnimationDefinition)
.play()
.then(() => {
this.applyDefaultState(view);
view.notify({ eventName: 'opened', object: view });
resolve();
})
.catch((ex) => {
reject(new Error(`Error playing enter animation: ${ex}`));
return new Promise((resolve, reject) => {
setTimeout(() => {
// only apply initial state and animate after the first tick - ensures safe areas and other measurements apply correctly
this.applyInitialState(view, enterAnimationDefinition);
this.getEnterAnimation(view, enterAnimationDefinition)
.play()
.then(() => {
this.applyDefaultState(view);
view.notify({ eventName: 'opened', object: view });
resolve();
})
.catch((ex) => {
reject(new Error(`Error playing enter animation: ${ex}`));
});
});
});
});
});
}
// optional animation parameter to overwrite close animation declared when opening popup
// ability to remove any view instance from composite views
close(view: View, exitTo?: TransitionAnimation): Promise<void> {
return new Promise((resolve, reject) => {
const cleanupAndFinish = () => {
view.notify({ eventName: 'closed', object: view });
this.removeChild(view);
};
// use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
let exitAnimationDefinition = exitTo;
return new Promise<void>((resolve, reject) => {
if (!(view instanceof View)) {
return reject(new Error(`Invalid close view: ${view}`));
}
@@ -99,51 +112,51 @@ export class RootLayoutBase extends GridLayout {
return reject(new Error(`Unable to close popup. ${view} not found`));
}
const popupIndex = this.getPopupIndex(view);
const poppedView = this.popupViews[popupIndex];
const cleanupAndFinish = () => {
view.notify({ eventName: 'closed', object: view });
this.removeChild(view);
resolve();
};
// use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
const exitAnimationDefinition = exitTo || poppedView?.options?.animation?.exitTo;
resolve();
})
.then(() => {
const popupIndex = this.getPopupIndex(view);
const poppedView = this.popupViews[popupIndex];
// Remove view from tracked popupviews
this.popupViews.splice(popupIndex, 1);
if (!exitAnimationDefinition) {
exitAnimationDefinition = poppedView?.options?.animation?.exitTo;
}
if (this.shadeCover) {
// update shade cover with the topmost popupView options (if not specifically told to ignore)
if (!poppedView?.options?.shadeCover?.ignoreShadeRestore) {
const shadeCoverOptions = this.popupViews[this.popupViews.length - 1]?.options?.shadeCover;
if (shadeCoverOptions) {
this.updateShadeCover(this.shadeCover, shadeCoverOptions);
// Remove view from tracked popupviews
this.popupViews.splice(popupIndex, 1);
if (this.shadeCover) {
// update shade cover with the topmost popupView options (if not specifically told to ignore)
if (!poppedView?.options?.shadeCover?.ignoreShadeRestore) {
const shadeCoverOptions = this.popupViews[this.popupViews.length - 1]?.options?.shadeCover;
if (shadeCoverOptions) {
return this.updateShadeCover(this.shadeCover, shadeCoverOptions);
}
}
// remove shade cover animation if this is the last opened popup view
if (this.popupViews.length === 0) {
return this.closeShadeCover(poppedView?.options?.shadeCover);
}
}
// remove shade cover animation if this is the last opened popup view
if (this.popupViews.length === 0) {
this.closeShadeCover(poppedView?.options?.shadeCover);
})
.then(() => {
if (exitAnimationDefinition) {
return this.getExitAnimation(view, exitAnimationDefinition)
.play()
.then(cleanupAndFinish.bind(this))
.catch((ex) => Promise.reject(new Error(`Error playing exit animation: ${ex}`)));
}
}
if (exitAnimationDefinition) {
this.getExitAnimation(view, exitAnimationDefinition)
.play()
.then(cleanupAndFinish.bind(this))
.catch((ex) => {
reject(new Error(`Error playing exit animation: ${ex}`));
});
} else {
cleanupAndFinish();
}
});
});
}
closeAll(): Promise<void[]> {
const toClose = [];
const views = this.popupViews.map((popupView) => popupView.view);
// Close all views at the same time and wait for all of them
while (this.popupViews.length > 0) {
toClose.push(this.close(this.popupViews[this.popupViews.length - 1].view));
for (const view of views) {
toClose.push(this.close(view));
}
return Promise.all(toClose);
}
@@ -342,7 +355,7 @@ export class RootLayoutBase extends GridLayout {
return shadeCover;
}
private updateShadeCover(shade: View, shadeOptions: ShadeCoverOptions = {}): void {
private updateShadeCover(shade: View, shadeOptions: ShadeCoverOptions = {}): Promise<void> {
if (shadeOptions.tapToClose !== undefined && shadeOptions.tapToClose !== null) {
shade.off('tap');
if (shadeOptions.tapToClose) {
@@ -351,7 +364,7 @@ export class RootLayoutBase extends GridLayout {
});
}
}
this._updateShadeCover(shade, shadeOptions);
return this._updateShadeCover(shade, shadeOptions);
}
private hasChild(view: View): boolean {

View File

@@ -74,7 +74,6 @@ class UIViewControllerImpl extends UIViewController {
public isBackstackSkipped: boolean;
public isBackstackCleared: boolean;
private didFirstLayout: boolean;
// this is initialized in initWithOwner since the constructor doesn't run on native classes
private _isRunningLayout: number;
private get isRunningLayout() {
@@ -85,7 +84,7 @@ class UIViewControllerImpl extends UIViewController {
}
private finishRunningLayout() {
this._isRunningLayout--;
this.didFirstLayout = true;
this.clearScheduledLayout();
}
private runLayout(cb: () => void) {
try {
@@ -96,11 +95,31 @@ class UIViewControllerImpl extends UIViewController {
}
}
layoutTimer: number;
private clearScheduledLayout() {
if (this.layoutTimer) {
clearTimeout(this.layoutTimer);
this.layoutTimer = null;
}
}
private scheduleLayout() {
if (this.layoutTimer) {
return;
}
setTimeout(() => {
this.layoutTimer = null;
if (!this.isRunningLayout) {
this.runLayout(() => this.layoutOwner());
}
});
}
public static initWithOwner(owner: WeakRef<Page>): UIViewControllerImpl {
const controller = <UIViewControllerImpl>UIViewControllerImpl.new();
controller._owner = owner;
controller._isRunningLayout = 0;
controller.didFirstLayout = false;
return controller;
}
@@ -120,7 +139,7 @@ class UIViewControllerImpl extends UIViewController {
return;
}
const frame = this.navigationController ? (<any>this.navigationController).owner : null;
const frame: Frame = this.navigationController ? (<any>this.navigationController).owner : null;
const newEntry = this[ENTRY];
// Don't raise event if currentPage was showing modal page.
@@ -130,6 +149,8 @@ class UIViewControllerImpl extends UIViewController {
}
if (frame) {
frame._resolvedPage = owner;
if (!owner.parent) {
owner._frame = frame;
if (!frame._styleScope) {
@@ -279,71 +300,77 @@ class UIViewControllerImpl extends UIViewController {
public viewSafeAreaInsetsDidChange(): void {
super.viewSafeAreaInsetsDidChange();
if (this.isRunningLayout || !this.didFirstLayout) {
return;
}
const owner = this._owner?.deref();
if (owner) {
this.runLayout(() => IOSHelper.layoutView(this, owner));
}
this.scheduleLayout();
}
public viewDidLayoutSubviews(): void {
this.startRunningLayout();
super.viewDidLayoutSubviews();
this.layoutOwner();
this.finishRunningLayout();
}
layoutOwner(force = false) {
const owner = this._owner?.deref();
if (owner) {
// layout(owner.actionBar)
// layout(owner.content)
if (!owner) {
return;
}
if (!force && owner.isLayoutValid && !owner.nativeViewProtected?.layer.needsLayout?.()) {
// we skip layout if the view is not yet laid out yet
// this usually means that viewDidLayoutSubviews will be called again
// so doing a layout pass now will layout with the wrong parameters
return;
}
if (majorVersion >= 11) {
// Handle nested Page safe area insets application.
// A Page is nested if its Frame has a parent.
// If the Page is nested, cross check safe area insets on top and bottom with Frame parent.
const frame = owner.parent;
// There is a legacy scenario where Page is not in a Frame - the root of a Modal View, so it has no parent.
let frameParent = frame && frame.parent;
// layout(owner.actionBar)
// layout(owner.content)
// Handle Angular scenario where TabView is in a ProxyViewContainer
// It is possible to wrap components in ProxyViewContainers indefinitely
// Not using instanceof ProxyViewContainer to avoid circular dependency
// TODO: Try moving UIViewControllerImpl out of page module
while (frameParent && !frameParent.nativeViewProtected) {
frameParent = frameParent.parent;
}
if (majorVersion >= 11) {
// Handle nested Page safe area insets application.
// A Page is nested if its Frame has a parent.
// If the Page is nested, cross check safe area insets on top and bottom with Frame parent.
const frame = owner.parent;
// There is a legacy scenario where Page is not in a Frame - the root of a Modal View, so it has no parent.
let frameParent = frame && frame.parent;
if (frameParent) {
const parentPageInsetsTop = frameParent.nativeViewProtected.safeAreaInsets.top;
const parentPageInsetsBottom = frameParent.nativeViewProtected.safeAreaInsets.bottom;
let currentInsetsTop = this.view.safeAreaInsets.top;
let currentInsetsBottom = this.view.safeAreaInsets.bottom;
// Safe area insets include additional safe area insets too, so subtract old values
if (this.additionalSafeAreaInsets) {
currentInsetsTop -= this.additionalSafeAreaInsets.top;
currentInsetsBottom -= this.additionalSafeAreaInsets.bottom;
}
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0);
if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) {
const additionalInsets = new UIEdgeInsets({
top: additionalInsetsTop,
left: 0,
bottom: additionalInsetsBottom,
right: 0,
});
this.additionalSafeAreaInsets = additionalInsets;
} else {
this.additionalSafeAreaInsets = null;
}
}
// Handle Angular scenario where TabView is in a ProxyViewContainer
// It is possible to wrap components in ProxyViewContainers indefinitely
// Not using instanceof ProxyViewContainer to avoid circular dependency
// TODO: Try moving UIViewControllerImpl out of page module
while (frameParent && !frameParent.nativeViewProtected) {
frameParent = frameParent.parent;
}
IOSHelper.layoutView(this, owner);
if (frameParent) {
const parentPageInsetsTop = frameParent.nativeViewProtected.safeAreaInsets.top;
const parentPageInsetsBottom = frameParent.nativeViewProtected.safeAreaInsets.bottom;
let currentInsetsTop = this.view.safeAreaInsets.top;
let currentInsetsBottom = this.view.safeAreaInsets.bottom;
// Safe area insets include additional safe area insets too, so subtract old values
if (this.additionalSafeAreaInsets) {
currentInsetsTop -= this.additionalSafeAreaInsets.top;
currentInsetsBottom -= this.additionalSafeAreaInsets.bottom;
}
const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0);
const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0);
if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) {
const additionalInsets = new UIEdgeInsets({
top: additionalInsetsTop,
left: 0,
bottom: additionalInsetsBottom,
right: 0,
});
this.additionalSafeAreaInsets = additionalInsets;
} else {
this.additionalSafeAreaInsets = null;
}
}
}
this.finishRunningLayout();
IOSHelper.layoutView(this, owner);
}
// Mind implementation for other controllerss

View File

@@ -33,6 +33,10 @@ export namespace ios {
const background = view.style.backgroundInternal;
const nativeView = <NativeScriptUIView>view.nativeViewProtected;
if (!nativeView) {
return;
}
if (background.clearFlags & BackgroundClearFlags.CLEAR_BOX_SHADOW) {
// clear box shadow if it has been removed!
view.setProperty('clipToBounds', true);

View File

@@ -1,9 +0,0 @@
import { KeyframeAnimationInfo, KeyframeDeclaration, KeyframeInfo, UnparsedKeyframe } from '../animation/keyframe-animation';
export class CssAnimationParser {
public static keyframeAnimationsFromCSSDeclarations(declarations: KeyframeDeclaration[]): KeyframeAnimationInfo[];
public static keyframesArrayFromCSS(keyframes: UnparsedKeyframe[]): KeyframeInfo[];
}
export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: KeyframeDeclaration[]): KeyframeDeclaration[];

View File

@@ -0,0 +1,523 @@
import { CoreTypes } from '../../core-types';
import type { KeyframeAnimationInfo, KeyframeInfo } from '../animation';
import { CssAnimationParser, keyframeAnimationsFromCSSProperty } from './css-animation-parser';
import { cssTreeParse } from '../../css/css-tree-parser';
describe('css-animation-parser', () => {
describe('shorthand-property-parser', () => {
// helper functions
function testSingleAnimation(css: string): KeyframeAnimationInfo {
const animations: KeyframeAnimationInfo[] = [];
keyframeAnimationsFromCSSProperty(css, animations);
return animations[0];
}
function testMultipleAnimations(css: string): KeyframeAnimationInfo[] {
const animations: KeyframeAnimationInfo[] = [];
keyframeAnimationsFromCSSProperty(css, animations);
return animations;
}
it('empty', () => {
const animation = testSingleAnimation('');
expect(animation).toBeUndefined();
});
// times to test for
const times = {
'0s': 0,
'0ms': 0,
'250ms': 250,
'0.5s': 500,
'1500ms': 1500,
'1s': 1000,
'3s': 3000,
};
const curves = {
ease: CoreTypes.AnimationCurve.ease,
linear: CoreTypes.AnimationCurve.linear,
'ease-in': CoreTypes.AnimationCurve.easeIn,
'ease-out': CoreTypes.AnimationCurve.easeOut,
'ease-in-out': CoreTypes.AnimationCurve.easeInOut,
spring: CoreTypes.AnimationCurve.spring,
'cubic-bezier(0.1, 1.0, 0.5, 0.5)': CoreTypes.AnimationCurve.cubicBezier(0.1, 1.0, 0.5, 0.5),
'cubic-bezier(0.42, 0.0, 1.0, 1.0);': CoreTypes.AnimationCurve.cubicBezier(0.42, 0.0, 1.0, 1.0),
};
it('parses duration', () => {
Object.entries(times).forEach(([timeString, ms]) => {
expect(testSingleAnimation(`${timeString}`).duration).toBe(ms);
});
});
it('parses delay', () => {
Object.entries(times).forEach(([timeString, ms]) => {
const animation = testSingleAnimation(`0s ${timeString}`);
expect(animation.duration).toBe(0);
expect(animation.delay).toBe(ms);
});
});
it('parses duration and delay', () => {
Object.entries(times).forEach(([timeString, ms]) => {
const animation = testSingleAnimation(`${timeString} ${timeString}`);
expect(animation.duration).toBe(ms);
expect(animation.delay).toBe(ms);
});
});
it('parses curve', () => {
Object.entries(curves).forEach(([curveString, curve]) => {
const animation = testSingleAnimation(`${curveString}`);
expect(animation.curve).toEqual(curve);
});
});
it('parses duration, curve and delay', () => {
Object.entries(curves).forEach(([curveString, curve]) => {
const animation1 = testSingleAnimation(`225ms 300ms ${curveString}`);
expect(animation1.duration).toBe(225);
expect(animation1.delay).toBe(300);
expect(animation1.curve).toEqual(curve);
// curve and delay can be swapped
const animation2 = testSingleAnimation(`225ms ${curveString} 300ms`);
expect(animation2.duration).toBe(225);
expect(animation2.delay).toBe(300);
expect(animation2.curve).toEqual(curve);
});
});
it('parses iteration count', () => {
expect(testSingleAnimation(`0s 0s ease 2`).iterations).toBe(2);
expect(testSingleAnimation(`0s 0s ease 2.5`).iterations).toBe(2.5);
expect(testSingleAnimation(`0s 0s ease infinite`).iterations).toBe(Infinity);
expect(testSingleAnimation(`2`).iterations).toBe(2);
expect(testSingleAnimation(`2.5`).iterations).toBe(2.5);
expect(testSingleAnimation(`infinite`).iterations).toBe(Infinity);
expect(testSingleAnimation(`1s 2`).iterations).toBe(2);
expect(testSingleAnimation(`1s 2.5`).iterations).toBe(2.5);
expect(testSingleAnimation(`1s infinite`).iterations).toBe(Infinity);
expect(testSingleAnimation(`ease 2`).iterations).toBe(2);
expect(testSingleAnimation(`ease 2.5`).iterations).toBe(2.5);
expect(testSingleAnimation(`ease infinite`).iterations).toBe(Infinity);
});
it('parses direction', () => {
expect(testSingleAnimation(`1s`).isReverse).toBe(false);
expect(testSingleAnimation(`1s normal`).isReverse).toBe(false);
expect(testSingleAnimation(`1s reverse`).isReverse).toBe(true);
expect(testSingleAnimation(`1s 1s reverse`).isReverse).toBe(true);
expect(testSingleAnimation(`1s 1s ease reverse`).isReverse).toBe(true);
expect(testSingleAnimation(`1s 1s ease 2 reverse`).isReverse).toBe(true);
expect(testSingleAnimation(`1s 1s ease infinite reverse`).isReverse).toBe(true);
expect(testSingleAnimation(`1s ease reverse`).isReverse).toBe(true);
expect(testSingleAnimation(`1s ease 1s reverse`).isReverse).toBe(true);
expect(testSingleAnimation(`1s ease 1s 2 reverse`).isReverse).toBe(true);
// unsupported values should still work
expect(testSingleAnimation(`1s alternate`).isReverse).toBe(false);
expect(testSingleAnimation(`1s alternate-reverse`).isReverse).toBe(false);
});
it('parses fill-mode', () => {
expect(testSingleAnimation(`1s`).isForwards).toBe(false);
expect(testSingleAnimation(`1s none`).isForwards).toBe(false);
expect(testSingleAnimation(`1s backwards`).isForwards).toBe(false);
expect(testSingleAnimation(`1s both`).isForwards).toBe(true);
expect(testSingleAnimation(`1s forwards`).isForwards).toBe(true);
expect(testSingleAnimation(`1s 1s forwards`).isForwards).toBe(true);
expect(testSingleAnimation(`1s 1s ease forwards`).isForwards).toBe(true);
expect(testSingleAnimation(`1s 1s ease 2 forwards`).isForwards).toBe(true);
expect(testSingleAnimation(`1s 1s ease infinite forwards`).isForwards).toBe(true);
expect(testSingleAnimation(`1s ease forwards`).isForwards).toBe(true);
expect(testSingleAnimation(`1s ease 1s forwards`).isForwards).toBe(true);
});
it('parses play-state', () => {
// TODO: implement play-state?
expect(testSingleAnimation(`1s`)).not.toBeUndefined();
expect(testSingleAnimation(`1s running`)).not.toBeUndefined();
expect(testSingleAnimation(`1s paused`)).not.toBeUndefined();
expect(testSingleAnimation(`1s 1s paused`)).not.toBeUndefined();
expect(testSingleAnimation(`1s 1s ease paused`)).not.toBeUndefined();
expect(testSingleAnimation(`1s 1s ease 2 paused`)).not.toBeUndefined();
expect(testSingleAnimation(`1s 1s ease infinite paused`)).not.toBeUndefined();
expect(testSingleAnimation(`1s ease paused`)).not.toBeUndefined();
expect(testSingleAnimation(`1s ease 1s paused`)).not.toBeUndefined();
});
it('parses animation name', () => {
expect(testSingleAnimation(`1s`).name).toBe('');
expect(testSingleAnimation(`1s fade`).name).toBe('fade');
expect(testSingleAnimation(`1s 'fade'`).name).toBe('fade');
expect(testSingleAnimation(`1s "fade"`).name).toBe('fade');
expect(testSingleAnimation(`1s fade-in`).name).toBe('fade-in');
expect(testSingleAnimation(`1s 'fade-in'`).name).toBe('fade-in');
expect(testSingleAnimation(`1s "fade-in"`).name).toBe('fade-in');
expect(testSingleAnimation(`1s fade_in`).name).toBe('fade_in');
expect(testSingleAnimation(`1s 'fade_in'`).name).toBe('fade_in');
expect(testSingleAnimation(`1s "fade_in"`).name).toBe('fade_in');
});
it('parses MDN example: 3s ease-in 1s 2 reverse both paused slidein', () => {
const animation = testSingleAnimation(`3s ease-in 1s 2 reverse both paused slidein`);
expect(animation.duration).toBe(3000);
expect(animation.delay).toBe(1000);
expect(animation.curve).toBe(CoreTypes.AnimationCurve.easeIn);
expect(animation.iterations).toBe(2);
expect(animation.isReverse).toBe(true);
expect(animation.isForwards).toBe(true);
expect(animation.name).toBe('slidein');
});
it('parses MDN example: 3s linear 1s slidein', () => {
const animation = testSingleAnimation(`3s linear 1s slidein`);
expect(animation.duration).toBe(3000);
expect(animation.delay).toBe(1000);
expect(animation.curve).toBe(CoreTypes.AnimationCurve.linear);
expect(animation.name).toBe('slidein');
});
it('parses MDN example: 3s linear slidein, 3s ease-out 5s slideout', () => {
const [animation1, animation2] = testMultipleAnimations(`3s linear slidein, 3s ease-out 5s slideout`);
expect(animation1.duration).toBe(3000);
expect(animation1.curve).toBe(CoreTypes.AnimationCurve.linear);
expect(animation1.name).toBe('slidein');
expect(animation2.duration).toBe(3000);
expect(animation2.delay).toBe(5000);
expect(animation2.curve).toBe(CoreTypes.AnimationCurve.easeOut);
expect(animation2.name).toBe('slideout');
});
it('parses SPEC example: 3s none backwards', () => {
const animation = testSingleAnimation(`3s none backwards`);
expect(animation.duration).toBe(3000);
expect(animation.isForwards).toBe(false);
expect(animation.name).toBe('backwards');
});
it('parses SPEC example: 3s backwards', () => {
const animation = testSingleAnimation(`3s backwards`);
expect(animation.duration).toBe(3000);
expect(animation.isForwards).toBe(false);
expect(animation.name).toBe('');
});
it('does not throw on invalid values', () => {
// prettier-ignore
const invalidValues = [
'asd',
'$#-1401;lk',
'1 1 1 1 1 1 1 1 1 1',
'1s 1s 1s 1s',
',,,,',
'$,1s-_1:s>',
Infinity.toString(),
NaN.toString(),
null,
undefined
];
invalidValues.forEach((value) => {
expect(() => testSingleAnimation(value)).not.toThrow();
});
});
});
describe('keyframe-parser', () => {
// helper function
function testKeyframesArrayFromCSS(css: string, expectedName?: string): KeyframeInfo[] {
const ast = cssTreeParse(css, 'test.css');
const rules = ast.stylesheet.rules;
const firstRule = rules[0];
expect(rules.length).toBe(1);
expect(firstRule.type).toBe('keyframes');
const name = firstRule.name;
const keyframes = firstRule.keyframes;
if (expectedName) {
expect(name).toBe(expectedName);
}
return CssAnimationParser.keyframesArrayFromCSS(keyframes);
}
it('parses "from" keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
from { opacity: 0; }
}`,
'fade'
);
expect(res.length).toBe(1);
const [from] = res;
expect(from.duration).toBe(0);
expect(from.declarations.length).toBe(1);
expect(from.declarations[0].property).toBe('opacity');
expect(from.declarations[0].value).toBe(0);
});
it('parses "to" keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
to { opacity: 1; }
}`,
'fade'
);
expect(res.length).toBe(1);
const [to] = res;
expect(to.duration).toBe(1);
expect(to.declarations.length).toBe(1);
expect(to.declarations[0].property).toBe('opacity');
expect(to.declarations[0].value).toBe(1);
});
it('parses "from/to" keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}`,
'fade'
);
expect(res.length).toBe(2);
const [from, to] = res;
expect(from.duration).toBe(0);
expect(from.declarations.length).toBe(1);
expect(from.declarations[0].property).toBe('opacity');
expect(from.declarations[0].value).toBe(0);
expect(to.duration).toBe(1);
expect(to.declarations.length).toBe(1);
expect(to.declarations[0].property).toBe('opacity');
expect(to.declarations[0].value).toBe(1);
});
it('parses "0%" keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
0% { opacity: 0; }
}`,
'fade'
);
expect(res.length).toBe(1);
const [from] = res;
expect(from.duration).toBe(0);
expect(from.declarations.length).toBe(1);
expect(from.declarations[0].property).toBe('opacity');
expect(from.declarations[0].value).toBe(0);
});
it('parses "100%" keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
100% { opacity: 1; }
}`,
'fade'
);
expect(res.length).toBe(1);
const [to] = res;
expect(to.duration).toBe(1);
expect(to.declarations.length).toBe(1);
expect(to.declarations[0].property).toBe('opacity');
expect(to.declarations[0].value).toBe(1);
});
it('parses "0%/100%" keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
0% { opacity: 0; }
100% { opacity: 1; }
}`,
'fade'
);
expect(res.length).toBe(2);
const [from, to] = res;
expect(from.duration).toBe(0);
expect(from.declarations.length).toBe(1);
expect(from.declarations[0].property).toBe('opacity');
expect(from.declarations[0].value).toBe(0);
expect(to.duration).toBe(1);
expect(to.declarations.length).toBe(1);
expect(to.declarations[0].property).toBe('opacity');
expect(to.declarations[0].value).toBe(1);
});
it('parses "via" keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
50% { opacity: 0.5; }
}`,
'fade'
);
expect(res.length).toBe(1);
const [via] = res;
expect(via.duration).toBe(0.5);
expect(via.declarations.length).toBe(1);
expect(via.declarations[0].property).toBe('opacity');
expect(via.declarations[0].value).toBe(0.5);
});
it('parses multiple keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
0% { opacity: 0; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}`,
'fade'
);
expect(res.length).toBe(3);
const [from, via, to] = res;
expect(from.duration).toBe(0);
expect(from.declarations.length).toBe(1);
expect(from.declarations[0].property).toBe('opacity');
expect(from.declarations[0].value).toBe(0);
expect(via.duration).toBe(0.5);
expect(via.declarations.length).toBe(1);
expect(via.declarations[0].property).toBe('opacity');
expect(via.declarations[0].value).toBe(0.5);
expect(to.duration).toBe(1);
expect(to.declarations.length).toBe(1);
expect(to.declarations[0].property).toBe('opacity');
expect(to.declarations[0].value).toBe(1);
});
it('parses multiple keyframes with mixed stops', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
from { opacity: 0; }
50% { opacity: 0.5; }
to { opacity: 1; }
}`,
'fade'
);
expect(res.length).toBe(3);
const [from, via, to] = res;
expect(from.duration).toBe(0);
expect(from.declarations.length).toBe(1);
expect(from.declarations[0].property).toBe('opacity');
expect(from.declarations[0].value).toBe(0);
expect(via.duration).toBe(0.5);
expect(via.declarations.length).toBe(1);
expect(via.declarations[0].property).toBe('opacity');
expect(via.declarations[0].value).toBe(0.5);
expect(to.duration).toBe(1);
expect(to.declarations.length).toBe(1);
expect(to.declarations[0].property).toBe('opacity');
expect(to.declarations[0].value).toBe(1);
});
it('parses duplicate keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
0% { opacity: 0; }
50% { opacity: 0.5; }
50% { translateX: 100; }
100% { opacity: 1; }
}`,
'fade'
);
expect(res.length).toBe(3);
const [from, via, to] = res;
expect(from.duration).toBe(0);
expect(from.declarations.length).toBe(1);
expect(from.declarations[0].property).toBe('opacity');
expect(from.declarations[0].value).toBe(0);
expect(via.duration).toBe(0.5);
expect(via.declarations.length).toBe(2);
expect(via.declarations[0].property).toBe('opacity');
expect(via.declarations[0].value).toBe(0.5);
expect(via.declarations[1].property).toBe('translateX');
expect(via.declarations[1].value).toBe(100);
expect(to.duration).toBe(1);
expect(to.declarations.length).toBe(1);
expect(to.declarations[0].property).toBe('opacity');
expect(to.declarations[0].value).toBe(1);
});
it('parses timing functions in keyframes', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
from { opacity: 0; animation-timing-function: ease-in; }
to { opacity: 1; }
}`,
'fade'
);
expect(res.length).toBe(2);
const [from, to] = res;
expect(from.curve).toBe(CoreTypes.AnimationCurve.easeIn);
expect(to.curve).not.toBeDefined();
});
it('sorts multiple keyframes with mixed order', () => {
const res = testKeyframesArrayFromCSS(
`@keyframes fade {
100% { opacity: 1; }
0% { opacity: 0; }
50% { opacity: 0.5; }
}`,
'fade'
);
expect(res.length).toBe(3);
const [from, via, to] = res;
expect(from.duration).toBe(0);
expect(from.declarations.length).toBe(1);
expect(from.declarations[0].property).toBe('opacity');
expect(from.declarations[0].value).toBe(0);
expect(via.duration).toBe(0.5);
expect(via.declarations.length).toBe(1);
expect(via.declarations[0].property).toBe('opacity');
expect(via.declarations[0].value).toBe(0.5);
expect(to.duration).toBe(1);
expect(to.declarations.length).toBe(1);
expect(to.declarations[0].property).toBe('opacity');
expect(to.declarations[0].value).toBe(1);
});
});
});

View File

@@ -4,15 +4,16 @@ import { KeyframeAnimationInfo, KeyframeDeclaration, KeyframeInfo, UnparsedKeyfr
import { timeConverter, animationTimingFunctionConverter } from '../styling/converters';
import { transformConverter } from '../styling/style-properties';
import { cleanupImportantFlags } from './css-utils';
const ANIMATION_PROPERTY_HANDLERS = Object.freeze({
'animation-name': (info: any, value: any) => (info.name = value),
'animation-name': (info: any, value: any) => (info.name = value.replace(/['"]/g, '')),
'animation-duration': (info: any, value: any) => (info.duration = timeConverter(value)),
'animation-delay': (info: any, value: any) => (info.delay = timeConverter(value)),
'animation-timing-function': (info: any, value: any) => (info.curve = animationTimingFunctionConverter(value)),
'animation-iteration-count': (info: any, value: any) => (info.iterations = value === 'infinite' ? Number.POSITIVE_INFINITY : parseFloat(value)),
'animation-direction': (info: any, value: any) => (info.isReverse = value === 'reverse'),
'animation-fill-mode': (info: any, value: any) => (info.isForwards = value === 'forwards'),
'animation-fill-mode': (info: any, value: any) => (info.isForwards = value === 'forwards' || value === 'both'),
});
export class CssAnimationParser {
@@ -64,6 +65,7 @@ export class CssAnimationParser {
if (current === undefined) {
current = <KeyframeInfo>{};
current.duration = time;
current.declarations = [];
parsedKeyframes[time] = current;
}
for (const declaration of keyframe.declarations) {
@@ -71,7 +73,7 @@ export class CssAnimationParser {
current.curve = animationTimingFunctionConverter(declaration.value);
}
}
current.declarations = declarations;
current.declarations = current.declarations.concat(declarations);
}
}
const array = [];
@@ -86,49 +88,108 @@ export class CssAnimationParser {
}
}
function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
if (typeof value === 'string') {
const values = value.split(/[,]+/);
for (const parsedValue of values) {
const animationInfo = new KeyframeAnimationInfo();
const arr = (<string>parsedValue).trim().split(/[ ]+/);
/**
* @see https://w3c.github.io/csswg-drafts/css-animations/#propdef-animation
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/animation
* @internal - exported for testing
* @param value
* @param animations
*/
export function keyframeAnimationsFromCSSProperty(value: any, animations: KeyframeAnimationInfo[]) {
if (typeof value !== 'string') {
return;
}
if (arr.length > 0) {
animationInfo.name = arr[0];
}
if (arr.length > 1) {
animationInfo.duration = timeConverter(arr[1]);
}
if (arr.length > 2) {
animationInfo.curve = animationTimingFunctionConverter(arr[2]);
}
if (arr.length > 3) {
animationInfo.delay = timeConverter(arr[3]);
}
if (arr.length > 4) {
animationInfo.iterations = parseInt(arr[4]);
}
if (arr.length > 5) {
animationInfo.isReverse = arr[4] === 'reverse';
}
if (arr.length > 6) {
animationInfo.isForwards = arr[5] === 'forwards';
}
if (arr.length > 7) {
throw new Error('Invalid value for animation: ' + value);
}
animations.push(animationInfo);
if (value.trim().length === 0) {
return;
}
/**
* Matches whitespace except if the whitespace is contained in parenthesis - ex. cubic-bezier(1, 1, 1, 1).
*/
const VALUE_SPLIT_RE = /\s(?![^(]*\))/;
/**
* Matches commas except if the comma is contained in parenthesis - ex. cubic-bezier(1, 1, 1, 1).
*/
const MULTIPLE_SPLIT_RE = /,(?![^(]*\))/;
const isTime = (v: string) => !!v.match(/\dm?s$/g);
const isTimingFunction = (v: string) => !!v.match(/ease|linear|ease-in|ease-out|ease-in-out|spring|cubic-bezier/g);
const isIterationCount = (v: string) => !!v.match(/infinite|[\d.]+$/g);
const isDirection = (v: string) => !!v.match(/normal|reverse|alternate|alternate-reverse/g);
const isFillMode = (v: string) => !!v.match(/none|forwards|backwards|both/g);
const isPlayState = (v: string) => !!v.match(/running|paused/g);
const values = value.split(MULTIPLE_SPLIT_RE);
for (const parsedValue of values) {
const animationInfo = new KeyframeAnimationInfo();
const parts = (<string>parsedValue).trim().split(VALUE_SPLIT_RE);
const [duration, delay] = parts.filter(isTime);
const [timing] = parts.filter(isTimingFunction);
const [iterationCount] = parts.filter(isIterationCount);
const [direction] = parts.filter(isDirection);
const [fillMode] = parts.filter(isFillMode);
const [playState] = parts.filter(isPlayState);
const [name] = parts.filter((v) => {
// filter out "consumed" values
return ![duration, delay, timing, iterationCount, direction, fillMode, playState].filter(Boolean).includes(v);
});
// console.log({
// duration,
// delay,
// timing,
// iterationCount,
// direction,
// fillMode,
// playState,
// name,
// });
if (duration) {
ANIMATION_PROPERTY_HANDLERS['animation-duration'](animationInfo, duration);
}
if (delay) {
ANIMATION_PROPERTY_HANDLERS['animation-delay'](animationInfo, delay);
}
if (timing) {
ANIMATION_PROPERTY_HANDLERS['animation-timing-function'](animationInfo, timing);
}
if (iterationCount) {
ANIMATION_PROPERTY_HANDLERS['animation-iteration-count'](animationInfo, iterationCount);
}
if (direction) {
ANIMATION_PROPERTY_HANDLERS['animation-direction'](animationInfo, direction);
}
if (fillMode) {
ANIMATION_PROPERTY_HANDLERS['animation-fill-mode'](animationInfo, fillMode);
}
if (playState) {
// TODO: implement play state? Currently not supported...
}
if (name) {
ANIMATION_PROPERTY_HANDLERS['animation-name'](animationInfo, name);
} else {
// based on the SPEC we should set the name to 'none' if no name is provided
// however we just don't set the name at all.
// perhaps we should set it to 'none' and handle it accordingly.
// animationInfo.name = 'none'
}
animations.push(animationInfo);
}
}
export function parseKeyframeDeclarations(unparsedKeyframeDeclarations: KeyframeDeclaration[]): KeyframeDeclaration[] {
const declarations = unparsedKeyframeDeclarations.reduce((declarations, { property: unparsedProperty, value: unparsedValue }) => {
const property = CssAnimationProperty._getByCssName(unparsedProperty);
unparsedValue = cleanupImportantFlags(unparsedValue, property?.cssLocalName);
if (typeof unparsedProperty === 'string' && property && property._valueConverter) {
if (typeof unparsedProperty === 'string' && property?._valueConverter) {
declarations[property.name] = property._valueConverter(<string>unparsedValue);
} else if (typeof unparsedValue === 'string' && unparsedProperty === 'transform') {
} else if (unparsedProperty === 'transform') {
const transformations = transformConverter(unparsedValue);
Object.assign(declarations, transformations);
}

View File

@@ -0,0 +1,12 @@
import { Trace } from '../../trace';
export function cleanupImportantFlags(value: string, propertyName: string) {
const index = value?.indexOf('!important');
if (index >= 0) {
if (Trace.isEnabled()) {
Trace.write(`The !important css rule is currently not supported. Property: ${propertyName}`, Trace.categories.Style, Trace.messageType.warn);
}
return value.substring(0, index).trim();
}
return value;
}

View File

@@ -91,7 +91,8 @@ export class Font extends FontBase {
getUIFont(defaultFont: UIFont): UIFont {
return getUIFontCached({
fontFamily: parseFontFamily(this.fontFamily),
fontSize: this.fontSize || defaultFont.pointSize,
// Apply a11y scale and calculate proper font size (avoid applying multiplier to native point size as it's messing calculations)
fontSize: this.fontSize ? this.fontSize * this.fontScale : defaultFont.pointSize,
fontWeight: getNativeFontWeight(this.fontWeight),
fontVariationSettings: this.fontVariationSettings,
isBold: this.isBold,

View File

@@ -1329,20 +1329,7 @@ fontFamilyProperty.register(Style);
export const fontScaleProperty = new InheritedCssProperty<Style, number>({
name: '_fontScale',
cssName: '_fontScale',
affectsLayout: global.isIOS,
valueChanged: (target, oldValue, newValue) => {
if (global.isIOS) {
if (target.viewRef['handleFontSize'] === true) {
return;
}
const currentFont = target.fontInternal || Font.default;
if (currentFont.fontScale !== newValue) {
const newFont = currentFont.withFontScale(newValue);
target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
}
}
},
defaultValue: 1.0,
valueConverter: (v) => parseFloat(v),
});
fontScaleProperty.register(Style);

View File

@@ -21,6 +21,7 @@ function ensureKeyframeAnimationModule() {
import * as capm from './css-animation-parser';
import { sanitizeModuleName } from '../builder/module-name-sanitizer';
import { resolveModuleName } from '../../module-name-resolver';
import { cleanupImportantFlags } from './css-utils';
let cssAnimationParserModule: typeof capm;
function ensureCssAnimationParserModule() {
@@ -563,7 +564,6 @@ export class CssState {
const view = this.viewRef.get();
if (!view) {
Trace.write(`${matchingSelectors} not set to view's property because ".viewRef" is cleared`, Trace.categories.Style, Trace.messageType.warn);
return;
}
@@ -579,7 +579,8 @@ export class CssState {
const replacementFunc = (g) => g[1].toUpperCase();
for (const property in newPropertyValues) {
const value = newPropertyValues[property];
const value = cleanupImportantFlags(newPropertyValues[property], property);
const isCssExp = isCssVariableExpression(value) || isCssCalcExpression(value);
if (isCssExp) {

View File

@@ -105,6 +105,9 @@ export class Style extends Observable implements StyleDefinition {
}
public fontInternal: Font;
/**
* This property ensures inheritance of a11y scale among views.
*/
public _fontScale: number;
public backgroundInternal: Background;

View File

@@ -4,16 +4,15 @@ import { CSSShadow } from '../styling/css-shadow';
// Requires
import { Font } from '../styling/font';
import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, letterSpacingProperty, lineHeightProperty, resetSymbol } from './text-base-common';
import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, letterSpacingProperty, lineHeightProperty, maxLinesProperty, resetSymbol } from './text-base-common';
import { Color } from '../../color';
import { FormattedString } from './formatted-string';
import { Span } from './span';
import { colorProperty, fontInternalProperty, Length } from '../styling/style-properties';
import { colorProperty, fontInternalProperty, fontScaleProperty, Length } from '../styling/style-properties';
import { isString, isNullOrUndefined } from '../../utils/types';
import { iOSNativeHelper } from '../../utils';
import { Trace } from '../../trace';
import { CoreTypes } from '../../core-types';
import { maxLinesProperty } from './text-base-common';
export * from './text-base-common';
@@ -188,7 +187,26 @@ export class TextBase extends TextBaseCommon {
if (!(value instanceof Font) || !this.formattedText) {
let nativeView = this.nativeTextViewProtected;
nativeView = nativeView instanceof UIButton ? nativeView.titleLabel : nativeView;
nativeView.font = value instanceof Font ? value.getUIFont(nativeView.font) : value;
if (value instanceof Font) {
// Apply a11y font scale if not set
if (value.fontScale !== this.style._fontScale) {
value.fontScale = this.style._fontScale;
}
nativeView.font = value.getUIFont(nativeView.font);
} else {
nativeView.font = value;
}
}
}
[fontScaleProperty.setNative](value: number) {
const nativeView = this.nativeTextViewProtected instanceof UIButton ? this.nativeTextViewProtected.titleLabel : this.nativeTextViewProtected;
const currentFont = this.style.fontInternal || Font.default.withFontSize(nativeView.font.pointSize);
if (currentFont.fontScale !== value) {
const newFont = currentFont.withFontScale(value);
this.style.fontInternal = newFont;
this.requestLayout();
}
}

View File

@@ -21,88 +21,6 @@ interface Owned {
}
//@endprivate
/**
* Module with android specific utilities.
*/
export namespace ad {
/**
* Gets the native Android application instance.
*/
export function getApplication(): any; /* android.app.Application */
/**
* Gets the Android application context.
*/
export function getApplicationContext(): any; /* android.content.Context */
/**
* Gets the native Android input method manager.
*/
export function getInputMethodManager(): any; /* android.view.inputmethod.InputMethodManager */
/**
* Hides the soft input method, usually a soft keyboard.
*/
export function dismissSoftInput(nativeView?: any /* android.view.View */): void;
/**
* Shows the soft input method, usually a soft keyboard.
*/
export function showSoftInput(nativeView: any /* android.view.View */): void;
/**
* Utility module dealing with some android collections.
*/
namespace collections {
/**
* Converts string array into a String [hash set](http://developer.android.com/reference/java/util/HashSet.html).
* @param str - An array of strings to convert.
*/
export function stringArrayToStringSet(str: string[]): any;
/**
* Converts string hash set into array of strings.
* @param stringSet - A string hash set to convert.
*/
export function stringSetToStringArray(stringSet: any): string[];
}
/**
* Utility module related to android resources.
*/
export namespace resources {
/**
* Gets the drawable id from a given name.
* @param name - Name of the resource.
*/
export function getDrawableId(name);
/**
* Gets the string id from a given name.
* @param name - Name of the resource.
*/
export function getStringId(name);
/**
* Gets the id from a given name.
* @param name - Name of the resource.
*/
export function getId(name: string): number;
/**
* [Obsolete - please use getPaletteColor] Gets a color from current theme.
* @param name - Name of the color
*/
export function getPalleteColor();
/**
* Gets a color from the current theme.
* @param name - Name of the color resource.
*/
export function getPaletteColor(name: string, context: any /* android.content.Context */): number;
}
}
/**
* An utility function that invokes garbage collection on the JavaScript side.
*/

View File

@@ -1,4 +1,4 @@
import { android as ad } from '../application';
import { ad } from './native-helper';
export function dispatchToMainThread(func: () => void) {
const runOnMainThread = (global as any).__runOnMainThread;
@@ -20,7 +20,7 @@ export function isMainThread(): boolean {
}
export function dispatchToUIThread(func: () => void) {
const activity: androidx.appcompat.app.AppCompatActivity = ad.foregroundActivity || ad.startActivity;
const activity: androidx.appcompat.app.AppCompatActivity = ad.getCurrentActivity();
if (activity && func) {
activity.runOnUiThread(
new java.lang.Runnable({

View File

@@ -397,7 +397,7 @@ function _generateAmpMap(): any {
}
// android-specific implementation, which pre-populates the map to get it saved into the heap blob
if ((<any>global).__snapshot) {
if (global.__snapshot) {
_ampCodes = _generateAmpMap();
}