UNPKG

@nativescript-community/ui-image

Version:

Advanced and efficient image display plugin which uses Fresco (Android) and SDWebImage (iOS) to implement caching, placeholders, image effects, and much more.

577 lines 24.4 kB
var _a, _b, _c, _d, _e; 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, progressBarColorProperty, showProgressBarProperty, 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); this.isLoading = false; this.contextOptions = null; this.mImageSourceAffectsLayout = true; this.handleImageLoaded = (image, error, cacheType) => { this.isLoading = false; 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(); } 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 }); } this.handleImageProgress(1); }; this.onLoadProgress = (currentSize, totalSize) => { this.handleImageProgress(totalSize > 0 ? currentSize / totalSize : -1, totalSize); }; } 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) { widthMeasureSpec = layout.makeMeasureSpec(height * ratio, layout.EXACTLY); } else if (!finiteHeight && finiteWidth) { heightMeasureSpec = layout.makeMeasureSpec(width / ratio, layout.EXACTLY); } 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); } } 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) { // switch (this.transition) { // case 'fade': this.nativeImageViewProtected.alpha = 0.0; this.nativeImageViewProtected.image = nativeImage; UIView.animateWithDurationAnimations(this.fadeDuration / 1000, () => { this.nativeImageViewProtected.alpha = this.opacity; }); // break; // case 'curlUp': // UIView.transitionWithViewDurationOptionsAnimationsCompletion( // this.nativeImageViewProtected, // 0.3, // UIViewAnimationOptions.TransitionCrossDissolve, // () => { // this._setNativeImage(image); // }, // null // ); // break; // default: // this._setNativeImage(image); // } } else { this.nativeImageViewProtected.image = nativeImage; } 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); this.isLoading = true; 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); } if (this.showProgressBar) { try { if (this.progressBarColor) { const indicator = new SDWebImageActivityIndicator(); indicator.indicatorView.color = this.progressBarColor.ios; this.nativeImageViewProtected.sd_imageIndicator = indicator; } else { this.nativeImageViewProtected.sd_imageIndicator = SDWebImageActivityIndicator.grayIndicator; } } catch (ex) { console.error(ex); } } 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) { this.initImage(); } [_b = imageRotationProperty.setNative](value) { this.initImage(); } [_c = placeholderImageUriProperty.setNative]() { } [_d = showProgressBarProperty.setNative](value) { this.showProgressBar = value; } [progressBarColorProperty.setNative](value) { this.progressBarColor = value; } [_e = 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); __decorate([ needRequestImage ], Img.prototype, _e, null); //# sourceMappingURL=index.ios.js.map