UNPKG

@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
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