diff --git a/apps/toolbox/src/pages/image-handling.ts b/apps/toolbox/src/pages/image-handling.ts
index 528712f8b1..71d1381b15 100644
--- a/apps/toolbox/src/pages/image-handling.ts
+++ b/apps/toolbox/src/pages/image-handling.ts
@@ -1,4 +1,4 @@
-import { Observable, EventData, Page, ImageSource, knownFolders, path } from '@nativescript/core';
+import { Observable, EventData, Page, ImageSource, knownFolders, path, ImageSymbolEffect } from '@nativescript/core';
import { create, ImagePickerMediaType } from '@nativescript/imagepicker';
let page: Page;
@@ -10,6 +10,32 @@ export function navigatingTo(args: EventData) {
export class DemoModel extends Observable {
addingPhoto = false;
+ symbolWiggleEffect: ImageSymbolEffect;
+ symbolBounceEffect: ImageSymbolEffect;
+ symbolBreathEffect: ImageSymbolEffect;
+ symbolRotateEffect: ImageSymbolEffect;
+
+ constructor() {
+ super();
+ if (__APPLE__) {
+ this.symbolWiggleEffect = {
+ effect: NSSymbolWiggleEffect.effect(),
+ start: true,
+ };
+ this.symbolBounceEffect = {
+ effect: NSSymbolBounceEffect.effect(),
+ start: true,
+ };
+ this.symbolBreathEffect = {
+ effect: NSSymbolBreatheEffect.effect(),
+ start: true,
+ };
+ this.symbolRotateEffect = {
+ effect: NSSymbolRotateEffect.effect(),
+ start: true,
+ };
+ }
+ }
pickImage() {
const context = create({
diff --git a/apps/toolbox/src/pages/image-handling.xml b/apps/toolbox/src/pages/image-handling.xml
index 0b67d295dd..ad735e19b8 100644
--- a/apps/toolbox/src/pages/image-handling.xml
+++ b/apps/toolbox/src/pages/image-handling.xml
@@ -5,9 +5,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/css/system-classes.ts b/packages/core/css/system-classes.ts
index 3c1edadcb9..a40e6a6607 100644
--- a/packages/core/css/system-classes.ts
+++ b/packages/core/css/system-classes.ts
@@ -6,6 +6,7 @@ export namespace CSSUtils {
export const CLASS_PREFIX = 'ns-';
export const MODAL_ROOT_VIEW_CSS_CLASS = `${CLASS_PREFIX}${MODAL}`;
export const ROOT_VIEW_CSS_CLASS = `${CLASS_PREFIX}${ROOT}`;
+ export const IgnoredCssDynamicAttributeTracking = new Set();
export function getSystemCssClasses(): string[] {
return cssClasses;
diff --git a/packages/core/image-source/index.android.ts b/packages/core/image-source/index.android.ts
index ca89f85247..ca36e9f4bd 100644
--- a/packages/core/image-source/index.android.ts
+++ b/packages/core/image-source/index.android.ts
@@ -149,6 +149,14 @@ export class ImageSource implements ImageSourceDefinition {
return ImageSource.fromFileSync(path);
}
+ static fromSystemImageSync(name: string): ImageSource {
+ return ImageSource.fromResourceSync(name);
+ }
+
+ static fromSystemImage(name: string): Promise {
+ return ImageSource.fromResource(name);
+ }
+
static fromDataSync(data: any): ImageSource {
const bitmap = android.graphics.BitmapFactory.decodeStream(data);
@@ -335,7 +343,7 @@ export class ImageSource implements ImageSourceDefinition {
reject();
}
},
- })
+ }),
);
});
}
@@ -375,7 +383,7 @@ export class ImageSource implements ImageSourceDefinition {
reject();
}
},
- })
+ }),
);
});
}
@@ -404,7 +412,7 @@ export class ImageSource implements ImageSourceDefinition {
reject();
}
},
- })
+ }),
);
});
}
diff --git a/packages/core/image-source/index.d.ts b/packages/core/image-source/index.d.ts
index 55037abc92..57e533595e 100644
--- a/packages/core/image-source/index.d.ts
+++ b/packages/core/image-source/index.d.ts
@@ -54,6 +54,18 @@ export class ImageSource {
*/
static fromResource(name: string): Promise;
+ /**
+ * Loads this instance from the specified system image name.
+ * @param name the name of the system image
+ */
+ static fromSystemImageSync(name: string): ImageSource;
+
+ /**
+ * Loads this instance from the specified system image name asynchronously.
+ * @param name the name of the system image
+ */
+ static fromSystemImage(name: string): Promise;
+
/**
* Loads this instance from the specified file.
* @param path The location of the file on the file system.
diff --git a/packages/core/image-source/index.ios.ts b/packages/core/image-source/index.ios.ts
index fd8fbf6df4..f6d854ec8a 100644
--- a/packages/core/image-source/index.ios.ts
+++ b/packages/core/image-source/index.ios.ts
@@ -8,7 +8,7 @@ import { Trace } from '../trace';
// Types.
import { path as fsPath, knownFolders } from '../file-system';
-import { isFileOrResourcePath, RESOURCE_PREFIX, layout, releaseNativeObject } from '../utils';
+import { isFileOrResourcePath, RESOURCE_PREFIX, layout, releaseNativeObject, SYSTEM_PREFIX } from '../utils';
import { getScaledDimensions } from './image-source-common';
@@ -73,6 +73,27 @@ export class ImageSource implements ImageSourceDefinition {
return http.getImage(url);
}
+ static fromSystemImageSync(name: string): ImageSource {
+ const image = UIImage.systemImageNamed(name);
+
+ return image ? new ImageSource(image) : null;
+ }
+
+ static fromSystemImage(name: string): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const image = UIImage.systemImageNamed(name);
+ if (image) {
+ resolve(new ImageSource(image));
+ } else {
+ reject(new Error(`Failed to load system icon with name: ${name}`));
+ }
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+ }
+
static fromResourceSync(name: string): ImageSource {
const nativeSource = (UIImage).tns_safeImageNamed(name) || (UIImage).tns_safeImageNamed(`${name}.jpg`);
@@ -126,7 +147,10 @@ export class ImageSource implements ImageSourceDefinition {
}
if (path.indexOf(RESOURCE_PREFIX) === 0) {
- return ImageSource.fromResourceSync(path.substr(RESOURCE_PREFIX.length));
+ return ImageSource.fromResourceSync(path.slice(RESOURCE_PREFIX.length));
+ }
+ if (path.indexOf(SYSTEM_PREFIX) === 0) {
+ return ImageSource.fromSystemImageSync(path.slice(SYSTEM_PREFIX.length));
}
return ImageSource.fromFileSync(path);
diff --git a/packages/core/package.json b/packages/core/package.json
index 4137a43172..816cdd412f 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@nativescript/core",
- "version": "8.7.3",
+ "version": "8.8.0-alpha.2",
"description": "A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.",
"main": "index",
"types": "index.d.ts",
diff --git a/packages/core/references.d.ts b/packages/core/references.d.ts
index 3f788d6246..93bd89d8d6 100644
--- a/packages/core/references.d.ts
+++ b/packages/core/references.d.ts
@@ -2,6 +2,7 @@
///
///
///
+///
///
///
///
diff --git a/packages/core/ui/image/image-common.ts b/packages/core/ui/image/image-common.ts
index 91bb9a1f99..1b22851a2e 100644
--- a/packages/core/ui/image/image-common.ts
+++ b/packages/core/ui/image/image-common.ts
@@ -4,7 +4,7 @@ import { booleanConverter } from '../core/view-base';
import { CoreTypes } from '../../core-types';
import { ImageAsset } from '../../image-asset';
import { ImageSource } from '../../image-source';
-import { isDataURI, isFontIconURI, isFileOrResourcePath, RESOURCE_PREFIX } from '../../utils';
+import { isDataURI, isFontIconURI, isFileOrResourcePath, RESOURCE_PREFIX, SYSTEM_PREFIX } from '../../utils';
import { Color } from '../../color';
import { Style } from '../styling/style';
import { Length } from '../styling/style-properties';
@@ -75,13 +75,21 @@ export abstract class ImageBase extends View implements ImageDefinition {
}
} else if (isFileOrResourcePath(value)) {
if (value.indexOf(RESOURCE_PREFIX) === 0) {
- const resPath = value.substr(RESOURCE_PREFIX.length);
+ const resPath = value.slice(RESOURCE_PREFIX.length);
if (sync) {
imageLoaded(ImageSource.fromResourceSync(resPath));
} else {
this.imageSource = null;
ImageSource.fromResource(resPath).then(imageLoaded);
}
+ } else if (value.indexOf(SYSTEM_PREFIX) === 0) {
+ const sysPath = value.slice(SYSTEM_PREFIX.length);
+ if (sync) {
+ imageLoaded(ImageSource.fromSystemImageSync(sysPath));
+ } else {
+ this.imageSource = null;
+ ImageSource.fromSystemImage(sysPath).then(imageLoaded);
+ }
} else {
if (sync) {
imageLoaded(ImageSource.fromFileSync(value));
@@ -178,3 +186,22 @@ export const decodeWidthProperty = new Property
valueConverter: Length.parse,
});
decodeWidthProperty.register(ImageBase);
+
+/**
+ * iOS only
+ * Symbol effects: https://developer.apple.com/documentation/symbols?language=objc
+ */
+export type ImageSymbolEffect = {
+ effect?: NSSymbolEffect;
+ options?: NSSymbolEffectOptions;
+ completion?: (context: UISymbolEffectCompletionContext) => void;
+ start?: boolean;
+};
+
+/**
+ * iOS only
+ */
+export const symbolEffectProperty = new Property({
+ name: 'symbolEffect',
+});
+symbolEffectProperty.register(ImageBase);
diff --git a/packages/core/ui/image/index.d.ts b/packages/core/ui/image/index.d.ts
index d142de3b6e..886fbbfcaa 100644
--- a/packages/core/ui/image/index.d.ts
+++ b/packages/core/ui/image/index.d.ts
@@ -6,6 +6,7 @@ import { Color } from '../../color';
import { Property, InheritedCssProperty } from '../core/properties';
import { CoreTypes } from '../../core-types';
+export type { ImageSymbolEffect } from './image-common';
/**
* Represents a class that provides functionality for loading and streching image(s).
*/
diff --git a/packages/core/ui/image/index.ios.ts b/packages/core/ui/image/index.ios.ts
index 2a3ae51c28..5eb00fbfb3 100644
--- a/packages/core/ui/image/index.ios.ts
+++ b/packages/core/ui/image/index.ios.ts
@@ -1,4 +1,4 @@
-import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty } from './image-common';
+import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty, symbolEffectProperty, ImageSymbolEffect } from './image-common';
import { ImageSource } from '../../image-source';
import { ImageAsset } from '../../image-asset';
import { Color } from '../../color';
@@ -194,4 +194,14 @@ export class Image extends ImageBase {
[srcProperty.setNative](value: string | ImageSource | ImageAsset) {
this._createImageSourceFromSrc(value);
}
+
+ [symbolEffectProperty.setNative](value: ImageSymbolEffect) {
+ if (this.nativeViewProtected) {
+ if (value?.start) {
+ this.nativeViewProtected.addSymbolEffectOptionsAnimatedCompletion(value.effect || NSSymbolScaleEffect.effect(), value.options || NSSymbolEffectOptions.optionsWithRepeating(), true, value.completion || null);
+ } else {
+ this.nativeViewProtected.removeAllSymbolEffects();
+ }
+ }
+ }
}
diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts
index 893eb4c336..61a226f038 100644
--- a/packages/core/ui/index.ts
+++ b/packages/core/ui/index.ts
@@ -31,7 +31,7 @@ export { GesturesObserver, TouchAction, GestureTypes, GestureStateTypes, SwipeDi
export type { GestureEventData, GestureEventDataWithState, TapGestureEventData, PanGestureEventData, PinchGestureEventData, RotationGestureEventData, SwipeGestureEventData, TouchGestureEventData, TouchAnimationOptions, VisionHoverOptions } from './gestures';
export { HtmlView } from './html-view';
-export { Image } from './image';
+export { Image, ImageSymbolEffect } from './image';
export { Cache as ImageCache } from './image-cache';
export type { DownloadError, DownloadRequest, DownloadedData } from './image-cache';
export { Label } from './label';
diff --git a/packages/core/ui/styling/css-selector.ts b/packages/core/ui/styling/css-selector.ts
index bccd3971dc..580b8b0df6 100644
--- a/packages/core/ui/styling/css-selector.ts
+++ b/packages/core/ui/styling/css-selector.ts
@@ -4,6 +4,7 @@ import { isNullOrUndefined } from '../../utils/types';
import * as ReworkCSS from '../../css';
import { Combinator as ICombinator, SimpleSelectorSequence as ISimpleSelectorSequence, Selector as ISelector, SimpleSelector as ISimpleSelector, parseSelector } from '../../css/parser';
+import { CSSUtils } from '../../css/system-classes';
/**
* An interface describing the shape of a type on which the selectors may apply.
@@ -675,6 +676,9 @@ export class SelectorsMatch implements ChangeAccumulator {
public selectors: SelectorCore[];
public addAttribute(node: T, attribute: string): void {
+ if (CSSUtils.IgnoredCssDynamicAttributeTracking.has(attribute)) {
+ return;
+ }
const deps: Changes = this.properties(node);
if (!deps.attributes) {
deps.attributes = new Set();
diff --git a/packages/core/ui/styling/style-scope.ts b/packages/core/ui/styling/style-scope.ts
index a71ff6f9ec..c933383db6 100644
--- a/packages/core/ui/styling/style-scope.ts
+++ b/packages/core/ui/styling/style-scope.ts
@@ -22,6 +22,7 @@ import * as capm from './css-animation-parser';
import { sanitizeModuleName } from '../../utils/common';
import { resolveModuleName } from '../../module-name-resolver';
import { cleanupImportantFlags } from './css-utils';
+import { Observable, PropertyChangeData } from '../../data/observable';
let cssAnimationParserModule: typeof capm;
function ensureCssAnimationParserModule() {
@@ -418,6 +419,8 @@ export class CssState {
_matchInvalid: boolean;
_playsKeyframeAnimations: boolean;
+ private _dynamicUpdateListenerMap: Map void> = new Map();
+
constructor(private viewRef: WeakRef) {
this._onDynamicStateChangeHandler = () => this.updateDynamicState();
}
@@ -650,9 +653,14 @@ export class CssState {
const changeMap = this._match.changeMap;
changeMap.forEach((changes, view) => {
if (changes.attributes) {
- changes.attributes.forEach((attribute) => {
- view.addEventListener(attribute + 'Change', this._onDynamicStateChangeHandler);
- });
+ const attributes = changes.attributes;
+ const listener = (args: PropertyChangeData) => {
+ if (attributes.has(args.propertyName)) {
+ this._onDynamicStateChangeHandler();
+ }
+ };
+ this._dynamicUpdateListenerMap.set(view, listener);
+ view.addEventListener(Observable.propertyChangeEvent, listener);
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach((pseudoClass) => {
@@ -669,10 +677,8 @@ export class CssState {
private unsubscribeFromDynamicUpdates(): void {
this._appliedChangeMap.forEach((changes, view) => {
- if (changes.attributes) {
- changes.attributes.forEach((attribute) => {
- view.removeEventListener(attribute + 'Change', this._onDynamicStateChangeHandler);
- });
+ if (this._dynamicUpdateListenerMap.has(view)) {
+ view.removeEventListener(Observable.propertyChangeEvent, this._dynamicUpdateListenerMap.get(view));
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach((pseudoClass) => {
@@ -684,6 +690,7 @@ export class CssState {
});
}
});
+ this._dynamicUpdateListenerMap.clear();
this._appliedChangeMap = CssState.emptyChangeMap;
}
diff --git a/packages/core/utils/common.ts b/packages/core/utils/common.ts
index 273f5eb90c..2f8f46379d 100644
--- a/packages/core/utils/common.ts
+++ b/packages/core/utils/common.ts
@@ -8,6 +8,7 @@ export * from './mainthread-helper';
export * from './macrotask-scheduler';
export const RESOURCE_PREFIX = 'res://';
+export const SYSTEM_PREFIX = 'sys://';
export const FILE_PREFIX = 'file:///';
export function escapeRegexSymbols(source: string): string {
@@ -75,7 +76,8 @@ export function isFileOrResourcePath(path: string): boolean {
return (
path.indexOf('~/') === 0 || // relative to AppRoot
path.indexOf('/') === 0 || // absolute path
- path.indexOf(RESOURCE_PREFIX) === 0
+ path.indexOf(RESOURCE_PREFIX) === 0 ||
+ path.indexOf(SYSTEM_PREFIX) === 0
); // resource
}
@@ -215,7 +217,7 @@ export function queueGC(delay = 900, useThrottle?: boolean) {
if (!throttledGC.get(delay)) {
throttledGC.set(
delay,
- throttle(() => GC(), delay)
+ throttle(() => GC(), delay),
);
}
throttledGC.get(delay)();
@@ -226,7 +228,7 @@ export function queueGC(delay = 900, useThrottle?: boolean) {
if (!debouncedGC.get(delay)) {
debouncedGC.set(
delay,
- debounce(() => GC(), delay)
+ debounce(() => GC(), delay),
);
}
debouncedGC.get(delay)();