diff --git a/apps/tests/ui/image/image-tests.ts b/apps/tests/ui/image/image-tests.ts index 2411d24ca..c6d42e066 100644 --- a/apps/tests/ui/image/image-tests.ts +++ b/apps/tests/ui/image/image-tests.ts @@ -42,12 +42,7 @@ export var test_settingImageSource = function () { } */ -export var test_SettingImageSrc = function (done) { - // >> img-create-src - var image = new ImageModule.Image(); - image.src = "https://www.google.com/images/errors/logo_sm_2.png"; - // << img-create-src - +function runImageTest(done, image: ImageModule.Image, src: string) { image.src = null; var testModel = new ObservableModule.Observable(); @@ -61,10 +56,16 @@ export var test_SettingImageSrc = function (done) { TKUnit.assertTrue(!image.isLoading, "Image.isLoading should be false."); TKUnit.assertTrue(!testModel.get("imageIsLoading"), "imageIsLoading on viewModel should be false."); TKUnit.assertTrue(imageIsLoaded, "imageIsLoading should be true."); - done(null); + if (done) { + done(null); + } } catch (e) { - done(e); + if (done) { + done(e); + } else { + throw e; + } } }; @@ -74,53 +75,55 @@ export var test_SettingImageSrc = function (done) { twoWay: true }, testModel); - image.src = "https://www.google.com/images/errors/logo_sm_2.png"; + image.src = src; testModel.on(ObservableModule.Observable.propertyChangeEvent, handler); - TKUnit.assertTrue(image.isLoading, "Image.isLoading should be true."); - TKUnit.assertTrue(testModel.get("imageIsLoading"), "model.isLoading should be true."); + if (done) { + TKUnit.assertTrue(image.isLoading, "Image.isLoading should be true."); + TKUnit.assertTrue(testModel.get("imageIsLoading"), "model.isLoading should be true."); + } else { + // Since it is synchronous check immediately. + handler(null); + } } -export var test_SettingImageSrcToFileWithinApp = function (done) { +export var test_SettingImageSrc = function (done) { + // >> img-create-src + var image = new ImageModule.Image(); + image.src = "https://www.google.com/images/errors/logo_sm_2.png"; + // << img-create-src + runImageTest(done, image, image.src) +} + +export var test_SettingImageSrcToFileWithinApp = function () { // >> img-create-local var image = new ImageModule.Image(); image.src = "~/logo.png"; // << img-create-local - var testFunc = function (views: Array) { - var testImage = views[0]; - TKUnit.waitUntilReady(() => !testImage.isLoading, 3); - try { - TKUnit.assertTrue(!testImage.isLoading, "isLoading should be false."); - done(null); - } - catch (e) { - done(e); - } - } - - helper.buildUIAndRunTest(image, testFunc); + runImageTest(null, image, image.src) } -export var test_SettingImageSrcToDataURI = function (done) { +export var test_SettingImageSrcToDataURI = function () { // >> img-create-datauri var image = new ImageModule.Image(); image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAAXNSR0IArs4c6QAAABxpRE9UAAAAAgAAAAAAAAACAAAAKAAAAAIAAAACAAAARiS4uJEAAAASSURBVBgZYvjPwABHSMz/DAAAAAD//0GWpK0AAAAOSURBVGNgYPiPhBgQAACEvQv1D5y/pAAAAABJRU5ErkJggg=="; // << img-create-datauri - var testFunc = function (views: Array) { - var testImage = views[0]; - TKUnit.waitUntilReady(() => !testImage.isLoading, 3); - try { - TKUnit.assertTrue(!testImage.isLoading, "isLoading should be false."); - TKUnit.assertNotNull(testImage.imageSource); - done(null); - } - catch (e) { - done(e); - } - } + runImageTest(null, image, image.src) +} - helper.buildUIAndRunTest(image, testFunc); +export var test_SettingImageSrcToFileWithinAppAsync = function (done) { + var image = new ImageModule.Image(); + image.loadMode = "async"; + image.src = "~/logo.png"; + runImageTest(done, image, image.src) +} + +export var test_SettingImageSrcToDataURIAsync = function (done) { + var image = new ImageModule.Image(); + image.loadMode = "async"; + image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAAXNSR0IArs4c6QAAABxpRE9UAAAAAgAAAAAAAAACAAAAKAAAAAIAAAACAAAARiS4uJEAAAASSURBVBgZYvjPwABHSMz/DAAAAAD//0GWpK0AAAAOSURBVGNgYPiPhBgQAACEvQv1D5y/pAAAAABJRU5ErkJggg=="; + runImageTest(done, image, image.src) } export var test_SettingStretch_AspectFit = function () { diff --git a/http/http-request.ios.ts b/http/http-request.ios.ts index fb5bd25b8..ed2fe718a 100644 --- a/http/http-request.ios.ts +++ b/http/http-request.ios.ts @@ -98,27 +98,14 @@ export function request(options: http.HttpRequestOptions): Promise { ensureImageSource(); - if (UIImage.imageWithData["async"]) { - return UIImage.imageWithData["async"](UIImage, [data]) - .then(image => { - if (!image) { - throw new Error("Response content may not be converted to an Image"); - } - - var source = new imageSource.ImageSource(); - source.setNativeSource(image); - return source; - }); - } - - return new Promise((resolveImage, rejectImage) => { - var img = imageSource.fromData(data); - if (img instanceof imageSource.ImageSource) { - resolveImage(img); - } else { - rejectImage(new Error("Response content may not be converted to an Image")); - } - + return new Promise((resolve, reject) => { + (UIImage).tns_decodeImageWithDataCompletion(data, image => { + if (image) { + resolve(imageSource.fromNativeSource(image)) + } else { + reject(new Error("Response content may not be converted to an Image")); + } + }); }); }, toFile: (destinationFilePath?: string) => { diff --git a/http/http.ts b/http/http.ts index 5923c7514..e4c7cd4a1 100644 --- a/http/http.ts +++ b/http/http.ts @@ -33,12 +33,9 @@ export function getJSON(arg: any): Promise { } export function getImage(arg: any): Promise { - return new Promise((resolve, reject) => { - httpRequest.request(typeof arg === "string" ? { url: arg, method: "GET" } : arg) - .then(r => { - r.content.toImage().then(source => resolve(source), e => reject(e)); - }, e => reject(e)); - }); + return httpRequest + .request(typeof arg === "string" ? { url: arg, method: "GET" } : arg) + .then(responce => responce.content.toImage()); } export function getFile(arg: any, destinationFilePath?: string): Promise { diff --git a/image-source/image-source.android.ts b/image-source/image-source.android.ts index fcf09ef48..28eeb4696 100644 --- a/image-source/image-source.android.ts +++ b/image-source/image-source.android.ts @@ -44,7 +44,7 @@ export class ImageSource implements definition.ImageSource { // Load BitmapDrawable with getDrawable to make use of Android internal caching var bitmapDrawable = res.getDrawable(identifier); if (bitmapDrawable && bitmapDrawable.getBitmap) { - this.android = bitmapDrawable.getBitmap(); + this.android = bitmapDrawable.getBitmap(); } } } @@ -52,6 +52,12 @@ export class ImageSource implements definition.ImageSource { return this.android != null; } + public fromResource(name: string): Promise { + return new Promise((resolve, reject) => { + resolve(this.loadFromResource(name)); + }); + } + public loadFromFile(path: string): boolean { ensureFS(); @@ -64,11 +70,23 @@ export class ImageSource implements definition.ImageSource { return this.android != null; } + public fromFile(path: string): Promise { + return new Promise((resolve, reject) => { + resolve(this.loadFromFile(path)); + }); + } + public loadFromData(data: any): boolean { this.android = android.graphics.BitmapFactory.decodeStream(data); return this.android != null; } + public fromData(data: any): Promise { + return new Promise((resolve, reject) => { + resolve(this.loadFromData(data)); + }); + } + public loadFromBase64(source: string): boolean { if (types.isString(source)) { var bytes = android.util.Base64.decode(source, android.util.Base64.DEFAULT); @@ -77,6 +95,12 @@ export class ImageSource implements definition.ImageSource { return this.android != null; } + public fromBase64(data: any): Promise { + return new Promise((resolve, reject) => { + resolve(this.loadFromBase64(data)); + }); + } + public setNativeSource(source: any): boolean { this.android = source; return source != null; diff --git a/image-source/image-source.d.ts b/image-source/image-source.d.ts index aa71208e3..74b9898ac 100644 --- a/image-source/image-source.d.ts +++ b/image-source/image-source.d.ts @@ -33,23 +33,47 @@ declare module "image-source" { */ loadFromResource(name: string): boolean; + /** + * Loads this instance from the specified resource name asynchronously. + * @param name The name of the resource (without its extension). + */ + fromResource(name: string): Promise; + /** * Loads this instance from the specified file. * @param path The location of the file on the file system. */ loadFromFile(path: string): boolean; + /** + * Loads this instance from the specified file asynchronously. + * @param path The location of the file on the file system. + */ + fromFile(path: string): Promise; + /** * Loads this instance from the specified native image data. * @param data The native data (byte array) to load the image from. This will be either Stream for Android or NSData for iOS. */ loadFromData(data: any): boolean; + + /** + * Loads this instance from the specified native image data asynchronously. + * @param data The native data (byte array) to load the image from. This will be either Stream for Android or NSData for iOS. + */ + fromData(data: any): Promise; /** * Loads this instance from the specified native image data. * @param source The Base64 string to load the image from. */ loadFromBase64(source: string): boolean; + + /** + * Loads this instance from the specified native image data asynchronously. + * @param source The Base64 string to load the image from. + */ + fromBase64(source: string): Promise; /** * Sets the provided native source object (typically a Bitmap). diff --git a/image-source/image-source.ios.ts b/image-source/image-source.ios.ts index 01c2ba4f3..d0937097a 100644 --- a/image-source/image-source.ios.ts +++ b/image-source/image-source.ios.ts @@ -11,10 +11,30 @@ export class ImageSource implements definition.ImageSource { public ios: UIImage; public loadFromResource(name: string): boolean { - this.ios = UIImage.imageNamed(name) || UIImage.imageNamed(`${name}.jpg`); + this.ios = (UIImage).tns_safeImageNamed(name) || (UIImage).tns_safeImageNamed(`${name}.jpg`); return this.ios != null; } + public fromResource(name: string): Promise { + return new Promise((resolve, reject) => { + try { + (UIImage).tns_safeDecodeImageNamedCompletion(name, image => { + if (image) { + this.ios = image; + resolve(true); + } else { + (UIImage).tns_safeDecodeImageNamedCompletion(`${name}.jpg`, image => { + this.ios = image; + resolve(true); + }); + } + }); + } catch (ex) { + reject(ex); + } + }); + } + public loadFromFile(path: string): boolean { var fileName = types.isString(path) ? path.trim() : ""; @@ -26,19 +46,67 @@ export class ImageSource implements definition.ImageSource { return this.ios != null; } + public fromFile(path: string): Promise { + return new Promise((resolve, reject) => { + try { + var fileName = types.isString(path) ? path.trim() : ""; + + if (fileName.indexOf("~/") === 0) { + fileName = fs.path.join(fs.knownFolders.currentApp().path, fileName.replace("~/", "")); + } + + (UIImage).tns_decodeImageWidthContentsOfFileCompletion(fileName, image => { + this.ios = image; + resolve(true); + }); + } catch (ex) { + reject(ex); + } + }); + } + public loadFromData(data: any): boolean { this.ios = UIImage.imageWithData(data); return this.ios != null; } + public fromData(data: any): Promise { + return new Promise((resolve, reject) => { + try { + (UIImage).tns_decodeImageWithDataCompletion(data, image => { + this.ios = image; + resolve(true); + }); + } catch (ex) { + reject(ex); + } + }); + } + public loadFromBase64(source: string): boolean { if (types.isString(source)) { var data = NSData.alloc().initWithBase64EncodedStringOptions(source, NSDataBase64DecodingOptions.NSDataBase64DecodingIgnoreUnknownCharacters); this.ios = UIImage.imageWithData(data); } + return this.ios != null; } + public fromBase64(source: string): Promise { + return new Promise((resolve, reject) => { + try { + var data = NSData.alloc().initWithBase64EncodedStringOptions(source, NSDataBase64DecodingOptions.NSDataBase64DecodingIgnoreUnknownCharacters); + UIImage.imageWithData["async"](UIImage, [data]).then(image => { + this.ios = image; + resolve(true); + }); + + } catch (ex) { + reject(ex); + } + }); + } + public setNativeSource(source: any): boolean { this.ios = source; return source != null; @@ -103,4 +171,4 @@ function getImageData(instance: UIImage, format: string, quality = 1.0): NSData } return data; -} +} \ No newline at end of file diff --git a/package.json b/package.json index 18247d938..cf7def8c6 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ }, "typings": "tns-core-modules.d.ts", "dependencies": { - "tns-core-modules-widgets": "2.0.0" + "tns-core-modules-widgets": "next" }, "nativescript": { "platforms": { diff --git a/tsconfig.json b/tsconfig.json index 85a6841cc..2700f9662 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "declaration": false, "noImplicitAny": false, "noImplicitUseStrict": true, - "experimentalDecorators": true + "experimentalDecorators": true, + "diagnostics": false }, "filesGlob": [ "**/*.ts", @@ -695,8 +696,8 @@ "ui/transition/transition.android.ts", "ui/transition/transition.d.ts", "ui/transition/transition.ios.ts", - "ui/transition/fade-transition.d.ts", - "ui/transition/slide-transition.d.ts", + "ui/transition/fade-transition.d.ts", + "ui/transition/slide-transition.d.ts", "ui/utils.d.ts", "ui/utils.ios.ts", "ui/web-view/web-view-common.ts", diff --git a/ui/image/image-common.ts b/ui/image/image-common.ts index cc03af900..6e81152ad 100644 --- a/ui/image/image-common.ts +++ b/ui/image/image-common.ts @@ -11,6 +11,10 @@ import * as types from "utils/types"; var SRC = "src"; var IMAGE_SOURCE = "imageSource"; +var LOAD_MODE = "loadMode"; + +var SYNC = "sync"; +var ASYNC = "async"; var IMAGE = "Image"; var ISLOADING = "isLoading"; @@ -21,41 +25,8 @@ var AffectsLayout = platform.device.os === platform.platformNames.android ? depe function onSrcPropertyChanged(data: dependencyObservable.PropertyChangeData) { var image = data.object; - var value = data.newValue; - - if (types.isString(value)) { - value = value.trim(); - image.imageSource = null; - image["_url"] = value; - - image._setValue(Image.isLoadingProperty, true); - - if (utils.isDataURI(value)) { - var base64Data = value.split(",")[1]; - if (types.isDefined(base64Data)) { - image.imageSource = imageSource.fromBase64(base64Data); - image._setValue(Image.isLoadingProperty, false); - } - } - else if (imageSource.isFileOrResourcePath(value)) { - image.imageSource = imageSource.fromFileOrResource(value); - image._setValue(Image.isLoadingProperty, false); - } else { - imageSource.fromUrl(value).then((r) => { - if (image["_url"] === value) { - image.imageSource = r; - image._setValue(Image.isLoadingProperty, false); - } - }); - } - } - else if (value instanceof imageSource.ImageSource) { - // Support binding the imageSource trough the src property - image.imageSource = value; - } - else { - image.imageSource = imageSource.fromNativeSource(value); - } + // Check for delay... + image._createImageSourceFromSrc(); } export class Image extends view.View implements definition.Image { @@ -72,6 +43,9 @@ export class Image extends view.View implements definition.Image { public static stretchProperty = new dependencyObservable.Property(STRETCH, IMAGE, new proxy.PropertyMetadata(enums.Stretch.aspectFit, AffectsLayout)); + + public static loadModeProperty = new dependencyObservable.Property(LOAD_MODE, IMAGE, + new proxy.PropertyMetadata(SYNC, 0, null, (value) => value === SYNC || value === ASYNC, null)); get imageSource(): imageSource.ImageSource { return this._getValue(Image.imageSourceProperty); @@ -98,7 +72,80 @@ export class Image extends view.View implements definition.Image { this._setValue(Image.stretchProperty, value); } + get loadMode(): "sync" | "async" { + return this._getValue(Image.loadModeProperty); + } + set loadMode(value: "sync" | "async") { + this._setValue(Image.loadModeProperty, value); + } + public _setNativeImage(nativeImage: any) { // } + + /** + * @internal + */ + _createImageSourceFromSrc(): void { + var value = this.src; + if (types.isString(value)) { + value = value.trim(); + this.imageSource = null; + this["_url"] = value; + + this._setValue(Image.isLoadingProperty, true); + + var source = new imageSource.ImageSource(); + var imageLoaded = () => { + this.imageSource = source; + this._setValue(Image.isLoadingProperty, false); + } + if (utils.isDataURI(value)) { + var base64Data = value.split(",")[1]; + if (types.isDefined(base64Data)) { + if (this.loadMode === SYNC) { + source.loadFromBase64(base64Data); + imageLoaded(); + } else if (this.loadMode === ASYNC) { + source.fromBase64(base64Data).then(imageLoaded); + } + } + } + else if (imageSource.isFileOrResourcePath(value)) { + if (value.indexOf(utils.RESOURCE_PREFIX) === 0) { + let resPath = value.substr(utils.RESOURCE_PREFIX.length); + if (this.loadMode === SYNC) { + source.loadFromResource(resPath); + imageLoaded(); + } else if (this.loadMode === ASYNC) { + this.imageSource = null; + source.fromResource(resPath).then(imageLoaded); + } + } else { + if (this.loadMode === SYNC) { + source.loadFromFile(value); + imageLoaded(); + } else if (this.loadMode === ASYNC) { + this.imageSource = null; + source.fromFile(value).then(imageLoaded); + } + } + } else { + this.imageSource = null; + imageSource.fromUrl(value).then((r) => { + if (this["_url"] === value) { + this.imageSource = r; + this._setValue(Image.isLoadingProperty, false); + } + }); + } + } + else if (value instanceof imageSource.ImageSource) { + // Support binding the imageSource trough the src property + this.imageSource = value; + } + else { + this.imageSource = imageSource.fromNativeSource(value); + } + } } diff --git a/ui/image/image.d.ts b/ui/image/image.d.ts index e3748c86d..d58a90f3b 100644 --- a/ui/image/image.d.ts +++ b/ui/image/image.d.ts @@ -44,5 +44,12 @@ declare module "ui/image" { * Gets or sets the image stretch mode. */ stretch: string; + + /** + * Gets or sets the loading strategy for images on the local file system: + * - **sync** *(default)* - blocks the UI if necessary to display immediately, good for small icons. + * - **async** - will try to load in the background, may appear with short delay, good for large images. + */ + loadMode: "sync" | "async"; } } \ No newline at end of file