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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy