diff --git a/apps/toolbox/src/pages/image-handling.ts b/apps/toolbox/src/pages/image-handling.ts
index 528712f8b1..563498f1fa 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, ImageSymbolEffects } from '@nativescript/core';
import { create, ImagePickerMediaType } from '@nativescript/imagepicker';
let page: Page;
@@ -10,6 +10,10 @@ export function navigatingTo(args: EventData) {
export class DemoModel extends Observable {
addingPhoto = false;
+ symbolWiggleEffect: ImageSymbolEffects.Wiggle;
+ symbolBounceEffect: ImageSymbolEffects.Bounce;
+ symbolBreathEffect: ImageSymbolEffects.Breathe;
+ symbolRotateEffect: ImageSymbolEffects.Rotate;
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/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/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..d430ac3a6a 100644
--- a/packages/core/ui/image/image-common.ts
+++ b/packages/core/ui/image/image-common.ts
@@ -4,12 +4,13 @@ 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';
import { Property, InheritedCssProperty } from '../core/properties';
import { Trace } from '../../trace';
+import { ImageSymbolEffect, ImageSymbolEffects } from './symbol-effects';
@CSSType('Image')
export abstract class ImageBase extends View implements ImageDefinition {
@@ -75,13 +76,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 +187,13 @@ export const decodeWidthProperty = new Property
valueConverter: Length.parse,
});
decodeWidthProperty.register(ImageBase);
+
+/**
+ * iOS only
+ */
+export const iosSymbolEffectProperty = new Property({
+ name: 'iosSymbolEffect',
+});
+iosSymbolEffectProperty.register(ImageBase);
+
+export { ImageSymbolEffect, ImageSymbolEffects };
diff --git a/packages/core/ui/image/index.d.ts b/packages/core/ui/image/index.d.ts
index d142de3b6e..1d1186d6e7 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 { ImageSymbolEffect, ImageSymbolEffects } 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..cf3d0d179c 100644
--- a/packages/core/ui/image/index.ios.ts
+++ b/packages/core/ui/image/index.ios.ts
@@ -1,9 +1,10 @@
-import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty } from './image-common';
+import { ImageBase, stretchProperty, imageSourceProperty, tintColorProperty, srcProperty, iosSymbolEffectProperty, ImageSymbolEffect, ImageSymbolEffects } from './image-common';
import { ImageSource } from '../../image-source';
import { ImageAsset } from '../../image-asset';
import { Color } from '../../color';
import { Trace } from '../../trace';
import { layout, queueGC } from '../../utils';
+import { SDK_VERSION } from '../../utils/constants';
export * from './image-common';
@@ -194,4 +195,16 @@ export class Image extends ImageBase {
[srcProperty.setNative](value: string | ImageSource | ImageAsset) {
this._createImageSourceFromSrc(value);
}
+
+ [iosSymbolEffectProperty.setNative](value: ImageSymbolEffect | ImageSymbolEffects) {
+ if (SDK_VERSION < 17) {
+ return;
+ }
+ const symbol = typeof value === 'string' ? ImageSymbolEffect.fromSymbol(value) : value;
+ if (symbol && symbol.effect) {
+ this.nativeViewProtected.addSymbolEffectOptionsAnimatedCompletion(symbol.effect, symbol.options || NSSymbolEffectOptions.optionsWithRepeating(), true, symbol.completion || null);
+ } else {
+ this.nativeViewProtected.removeAllSymbolEffects();
+ }
+ }
}
diff --git a/packages/core/ui/image/symbol-effects-common.ts b/packages/core/ui/image/symbol-effects-common.ts
new file mode 100644
index 0000000000..cc06002510
--- /dev/null
+++ b/packages/core/ui/image/symbol-effects-common.ts
@@ -0,0 +1,37 @@
+export enum ImageSymbolEffects {
+ Appear = 'appear',
+ AppearUp = 'appearUp',
+ AppearDown = 'appearDown',
+ Bounce = 'bounce',
+ BounceUp = 'bounceUp',
+ BounceDown = 'bounceDown',
+ Disappear = 'disappear',
+ DisappearDown = 'disappearDown',
+ DisappearUp = 'disappearUp',
+ Pulse = 'pulse',
+ Scale = 'scale',
+ ScaleDown = 'scaleDown',
+ ScaleUp = 'scaleUp',
+ VariableColor = 'variableColor',
+ Breathe = 'breathe',
+ BreathePlain = 'breathePlain',
+ BreathePulse = 'breathePulse',
+ Rotate = 'rotate',
+ RotateClockwise = 'rotateClockwise',
+ RotateCounterClockwise = 'rotateCounterClockwise',
+ Wiggle = 'wiggle',
+ WiggleBackward = 'wiggleBackward',
+ WiggleClockwise = 'wiggleClockwise',
+ WiggleCounterClockwise = 'wiggleCounterClockwise',
+ WiggleDown = 'wiggleDown',
+ WiggleForward = 'wiggleForward',
+ WiggleUp = 'wiggleUp',
+ WiggleLeft = 'wiggleLeft',
+ WiggleRight = 'wiggleRight',
+}
+
+export class ImageSymbolEffectCommon {
+ effect?: NSSymbolEffect;
+ options?: NSSymbolEffectOptions;
+ completion?: (context: UISymbolEffectCompletionContext) => void;
+}
diff --git a/packages/core/ui/image/symbol-effects.android.ts b/packages/core/ui/image/symbol-effects.android.ts
new file mode 100644
index 0000000000..27eff8434d
--- /dev/null
+++ b/packages/core/ui/image/symbol-effects.android.ts
@@ -0,0 +1,9 @@
+import { ImageSymbolEffectCommon, ImageSymbolEffects } from './symbol-effects-common';
+import type { ImageSymbolEffect as ImageSymbolEffectDefinition } from './symbol-effects.d.ts';
+export { ImageSymbolEffects };
+
+export const ImageSymbolEffect: typeof ImageSymbolEffectDefinition = class ImageSymbolEffect extends ImageSymbolEffectCommon implements ImageSymbolEffectDefinition {
+ static fromSymbol(symbol: string): ImageSymbolEffectDefinition {
+ return new ImageSymbolEffect();
+ }
+};
diff --git a/packages/core/ui/image/symbol-effects.d.ts b/packages/core/ui/image/symbol-effects.d.ts
new file mode 100644
index 0000000000..ffcb3c43b2
--- /dev/null
+++ b/packages/core/ui/image/symbol-effects.d.ts
@@ -0,0 +1,13 @@
+export { ImageSymbolEffects } from './symbol-effects-common';
+
+/**
+ * iOS only
+ * Symbol effects: https://developer.apple.com/documentation/symbols?language=objc
+ */
+export class ImageSymbolEffect {
+ effect?: NSSymbolEffect;
+ options?: NSSymbolEffectOptions;
+ completion?: (context: UISymbolEffectCompletionContext) => void;
+ constructor(symbol: NSSymbolEffect);
+ static fromSymbol(symbol: string): ImageSymbolEffect | null;
+}
diff --git a/packages/core/ui/image/symbol-effects.ios.ts b/packages/core/ui/image/symbol-effects.ios.ts
new file mode 100644
index 0000000000..0ae1bfee7b
--- /dev/null
+++ b/packages/core/ui/image/symbol-effects.ios.ts
@@ -0,0 +1,95 @@
+import { SDK_VERSION } from '../../utils/constants';
+import { ImageSymbolEffectCommon, ImageSymbolEffects } from './symbol-effects-common';
+import type { ImageSymbolEffect as ImageSymbolEffectDefinition } from './symbol-effects.d.ts';
+
+export const ImageSymbolEffect: typeof ImageSymbolEffectDefinition = class ImageSymbolEffect extends ImageSymbolEffectCommon implements ImageSymbolEffectDefinition {
+ constructor(symbol: NSSymbolEffect) {
+ super();
+ this.effect = symbol;
+ }
+ static fromSymbol(symbol: string): ImageSymbolEffectDefinition | null {
+ if (SDK_VERSION < 17) {
+ return null;
+ }
+ switch (symbol) {
+ case ImageSymbolEffects.Appear:
+ return new ImageSymbolEffect(NSSymbolAppearEffect.effect());
+ case ImageSymbolEffects.AppearUp:
+ return new ImageSymbolEffect(NSSymbolAppearEffect.appearUpEffect());
+ case ImageSymbolEffects.AppearDown:
+ return new ImageSymbolEffect(NSSymbolAppearEffect.appearDownEffect());
+ case ImageSymbolEffects.Bounce:
+ return new ImageSymbolEffect(NSSymbolBounceEffect.effect());
+ case ImageSymbolEffects.BounceUp:
+ return new ImageSymbolEffect(NSSymbolBounceEffect.bounceUpEffect());
+ case ImageSymbolEffects.BounceDown:
+ return new ImageSymbolEffect(NSSymbolBounceEffect.bounceDownEffect());
+ case ImageSymbolEffects.Disappear:
+ return new ImageSymbolEffect(NSSymbolDisappearEffect.effect());
+ case ImageSymbolEffects.DisappearDown:
+ return new ImageSymbolEffect(NSSymbolDisappearEffect.disappearDownEffect());
+ case ImageSymbolEffects.DisappearUp:
+ return new ImageSymbolEffect(NSSymbolDisappearEffect.disappearUpEffect());
+ case ImageSymbolEffects.Pulse:
+ return new ImageSymbolEffect(NSSymbolPulseEffect.effect());
+ case ImageSymbolEffects.Scale:
+ return new ImageSymbolEffect(NSSymbolScaleEffect.effect());
+ case ImageSymbolEffects.ScaleDown:
+ return new ImageSymbolEffect(NSSymbolScaleEffect.scaleDownEffect());
+ case ImageSymbolEffects.ScaleUp:
+ return new ImageSymbolEffect(NSSymbolScaleEffect.scaleUpEffect());
+ case ImageSymbolEffects.VariableColor:
+ return new ImageSymbolEffect(NSSymbolVariableColorEffect.effect());
+ }
+ if (SDK_VERSION < 18) {
+ return null;
+ }
+ // TODO: remove ts-expect-error once we bump the types package
+ switch (symbol) {
+ case ImageSymbolEffects.Breathe:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolBreatheEffect.effect());
+ case ImageSymbolEffects.BreathePlain:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolBreatheEffect.breathePlainEffect());
+ case ImageSymbolEffects.Rotate:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolRotateEffect.effect());
+ case ImageSymbolEffects.RotateClockwise:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolRotateEffect.rotateClockwiseEffect());
+ case ImageSymbolEffects.RotateCounterClockwise:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolRotateEffect.rotateCounterClockwiseEffect());
+ case ImageSymbolEffects.Wiggle:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.effect());
+ case ImageSymbolEffects.WiggleBackward:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleBackwardEffect());
+ case ImageSymbolEffects.WiggleClockwise:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleClockwiseEffect());
+ case ImageSymbolEffects.WiggleCounterClockwise:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleCounterClockwiseEffect());
+ case ImageSymbolEffects.WiggleDown:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleDownEffect());
+ case ImageSymbolEffects.WiggleForward:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleForwardEffect());
+ case ImageSymbolEffects.WiggleUp:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleUpEffect());
+ case ImageSymbolEffects.WiggleLeft:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleLeftEffect());
+ case ImageSymbolEffects.WiggleRight:
+ // @ts-expect-error added on iOS 18
+ return new ImageSymbolEffect(NSSymbolWiggleEffect.wiggleRightEffect());
+ }
+
+ return null;
+ }
+};
diff --git a/packages/core/ui/index.ts b/packages/core/ui/index.ts
index 893eb4c336..6dae8fd7c1 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, ImageSymbolEffects } 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/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)();