async image loading from data, files and resources for ios

Use the tns_safeDecodeImageNamedCompletion from the widgets framework

Add loadMode on Image with sync and async options for local images
This commit is contained in:
Nedyalko Nikolov
2016-04-15 09:04:02 +03:00
committed by Panayot Cankov
parent b8785afd74
commit 1b395aac7f
10 changed files with 266 additions and 108 deletions

View File

@ -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<ViewModule.View>) {
var testImage = <ImageModule.Image> 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<ViewModule.View>) {
var testImage = <ImageModule.Image>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 () {

View File

@ -98,27 +98,14 @@ export function request(options: http.HttpRequestOptions): Promise<http.HttpResp
},
toImage: () => {
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<any>((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) => {
(<any>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) => {

View File

@ -33,12 +33,9 @@ export function getJSON<T>(arg: any): Promise<T> {
}
export function getImage(arg: any): Promise<image.ImageSource> {
return new Promise<image.ImageSource>((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<any> {

View File

@ -44,7 +44,7 @@ export class ImageSource implements definition.ImageSource {
// Load BitmapDrawable with getDrawable to make use of Android internal caching
var bitmapDrawable = <android.graphics.drawable.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<boolean> {
return new Promise<boolean>((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<boolean> {
return new Promise<boolean>((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<boolean> {
return new Promise<boolean>((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<boolean> {
return new Promise<boolean>((resolve, reject) => {
resolve(this.loadFromBase64(data));
});
}
public setNativeSource(source: any): boolean {
this.android = source;
return source != null;

View File

@ -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<boolean>;
/**
* 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<boolean>;
/**
* 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<boolean>;
/**
* 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<boolean>;
/**
* Sets the provided native source object (typically a Bitmap).

View File

@ -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 = (<any>UIImage).tns_safeImageNamed(name) || (<any>UIImage).tns_safeImageNamed(`${name}.jpg`);
return this.ios != null;
}
public fromResource(name: string): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
try {
(<any>UIImage).tns_safeDecodeImageNamedCompletion(name, image => {
if (image) {
this.ios = image;
resolve(true);
} else {
(<any>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<boolean> {
return new Promise<boolean>((resolve, reject) => {
try {
var fileName = types.isString(path) ? path.trim() : "";
if (fileName.indexOf("~/") === 0) {
fileName = fs.path.join(fs.knownFolders.currentApp().path, fileName.replace("~/", ""));
}
(<any>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<boolean> {
return new Promise<boolean>((resolve, reject) => {
try {
(<any>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<boolean> {
return new Promise<boolean>((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;
}
}

View File

@ -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": {

View File

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

View File

@ -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 = <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);
}
}
}

7
ui/image/image.d.ts vendored
View File

@ -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";
}
}