@nativescript-community/ui-image
Version:
Advanced and efficient image display plugin which uses Glide (Android) and SDWebImage (iOS) to implement caching, placeholders, image effects, and much more.
608 lines • 26.7 kB
JavaScript
var _a, _b, _c, _d;
export * from './index-common';
import { ApplicationSettings, ImageAsset, ImageSource, Screen, Trace, Utils, knownFolders, path } from '@nativescript/core';
import { debounce } from '@nativescript/core/utils';
import { layout } from '@nativescript/core/utils/layout-helper';
import { isString } from '@nativescript/core/utils/types';
import { CLog, CLogTypes, ImageBase, ScaleType, failureImageUriProperty, headersProperty, imageRotationProperty, placeholderImageUriProperty, srcProperty, stretchProperty, wrapNativeException } from './index-common';
export class ImageInfo {
constructor(width, height) {
this.width = width;
this.height = height;
}
getHeight() {
return this.height;
}
getWidth() {
return this.width;
}
}
const supportedLocalFormats = ['png', 'jpg', 'gif', 'jpeg', 'webp'];
let screenScale = -1;
function getScaleType(scaleType) {
if (isString(scaleType)) {
switch (scaleType) {
case ScaleType.Center:
case ScaleType.CenterCrop:
case ScaleType.AspectFill:
return 2 /* SDImageScaleMode.AspectFill */;
case ScaleType.CenterInside:
case ScaleType.FitCenter:
case ScaleType.FitEnd:
case ScaleType.FitStart:
case ScaleType.AspectFit:
return 1 /* SDImageScaleMode.AspectFit */;
case ScaleType.FitXY:
case ScaleType.FocusCrop:
case ScaleType.Fill:
return 0 /* SDImageScaleMode.Fill */;
default:
break;
}
}
return null;
}
function getUIImageScaleType(scaleType) {
if (isString(scaleType)) {
switch (scaleType) {
case ScaleType.Center:
return 4 /* UIViewContentMode.Center */;
case ScaleType.FocusCrop:
case ScaleType.CenterCrop:
case ScaleType.AspectFill:
return 2 /* UIViewContentMode.ScaleAspectFill */;
case ScaleType.AspectFit:
case ScaleType.CenterInside:
case ScaleType.FitCenter:
return 1 /* UIViewContentMode.ScaleAspectFit */;
case ScaleType.FitEnd:
return 8 /* UIViewContentMode.Right */;
case ScaleType.FitStart:
return 7 /* UIViewContentMode.Left */;
case ScaleType.Fill:
case ScaleType.FitXY:
return 0 /* UIViewContentMode.ScaleToFill */;
case ScaleType.None:
return 9 /* UIViewContentMode.TopLeft */;
default:
break;
}
}
return null;
}
export function initialize(config) {
SDImageLoadersManager.sharedManager.loaders = NSArray.arrayWithArray([SDWebImageDownloader.sharedDownloader, SDImagePhotosLoader.sharedLoader]);
}
export function shutDown() { }
const pluginsGetContextFromOptions = new Set();
export function registerPluginGetContextFromOptions(callback) {
pluginsGetContextFromOptions.add(callback);
}
function getContextFromOptions(options) {
const context = NSMutableDictionary.dictionary();
const transformers = [];
if (options.decodeWidth || options.decodeHeight) {
//@ts-ignore
transformers.push(NSImageDecodeSizeTransformer.transformerWithDecodeWidthDecodeHeight(options.decodeWidth || options.decodeHeight, options.decodeHeight || options.decodeWidth));
}
if (options.tintColor) {
transformers.push(SDImageTintTransformer.transformerWithColor(options.tintColor.ios));
}
if (options.blurRadius) {
transformers.push(SDImageBlurTransformer.transformerWithRadius(options.blurRadius));
}
if (options.roundAsCircle === true) {
//@ts-ignore
transformers.push(NSImageRoundAsCircleTransformer.transformer());
}
if (options.imageRotation !== 0 && !isNaN(options.imageRotation)) {
transformers.push(SDImageRotationTransformer.transformerWithAngleFitSize(-options.imageRotation * (Math.PI / 180), true));
}
if (options.roundBottomLeftRadius || options.roundBottomRightRadius || options.roundTopLeftRadius || options.roundTopRightRadius) {
transformers.push(
//@ts-ignore
NSImageRoundCornerTransformer.transformerWithTopLefRadiusTopRightRadiusBottomRightRadiusBottomLeftRadius(layout.toDeviceIndependentPixels(options.roundTopLeftRadius), layout.toDeviceIndependentPixels(options.roundTopRightRadius), layout.toDeviceIndependentPixels(options.roundBottomRightRadius), layout.toDeviceIndependentPixels(options.roundBottomLeftRadius)));
}
pluginsGetContextFromOptions.forEach((c) => c(context, transformers, options));
if (transformers.length > 0) {
context.setValueForKey(SDImagePipelineTransformer.transformerWithTransformers(transformers), SDWebImageContextImageTransformer);
}
return context;
}
// This is not the best solution as their might be a lot of corner cases
// for example if an image is removed from cache without going through ImagePipeline
// we wont know it and the cacheKeyMap will grow
// but i dont see a better way right now
const CACHE_KEYS_SETTING_KEY = 'NS_ui_image_cache_keys';
let cacheKeyMap = JSON.parse(ApplicationSettings.getString(CACHE_KEYS_SETTING_KEY, '{}'));
const saveCacheKeys = debounce(() => ApplicationSettings.setString(CACHE_KEYS_SETTING_KEY, JSON.stringify(cacheKeyMap)), 500);
function registerCacheKey(cacheKey, uri) {
const set = new Set(cacheKeyMap[uri] || []);
set.add(cacheKey);
cacheKeyMap[uri] = [...set];
saveCacheKeys();
}
export class ImagePipeline {
constructor() {
this.mIos = SDImageCache.sharedImageCache;
}
getCacheKey(uri, options) {
const context = getContextFromOptions(options);
return SDWebImageManager.sharedManager.cacheKeyForURLContext(NSURL.URLWithString(uri), context);
}
isInDiskCache(key) {
return this.mIos.diskImageDataExistsWithKey(key);
}
isInBitmapMemoryCache(key) {
return this.mIos.imageFromMemoryCacheForKey(key) !== null;
}
evictFromMemoryCache(key) {
const cachekKeys = (cacheKeyMap[key] || []).concat([key]);
cachekKeys.forEach((k) => {
this.mIos.removeImageFromMemoryForKey(k);
});
}
async evictFromDiskCache(key) {
return this.evictFromCache(key, 1 /* SDImageCacheType.Disk */);
}
async evictFromCache(key, type = 3 /* SDImageCacheType.All */) {
const cachekKeys = (cacheKeyMap[key] || []).concat([key]);
delete cacheKeyMap[key];
return Promise.all(cachekKeys.map((k) => new Promise((resolve) => {
this.mIos.removeImageForKeyCacheTypeCompletion(k, type, resolve);
})));
}
clearCaches() {
cacheKeyMap = {};
ApplicationSettings.remove(CACHE_KEYS_SETTING_KEY);
this.mIos.clearMemory();
this.mIos.clearDiskOnCompletion(null);
}
clearMemoryCaches() {
this.mIos.clearMemory();
}
clearDiskCaches() {
this.mIos.clearDiskOnCompletion(null);
}
prefetchToDiskCache(uri) {
return this.prefetchToCacheType(uri, 1 /* SDImageCacheType.Disk */);
}
prefetchToMemoryCache(uri) {
return this.prefetchToCacheType(uri, 2 /* SDImageCacheType.Memory */);
}
prefetchToCacheType(uri, cacheType) {
return new Promise((resolve, reject) => {
const context = NSMutableDictionary.alloc().initWithCapacity(1);
context.setObjectForKey(cacheType, SDWebImageContextStoreCacheType);
SDWebImagePrefetcher.sharedImagePrefetcher.context = context;
SDWebImagePrefetcher.sharedImagePrefetcher.prefetchURLsProgressCompleted([getUri(uri)], null, (finished, skipped) => {
if (finished && !skipped) {
resolve();
}
else {
reject(`prefetch failed for URI: ${uri}`);
}
});
});
}
get ios() {
return this.mIos;
}
}
ImagePipeline.iosComplexCacheEviction = false;
export const needRequestImage = function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
if (!this.mCanRequestImage) {
this.mNeedRequestImage = true;
// we need to ensure a hierarchy is set or the default aspect ratio wont be set
// because aspectFit is the default (wanted) but then we wont go into stretchProperty.setNative
// this.mNeedUpdateHierarchy = true;
return;
}
return originalMethod.apply(this, args);
};
};
export function getImagePipeline() {
const imagePineLine = new ImagePipeline();
return imagePineLine;
}
function getUri(src) {
let uri = src;
if (src instanceof ImageAsset) {
return NSURL.sd_URLWithAsset(src.ios);
}
if (uri.indexOf(Utils.RESOURCE_PREFIX) === 0) {
const resName = uri.substr(Utils.RESOURCE_PREFIX.length);
if (screenScale === -1) {
screenScale = Screen.mainScreen.scale;
}
const found = supportedLocalFormats.some((v) => {
for (let i = screenScale; i >= 1; i--) {
uri = NSBundle.mainBundle.URLForResourceWithExtension(i > 1 ? `${resName}@${i}x` : resName, v);
if (uri) {
return true;
}
}
return false;
});
if (found) {
return uri;
}
}
else if (uri.indexOf('~/') === 0) {
return NSURL.fileURLWithPath(`${path.join(knownFolders.currentApp().path, uri.replace('~/', ''))}`);
}
else if (uri.indexOf('/') === 0) {
return NSURL.fileURLWithPath(uri);
}
return NSURL.URLWithString(uri);
}
export class Img extends ImageBase {
constructor() {
super(...arguments);
// network detection + notification guard
this._isRemote = false;
this._notifiedLoadSourceNetworkStart = false;
this.contextOptions = null;
this.mImageSourceAffectsLayout = true;
this.handleImageLoaded = (image, error, cacheType) => {
if (!this.nativeViewProtected) {
return;
}
const animate = (this.alwaysFade || cacheType !== 2 /* SDImageCacheType.Memory */) && this.fadeDuration > 0;
if (image) {
this._setNativeImage(image, animate);
}
if (!this.autoPlayAnimations) {
this.nativeImageViewProtected.stopAnimating();
}
let sourceStr = 'none';
if (typeof cacheType === 'number') {
if (cacheType === 2 /* SDImageCacheType.Memory */)
sourceStr = 'memory';
else if (cacheType === 1 /* SDImageCacheType.Disk */)
sourceStr = 'disk';
else if (cacheType === 0 /* SDImageCacheType.None */)
sourceStr = 'network';
else
sourceStr = 'unknown';
}
if (error) {
this.notify({
eventName: Img.failureEvent,
error: wrapNativeException(error)
});
if (this.failureImageUri) {
image = this.getUIImage(this.failureImageUri);
this._setNativeImage(image, animate);
}
}
else if (image) {
this.notify({
eventName: ImageBase.finalImageSetEvent,
imageInfo: new ImageInfo(image.size.width, image.size.height),
ios: image,
source: sourceStr
});
}
// Notify where the image came from (memory/disk/network) in a loadSource event.
// For cached (memory/disk) images, emit loadSource now (we already emit a "network start" in onLoadProgress for remote)
if (cacheType !== 0 /* SDImageCacheType.None */ && this.hasListeners(ImageBase.loadSourceEvent)) {
this.notify({
eventName: ImageBase.loadSourceEvent,
source: sourceStr
});
}
this.handleImageProgress(1);
};
this.onLoadProgress = (currentSize, totalSize) => {
const fraction = totalSize > 0 ? currentSize / totalSize : -1;
this.handleImageProgress(fraction, totalSize);
Utils.executeOnMainThread(() => {
const eventName = ImageBase.progressEvent;
if (this.hasListeners(eventName)) {
// Notify progress event
this.notify({
eventName,
progress: fraction,
current: currentSize,
total: totalSize
});
}
// If this is a network load, notify loadSource event once at the first progress call
if (this._isRemote && !this._notifiedLoadSourceNetworkStart) {
this._notifiedLoadSourceNetworkStart = true;
if (this.hasListeners(ImageBase.loadSourceEvent)) {
this.notify({
eventName: ImageBase.loadSourceEvent,
source: 'network'
});
}
}
});
};
}
get cacheKey() {
return this.mCacheKey;
}
createNativeView() {
const result = this.animatedImageView ? SDAnimatedImageView.new() : UIImageView.new();
result.contentMode = 1 /* UIViewContentMode.ScaleAspectFit */;
result.clipsToBounds = true;
result.userInteractionEnabled = true; // needed for gestures to work
result.tintColor = null;
return result;
}
_setNativeClipToBounds() {
// Always set clipsToBounds for images
this.nativeViewProtected.clipsToBounds = true;
}
onMeasure(widthMeasureSpec, heightMeasureSpec) {
// We don't call super because we measure native view with specific size.
const width = layout.getMeasureSpecSize(widthMeasureSpec);
const widthMode = layout.getMeasureSpecMode(widthMeasureSpec);
const height = layout.getMeasureSpecSize(heightMeasureSpec);
const heightMode = layout.getMeasureSpecMode(heightMeasureSpec);
const image = this.nativeImageViewProtected.image;
const finiteWidth = widthMode === layout.EXACTLY;
const finiteHeight = heightMode === layout.EXACTLY;
this.mImageSourceAffectsLayout = !finiteWidth || !finiteHeight;
// if (Trace.isEnabled()) {
// CLog(CLogTypes.info, 'onMeasure', this.src, widthMeasureSpec, heightMeasureSpec, width, height, this.aspectRatio, image && image.imageOrientation);
// }
if (image || this.aspectRatio > 0) {
const nativeWidth = image ? layout.toDevicePixels(image.size.width) : 0;
const nativeHeight = image ? layout.toDevicePixels(image.size.height) : 0;
const imgRatio = nativeWidth / nativeHeight;
const ratio = this.aspectRatio || imgRatio;
// const scale = this.computeScaleFactor(width, height, finiteWidth, finiteHeight, nativeWidth, nativeHeight, this.aspectRatio || imgRatio );
if (!finiteWidth && finiteHeight) {
if (!this.noRatioEnforce || this.horizontalAlignment !== 'stretch') {
widthMeasureSpec = layout.makeMeasureSpec(height * ratio, layout.EXACTLY);
}
else {
widthMeasureSpec = layout.makeMeasureSpec(0, layout.AT_MOST);
}
}
else if (!finiteHeight && finiteWidth) {
if (!this.noRatioEnforce || this.verticalAlignment !== 'stretch') {
heightMeasureSpec = layout.makeMeasureSpec(width / ratio, layout.EXACTLY);
}
else {
heightMeasureSpec = layout.makeMeasureSpec(0, layout.AT_MOST);
}
}
else if (!finiteWidth && !finiteHeight) {
const viewRatio = width / height;
if (viewRatio < ratio) {
widthMeasureSpec = layout.makeMeasureSpec(width, layout.EXACTLY);
heightMeasureSpec = layout.makeMeasureSpec(width / ratio, layout.EXACTLY);
}
else {
widthMeasureSpec = layout.makeMeasureSpec(height * ratio, layout.EXACTLY);
heightMeasureSpec = layout.makeMeasureSpec(height, layout.EXACTLY);
}
}
if (Trace.isEnabled()) {
CLog(CLogTypes.info, 'onMeasure', this.src, this.aspectRatio, finiteWidth, finiteHeight, width, height, nativeWidth, nativeHeight, widthMeasureSpec, heightMeasureSpec);
}
}
else {
// if (!finiteWidth && finiteHeight) {
// widthMeasureSpec = layout.makeMeasureSpec(0, layout.UNSPECIFIED);
// } else if (!finiteHeight && finiteWidth) {
// heightMeasureSpec = layout.makeMeasureSpec(0, layout.UNSPECIFIED);
// }
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
async updateImageUri() {
const imagePipeLine = getImagePipeline();
const src = this.src;
const srcType = typeof src;
if (src && (srcType === 'string' || src instanceof ImageAsset)) {
// const isInCache = imagePipeLine.isInBitmapMemoryCache(cachekKey);
// if (isInCache) {
await imagePipeLine.evictFromCache(getUri(src).absoluteString);
// }
}
// this.src = null;
// ensure we clear the image as
this._setNativeImage(null, false);
this.initImage();
}
_setNativeImage(nativeImage, animated = true) {
if (animated && this.fadeDuration) {
// Crossfade from the currently visible content (placeholder/previous image) to the new image.
try {
UIView.transitionWithViewDurationOptionsAnimationsCompletion(this.nativeImageViewProtected, this.fadeDuration / 1000, 5242880 /* UIViewAnimationOptions.TransitionCrossDissolve */, () => {
this.nativeImageViewProtected.image = nativeImage;
}, null);
}
catch (ex) {
// Fall back to an alpha fade if transition isn't available for some reason.
this.nativeImageViewProtected.alpha = 0.0;
this.nativeImageViewProtected.image = nativeImage;
UIView.animateWithDurationAnimations(this.fadeDuration / 1000, () => {
this.nativeImageViewProtected.alpha = this.opacity;
});
}
}
else {
this.nativeImageViewProtected.image = nativeImage;
}
// Ensure final alpha is set to the view's current opacity.
this.nativeImageViewProtected.alpha = this.opacity;
if (this.mImageSourceAffectsLayout) {
// this.mImageSourceAffectsLayout = false;
this.requestLayout();
}
}
getUIImage(imagePath) {
if (!path) {
return null;
}
let image;
if (typeof imagePath === 'string') {
if (Utils.isFontIconURI(imagePath)) {
const fontIconCode = imagePath.split('//')[1];
if (fontIconCode !== undefined) {
// support sync mode only
const font = this.style.fontInternal;
const color = this.style.color;
image = ImageSource.fromFontIconCodeSync(fontIconCode, font, color).ios;
}
}
if (!image && Utils.isFileOrResourcePath(imagePath)) {
image = ImageSource.fromFileOrResourceSync(imagePath);
}
}
else {
image = imagePath;
}
return image?.ios;
}
async initImage() {
// this.nativeImageViewProtected.setImageURI(null);
this.handleImageSrc(this.src);
}
async handleImageSrc(src) {
if (this.nativeViewProtected) {
if (src instanceof Promise) {
this.handleImageSrc(await src);
return;
}
else if (typeof src === 'function') {
const newSrc = src();
if (newSrc instanceof Promise) {
await newSrc;
}
this.handleImageSrc(newSrc);
return;
}
if (src) {
const animate = this.fadeDuration > 0;
if (src instanceof ImageSource) {
this._setNativeImage(src.ios, animate);
return;
}
else if (typeof src === 'string') {
if (Utils.isFontIconURI(src)) {
const fontIconCode = src.split('//')[1];
if (fontIconCode !== undefined) {
// support sync mode only
const font = this.style.fontInternal;
const color = this.style.color;
this._setNativeImage(ImageSource.fromFontIconCodeSync(fontIconCode, font, color).ios, animate);
}
return;
}
}
const uri = getUri(src);
// detect whether this uri is a remote HTTP/HTTPS resource
const scheme = uri && uri.scheme ? (uri.scheme + '').toLowerCase() : null;
this._isRemote = scheme === 'http' || scheme === 'https';
// reset network-start notification guard for this new request
this._notifiedLoadSourceNetworkStart = false;
let options = 2048 /* SDWebImageOptions.ScaleDownLargeImages */ | 1024 /* SDWebImageOptions.AvoidAutoSetImage */;
if (this.placeholderImageUri) {
this.placeholderImage = this.getUIImage(this.placeholderImageUri);
this._setNativeImage(this.placeholderImage, animate);
}
if (this.noCache) {
// const key = uri.absoluteString;
// const imagePipeLine = getImagePipeline();
// const isInCache = imagePipeLine.isInBitmapMemoryCache(key);
// if (isInCache) {
// imagePipeLine.evictFromCache(key);
// }
options = options | 65536 /* SDWebImageOptions.FromLoaderOnly */;
}
if (this.alwaysFade === true) {
options |= 131072 /* SDWebImageOptions.ForceTransition */;
}
if (this.progressiveRenderingEnabled === true) {
options = options | 4 /* SDWebImageOptions.ProgressiveLoad */;
}
const context = getContextFromOptions(this);
if (this.animatedImageView) {
// as we use SDAnimatedImageView all images are loaded as SDAnimatedImage;
options |= 512 /* SDWebImageOptions.TransformAnimatedImage */;
}
if (this.contextOptions && typeof this.contextOptions === 'object') {
Object.keys(this.contextOptions).forEach((k) => {
const value = this.contextOptions[k];
context.setValueForKey(value, k);
});
}
if (this.headers) {
const requestModifier = SDWebImageDownloaderRequestModifier.requestModifierWithBlock((request) => {
const newRequest = request.mutableCopy();
Object.keys(this.headers).forEach((k) => {
newRequest.addValueForHTTPHeaderField(this.headers[k], k);
});
return newRequest.copy();
});
context.setValueForKey(requestModifier, SDWebImageContextDownloadRequestModifier);
}
this.mCacheKey = SDWebImageManager.sharedManager.cacheKeyForURLContext(uri, context);
if (ImagePipeline.iosComplexCacheEviction) {
registerCacheKey(this.mCacheKey, uri);
}
this.nativeImageViewProtected.sd_setImageWithURLPlaceholderImageOptionsContextProgressCompleted(uri, this.placeholderImage, options, context, this.onLoadProgress, this.handleImageLoaded);
}
else if (this.placeholderImage) {
this._setNativeImage(this.placeholderImage);
}
else {
this._setNativeImage(null);
}
}
}
[_a = srcProperty.setNative](value) { }
[_b = imageRotationProperty.setNative](value) { }
[_c = placeholderImageUriProperty.setNative]() { }
[_d = headersProperty.setNative](value) { }
[failureImageUriProperty.setNative]() {
// this.updateHierarchy();
}
[stretchProperty.setNative](value) {
if (!this.nativeView) {
return;
}
this.nativeImageViewProtected.contentMode = getUIImageScaleType(value);
}
// [ImageBase.blendingModeProperty.setNative](value: string) {
// switch (value) {
// case 'multiply':
// this.nativeImageViewProtected.layer.compositingFilter = 'multiply';
// break;
// case 'lighten':
// this.nativeImageViewProtected.layer.compositingFilter = 'lighten';
// break;
// case 'screen':
// this.nativeImageViewProtected.layer.compositingFilter = 'screen';
// break;
// }
// }
startAnimating() {
this.nativeImageViewProtected.startAnimating();
}
stopAnimating() {
this.nativeImageViewProtected.stopAnimating();
}
}
__decorate([
needRequestImage
], Img.prototype, _a, null);
__decorate([
needRequestImage
], Img.prototype, _b, null);
__decorate([
needRequestImage
], Img.prototype, _c, null);
__decorate([
needRequestImage
], Img.prototype, _d, null);
//# sourceMappingURL=index.ios.js.map