Skip to content

Commit af106de

Browse files
committed
feat: allow custom image scales on iOS
1 parent 9179ff8 commit af106de

File tree

4 files changed

+162
-51
lines changed

4 files changed

+162
-51
lines changed

packages/core/image-source/index.d.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import { ImageAsset } from '../image-asset';
22
import { Font } from '../ui/styling/font';
33
import { Color } from '../color';
4+
5+
export interface ImageSourceLoadOptions {
6+
/**
7+
* ios specific options
8+
*/
9+
ios?: {
10+
/**
11+
* The desired scale of the image.
12+
* By default it is set by the system based on a few factors:
13+
* if the image is loaded from a file, the scale is 1.0, except if you have a file@2x.png or the file itself ends in a @2x.png
14+
* in which case it will set the scale to 2 (and so on, depending on the scale of the device).
15+
* For everything else, it'll be 1.0.
16+
*/
17+
scale?: number;
18+
};
19+
}
420
/**
521
* Encapsulates the common abstraction behind a platform specific object (typically a Bitmap) that is used as a source for images.
622
*/
@@ -46,63 +62,63 @@ export class ImageSource {
4662
* Loads this instance from the specified resource name.
4763
* @param name The name of the resource (without its extension).
4864
*/
49-
static fromResourceSync(name: string): ImageSource;
65+
static fromResourceSync(name: string, options?: ImageSourceLoadOptions): ImageSource;
5066

5167
/**
5268
* Loads this instance from the specified resource name asynchronously.
5369
* @param name The name of the resource (without its extension).
5470
*/
55-
static fromResource(name: string): Promise<ImageSource>;
71+
static fromResource(name: string, options?: ImageSourceLoadOptions): Promise<ImageSource>;
5672

5773
/**
5874
* Loads this instance from the specified file.
5975
* @param path The location of the file on the file system.
6076
*/
61-
static fromFileSync(path: string): ImageSource;
77+
static fromFileSync(path: string, options?: ImageSourceLoadOptions): ImageSource;
6278

6379
/**
6480
* Loads this instance from the specified file asynchronously.
6581
* @param path The location of the file on the file system.
6682
*/
67-
static fromFile(path: string): Promise<ImageSource>;
83+
static fromFile(path: string, options?: ImageSourceLoadOptions): Promise<ImageSource>;
6884

6985
/**
7086
* Creates a new ImageSource instance and loads it from the specified local file or resource (if specified with the "res://" prefix).
7187
* @param path The location of the file on the file system.
7288
*/
73-
static fromFileOrResourceSync(path: string): ImageSource;
89+
static fromFileOrResourceSync(path: string, options?: ImageSourceLoadOptions): ImageSource;
7490

7591
/**
7692
* Loads this instance from the specified native image data.
7793
* @param data The native data (byte array) to load the image from. This will be either Stream for Android or NSData for iOS.
7894
*/
79-
static fromDataSync(data: any): ImageSource;
95+
static fromDataSync(data: any, options?: ImageSourceLoadOptions): ImageSource;
8096

8197
/**
8298
* Loads this instance from the specified native image data asynchronously.
8399
* @param data The native data (byte array) to load the image from. This will be either Stream for Android or NSData for iOS.
84100
*/
85-
static fromData(data: any): Promise<ImageSource>;
101+
static fromData(data: any, options?: ImageSourceLoadOptions): Promise<ImageSource>;
86102

87103
/**
88104
* Loads this instance from the specified base64 encoded string.
89105
* @param source The Base64 string to load the image from.
90106
*/
91-
static fromBase64Sync(source: string): ImageSource;
107+
static fromBase64Sync(source: string, options?: ImageSourceLoadOptions): ImageSource;
92108

93109
/**
94110
* Loads this instance from the specified base64 encoded string asynchronously.
95111
* @param source The Base64 string to load the image from.
96112
*/
97-
static fromBase64(source: string): Promise<ImageSource>;
113+
static fromBase64(source: string, options?: ImageSourceLoadOptions): Promise<ImageSource>;
98114

99115
/**
100116
* Creates a new ImageSource instance and loads it from the specified font icon code.
101117
* @param source The hex font icon code string
102118
* @param font The font for the corresponding font icon code
103119
* @param color The color of the generated icon image
104120
*/
105-
static fromFontIconCodeSync(source: string, font: Font, color: Color): ImageSource;
121+
static fromFontIconCodeSync(source: string, font: Font, color: Color, options?: ImageSourceLoadOptions): ImageSource;
106122

107123
/**
108124
* Creates a new ImageSource instance and sets the provided native source object (typically a Bitmap).

packages/core/image-source/index.ios.ts

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Definitions.
2-
import { ImageSource as ImageSourceDefinition } from '.';
2+
import type { ImageSource as ImageSourceDefinition, ImageSourceLoadOptions } from '.';
33
import { ImageAsset } from '../image-asset';
44
import * as httpModule from '../http';
55
import { Font } from '../ui/styling/font';
@@ -73,51 +73,77 @@ export class ImageSource implements ImageSourceDefinition {
7373
return http.getImage(url);
7474
}
7575

76-
static fromResourceSync(name: string): ImageSource {
77-
const nativeSource = (<any>UIImage).tns_safeImageNamed(name) || (<any>UIImage).tns_safeImageNamed(`${name}.jpg`);
76+
static fromResourceSync(name: string, options?: ImageSourceLoadOptions): ImageSource {
77+
const scale = options?.ios?.scale;
78+
const nativeSource = typeof scale === 'number' ? (<any>UIImage).tns_safeImageNamedScale(name, scale) || (<any>UIImage).tns_safeImageNamedScale(`${name}.jpg`, scale) : (<any>UIImage).tns_safeImageNamed(name) || (<any>UIImage).tns_safeImageNamed(`${name}.jpg`);
7879

7980
return nativeSource ? new ImageSource(nativeSource) : null;
8081
}
81-
static fromResource(name: string): Promise<ImageSource> {
82+
static fromResource(name: string, options?: ImageSourceLoadOptions): Promise<ImageSource> {
83+
const scale = options?.ios?.scale;
8284
return new Promise<ImageSource>((resolve, reject) => {
8385
try {
84-
(<any>UIImage).tns_safeDecodeImageNamedCompletion(name, (image) => {
85-
if (image) {
86-
resolve(new ImageSource(image));
87-
} else {
88-
(<any>UIImage).tns_safeDecodeImageNamedCompletion(`${name}.jpg`, (img) => {
89-
if (img) {
90-
resolve(new ImageSource(img));
91-
}
92-
});
93-
}
94-
});
86+
if (typeof scale === 'number') {
87+
(<any>UIImage).tns_safeDecodeImageNamedScaleCompletion(name, (image) => {
88+
if (image) {
89+
resolve(new ImageSource(image));
90+
} else {
91+
(<any>UIImage).tns_safeDecodeImageNamedScaleCompletion(`${name}.jpg`, (img) => {
92+
if (img) {
93+
resolve(new ImageSource(img));
94+
}
95+
});
96+
}
97+
});
98+
} else {
99+
(<any>UIImage).tns_safeDecodeImageNamedCompletion(name, (image) => {
100+
if (image) {
101+
resolve(new ImageSource(image));
102+
} else {
103+
(<any>UIImage).tns_safeDecodeImageNamedCompletion(`${name}.jpg`, (img) => {
104+
if (img) {
105+
resolve(new ImageSource(img));
106+
}
107+
});
108+
}
109+
});
110+
}
95111
} catch (ex) {
96112
reject(ex);
97113
}
98114
});
99115
}
100116

101-
static fromFileSync(path: string): ImageSource {
102-
const uiImage = UIImage.imageWithContentsOfFile(getFileName(path));
117+
static fromFileSync(path: string, options?: ImageSourceLoadOptions): ImageSource {
118+
const scale = options?.ios?.scale;
119+
const uiImage = typeof scale === 'number' ? UIImage.imageWithDataScale(NSData.dataWithContentsOfFile(getFileName(path)), scale) : UIImage.imageWithContentsOfFile(getFileName(path));
103120

104121
return uiImage ? new ImageSource(uiImage) : null;
105122
}
106-
static fromFile(path: string): Promise<ImageSource> {
123+
static fromFile(path: string, options?: ImageSourceLoadOptions): Promise<ImageSource> {
124+
const scale = options?.ios?.scale;
107125
return new Promise<ImageSource>((resolve, reject) => {
108126
try {
109-
(<any>UIImage).tns_decodeImageWidthContentsOfFileCompletion(getFileName(path), (uiImage) => {
110-
if (uiImage) {
111-
resolve(new ImageSource(uiImage));
112-
}
113-
});
127+
if (typeof scale === 'number') {
128+
(<any>UIImage).tns_decodeImageWidthContentsOfFileScaleCompletion(getFileName(path), scale, (uiImage) => {
129+
if (uiImage) {
130+
resolve(new ImageSource(uiImage));
131+
}
132+
});
133+
} else {
134+
(<any>UIImage).tns_decodeImageWidthContentsOfFileCompletion(getFileName(path), (uiImage) => {
135+
if (uiImage) {
136+
resolve(new ImageSource(uiImage));
137+
}
138+
});
139+
}
114140
} catch (ex) {
115141
reject(ex);
116142
}
117143
});
118144
}
119145

120-
static fromFileOrResourceSync(path: string): ImageSource {
146+
static fromFileOrResourceSync(path: string, options?: ImageSourceLoadOptions): ImageSource {
121147
if (!isFileOrResourcePath(path)) {
122148
if (Trace.isEnabled()) {
123149
Trace.write('Path "' + path + '" is not a valid file or resource.', Trace.categories.Binding, Trace.messageType.error);
@@ -126,48 +152,64 @@ export class ImageSource implements ImageSourceDefinition {
126152
}
127153

128154
if (path.indexOf(RESOURCE_PREFIX) === 0) {
129-
return ImageSource.fromResourceSync(path.substr(RESOURCE_PREFIX.length));
155+
return ImageSource.fromResourceSync(path.substr(RESOURCE_PREFIX.length), options);
130156
}
131157

132-
return ImageSource.fromFileSync(path);
158+
return ImageSource.fromFileSync(path, options);
133159
}
134160

135-
static fromDataSync(data: any): ImageSource {
136-
const uiImage = UIImage.imageWithData(data);
161+
static fromDataSync(data: any, options?: ImageSourceLoadOptions): ImageSource {
162+
const scale = options?.ios?.scale;
163+
const uiImage = typeof scale === 'number' ? UIImage.imageWithDataScale(data, scale) : UIImage.imageWithData(data);
137164

138165
return uiImage ? new ImageSource(uiImage) : null;
139166
}
140-
static fromData(data: any): Promise<ImageSource> {
167+
static fromData(data: any, options?: ImageSourceLoadOptions): Promise<ImageSource> {
168+
const scale = options?.ios?.scale;
141169
return new Promise<ImageSource>((resolve, reject) => {
142170
try {
143-
(<any>UIImage).tns_decodeImageWithDataCompletion(data, (uiImage) => {
144-
if (uiImage) {
145-
resolve(new ImageSource(uiImage));
146-
}
147-
});
171+
if (typeof scale === 'number') {
172+
(<any>UIImage).tns_decodeImageWithDataScaleCompletion(data, scale, (uiImage) => {
173+
if (uiImage) {
174+
resolve(new ImageSource(uiImage));
175+
}
176+
});
177+
} else {
178+
(<any>UIImage).tns_decodeImageWithDataCompletion(data, (uiImage) => {
179+
if (uiImage) {
180+
resolve(new ImageSource(uiImage));
181+
}
182+
});
183+
}
148184
} catch (ex) {
149185
reject(ex);
150186
}
151187
});
152188
}
153189

154-
static fromBase64Sync(source: string): ImageSource {
190+
static fromBase64Sync(source: string, options?: ImageSourceLoadOptions): ImageSource {
191+
const scale = options?.ios?.scale;
155192
let uiImage: UIImage;
156193
if (typeof source === 'string') {
157194
const data = NSData.alloc().initWithBase64EncodedStringOptions(source, NSDataBase64DecodingOptions.IgnoreUnknownCharacters);
158-
uiImage = UIImage.imageWithData(data);
195+
if (typeof scale === 'number') {
196+
uiImage = UIImage.imageWithDataScale(data, scale);
197+
} else {
198+
uiImage = UIImage.imageWithData(data);
199+
}
159200
}
160201

161202
return uiImage ? new ImageSource(uiImage) : null;
162203
}
163-
static fromBase64(source: string): Promise<ImageSource> {
204+
static fromBase64(source: string, options?: ImageSourceLoadOptions): Promise<ImageSource> {
205+
const scale = options?.ios?.scale;
164206
return new Promise<ImageSource>((resolve, reject) => {
165207
try {
166208
const data = NSData.alloc().initWithBase64EncodedStringOptions(source, NSDataBase64DecodingOptions.IgnoreUnknownCharacters);
167209
const main_queue = dispatch_get_current_queue();
168210
const background_queue = dispatch_get_global_queue(qos_class_t.QOS_CLASS_DEFAULT, 0);
169211
dispatch_async(background_queue, () => {
170-
const uiImage = UIImage.imageWithData(data);
212+
const uiImage = typeof scale === 'number' ? UIImage.imageWithDataScale(data, scale) : UIImage.imageWithData(data);
171213
dispatch_async(main_queue, () => {
172214
resolve(new ImageSource(uiImage));
173215
});
@@ -178,8 +220,9 @@ export class ImageSource implements ImageSourceDefinition {
178220
});
179221
}
180222

181-
static fromFontIconCodeSync(source: string, font: Font, color: Color): ImageSource {
223+
static fromFontIconCodeSync(source: string, font: Font, color: Color, options?: ImageSourceLoadOptions): ImageSource {
182224
font = font || Font.default;
225+
const scale = typeof options?.ios?.scale === 'number' ? options.ios.scale : 0.0;
183226

184227
// TODO: Consider making 36 font size as default for optimal look on TabView and ActionBar
185228
const attributes = {
@@ -192,7 +235,7 @@ export class ImageSource implements ImageSourceDefinition {
192235

193236
const attributedString = NSAttributedString.alloc().initWithStringAttributes(source, <NSDictionary<string, any>>attributes);
194237

195-
UIGraphicsBeginImageContextWithOptions(attributedString.size(), false, 0.0);
238+
UIGraphicsBeginImageContextWithOptions(attributedString.size(), false, scale);
196239
attributedString.drawAtPoint(CGPointMake(0, 0));
197240

198241
const iconImage = UIGraphicsGetImageFromCurrentImageContext();

packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@
1313
* It also draws the UIImage in a small thumb to force decoding potentially avoiding UI hicckups when displayed.
1414
*/
1515
+ (void) tns_safeDecodeImageNamed: (NSString*) name completion: (void (^) (UIImage*))callback;
16-
16+
+ (void) tns_safeDecodeImageNamed: (NSString*) name scale: (CGFloat) scale completion: (void (^) (UIImage*))callback;
1717
/**
1818
* Same as imageNamed, however calls to this method are sinchronized to be thread safe in iOS8 along with calls to tns_safeImageNamed and tns_safeDecodeImageNamed:completion:
1919
* imageNamed is thread safe in iOS 9 and later so in later versions this methods simply fallbacks to imageNamed:
2020
*/
2121
+ (UIImage*) tns_safeImageNamed: (NSString*) name;
22+
+ (UIImage*) tns_safeImageNamed: (NSString*) name scale: (CGFloat) scale;
2223

2324
+ (void) tns_decodeImageWithData: (NSData*) data completion: (void (^) (UIImage*))callback;
25+
+ (void) tns_decodeImageWithData: (NSData*) data scale: (CGFloat)scale completion: (void (^) (UIImage*))callback;
26+
27+
+ (void) tns_decodeImageWidthContentsOfFile: (NSString*) file scale: (CGFloat)scale completion: (void (^) (UIImage*))callback;
2428
+ (void) tns_decodeImageWidthContentsOfFile: (NSString*) file completion: (void (^) (UIImage*))callback;
2529

2630
@end

packages/ui-mobile-base/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.m

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ + (void) tns_safeDecodeImageNamed: (NSString*) name completion: (void (^) (UIIma
3939
}
4040
});
4141
}
42+
+ (void) tns_safeDecodeImageNamed: (NSString*) name scale:(CGFloat)scale completion: (void (^) (UIImage*))callback {
43+
dispatch_async(image_queue, ^(void){
44+
@autoreleasepool {
45+
UIImage* image = [UIImage imageNamed: name];
46+
image = [UIImage imageWithCGImage:[image CGImage] scale:scale orientation:[image imageOrientation]];
47+
[image tns_forceDecode];
48+
49+
dispatch_async(dispatch_get_main_queue(), ^(void) {
50+
callback(image);
51+
});
52+
}
53+
});
54+
}
4255

4356
+ (UIImage*) tns_safeImageNamed: (NSString*) name {
4457
UIImage* image;
@@ -48,6 +61,15 @@ + (UIImage*) tns_safeImageNamed: (NSString*) name {
4861
return image;
4962
}
5063

64+
+ (UIImage*) tns_safeImageNamed: (NSString*) name scale:(CGFloat)scale {
65+
UIImage* image;
66+
@autoreleasepool {
67+
image = [UIImage imageNamed: name];
68+
image = [UIImage imageWithCGImage:[image CGImage] scale:scale orientation:[image imageOrientation]];
69+
}
70+
return image;
71+
}
72+
5173
+ (void) tns_decodeImageWithData: (NSData*) data completion: (void (^) (UIImage*))callback {
5274
dispatch_async(image_queue, ^(void) {
5375
@autoreleasepool {
@@ -61,6 +83,19 @@ + (void) tns_decodeImageWithData: (NSData*) data completion: (void (^) (UIImage*
6183
});
6284
}
6385

86+
+ (void) tns_decodeImageWithData:(NSData *)data scale:(CGFloat)scale completion:(void (^)(UIImage *))callback {
87+
dispatch_async(image_queue, ^(void) {
88+
@autoreleasepool {
89+
UIImage* image = [UIImage imageWithData: data scale: scale];
90+
[image tns_forceDecode];
91+
92+
dispatch_async(dispatch_get_main_queue(), ^(void) {
93+
callback(image);
94+
});
95+
}
96+
});
97+
}
98+
6499
+ (void) tns_decodeImageWidthContentsOfFile: (NSString*) file completion: (void (^) (UIImage*))callback {
65100
dispatch_async(image_queue, ^(void) {
66101
@autoreleasepool {
@@ -74,4 +109,17 @@ + (void) tns_decodeImageWidthContentsOfFile: (NSString*) file completion: (void
74109
});
75110
}
76111

112+
+ (void) tns_decodeImageWidthContentsOfFile: (NSString*) file scale: (CGFloat) scale completion: (void (^) (UIImage*))callback {
113+
dispatch_async(image_queue, ^(void) {
114+
@autoreleasepool {
115+
UIImage* image = [UIImage imageWithData:[NSData dataWithContentsOfFile:file] scale:scale];
116+
[image tns_forceDecode];
117+
118+
dispatch_async(dispatch_get_main_queue(), ^(void) {
119+
callback(image);
120+
});
121+
}
122+
});
123+
}
124+
77125
@end

0 commit comments

Comments
 (0)