From 9d3977ea4fd6d6aedeab55e0472999c05ed5a7bd Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Wed, 16 Feb 2022 16:16:32 -0800 Subject: [PATCH] perf(ios): uifont and formatted string optimizations plus uiimage scaling (#9761) --- packages/core/image-asset/index.ios.ts | 10 +- packages/core/image-source/index.ios.ts | 13 +- .../platforms/ios/src/NativeScriptUtils.h | 16 + .../platforms/ios/src/NativeScriptUtils.m | 134 ++++++++ .../platforms/ios/src/UIView+NativeScript.h | 13 + .../platforms/ios/src/UIView+NativeScript.m | 127 +++++++ .../core/platforms/ios/src/module.modulemap | 10 + .../ios/typings/objc!NativeScriptUtils.d.ts | 19 ++ packages/core/references.d.ts | 1 + packages/core/ui/styling/font-common.ts | 20 +- packages/core/ui/styling/font.ios.ts | 120 +------ packages/core/ui/text-base/index.ios.ts | 313 +++++------------- 12 files changed, 429 insertions(+), 367 deletions(-) create mode 100644 packages/core/platforms/ios/src/NativeScriptUtils.h create mode 100644 packages/core/platforms/ios/src/NativeScriptUtils.m create mode 100644 packages/core/platforms/ios/src/UIView+NativeScript.h create mode 100644 packages/core/platforms/ios/src/UIView+NativeScript.m create mode 100644 packages/core/platforms/ios/typings/objc!NativeScriptUtils.d.ts diff --git a/packages/core/image-asset/index.ios.ts b/packages/core/image-asset/index.ios.ts index 67b0b912b..ef3e58b13 100644 --- a/packages/core/image-asset/index.ios.ts +++ b/packages/core/image-asset/index.ios.ts @@ -62,14 +62,6 @@ export class ImageAsset extends ImageAssetBase { } private scaleImage(image: UIImage, requestedSize: { width: number; height: number }): UIImage { - // scaleFactor = 0 takes the scale factor of the devices's main screen. - const scaleFactor = this.options && this.options.autoScaleFactor === false ? 1.0 : 0.0; - - UIGraphicsBeginImageContextWithOptions(requestedSize, false, scaleFactor); - image.drawInRect(CGRectMake(0, 0, requestedSize.width, requestedSize.height)); - const resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return resultImage; + return NativeScriptUtils.scaleImageWidthHeightScaleFactor(image, requestedSize.width, requestedSize.height, this.options?.autoScaleFactor === false ? 1.0 : 0.0); } } diff --git a/packages/core/image-source/index.ios.ts b/packages/core/image-source/index.ios.ts index 41ab10eb1..11112c44a 100644 --- a/packages/core/image-source/index.ios.ts +++ b/packages/core/image-source/index.ios.ts @@ -440,18 +440,7 @@ function getFileName(path: string): string { } function getImageData(instance: UIImage, format: 'png' | 'jpeg' | 'jpg', quality = 0.9): NSData { - let data = null; - switch (format) { - case 'png': - data = UIImagePNGRepresentation(instance); - break; - case 'jpeg': - case 'jpg': - data = UIImageJPEGRepresentation(instance, quality); - break; - } - - return data; + return NativeScriptUtils.getImageDataFormatQuality(instance, format, quality); } export function fromAsset(asset: ImageAsset): Promise { diff --git a/packages/core/platforms/ios/src/NativeScriptUtils.h b/packages/core/platforms/ios/src/NativeScriptUtils.h new file mode 100644 index 000000000..4676a8afd --- /dev/null +++ b/packages/core/platforms/ios/src/NativeScriptUtils.h @@ -0,0 +1,16 @@ +// +// NativeScriptUtils.h +// +// Created by Nathan Walker on 2/02/2022. +#include + +@interface NativeScriptUtils : NSObject + ++(UIFont*) getSystemFont:(CGFloat)size weight:(UIFontWeight)weight italic:(BOOL)italic symbolicTraits:(CGFloat)symbolicTraits; ++(UIFont*) createUIFont:(NSDictionary*)font; ++(NSMutableAttributedString*)createMutableStringWithDetails:(NSDictionary*)details; ++(NSMutableAttributedString*)createMutableStringForSpan:(NSString*)text font:(UIFont*)font color:(UIColor*)color backgroundColor:(UIColor*)backgroundColor textDecoration:(NSString*)textDecoration baselineOffset:(CGFloat)baselineOffset; ++(UIImage*)scaleImage:(UIImage*)image width:(CGFloat)width height:(CGFloat)height scaleFactor:(CGFloat)scaleFactor; ++(NSData*)getImageData:(UIImage*)image format:(NSString*)format quality:(CGFloat)quality; + +@end diff --git a/packages/core/platforms/ios/src/NativeScriptUtils.m b/packages/core/platforms/ios/src/NativeScriptUtils.m new file mode 100644 index 000000000..b53a6e8d8 --- /dev/null +++ b/packages/core/platforms/ios/src/NativeScriptUtils.m @@ -0,0 +1,134 @@ +#import "NativeScriptUtils.h" + +@implementation NativeScriptUtils + ++(UIFont*) getSystemFont:(CGFloat)size weight:(UIFontWeight)weight italic:(BOOL)italic symbolicTraits:(CGFloat)symbolicTraits { + UIFont *result = [UIFont systemFontOfSize:size weight:weight]; + if (italic) { + result = [UIFont fontWithDescriptor:[result.fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits] size:size]; + } + + return result; +} ++(UIFont*) createUIFont:(NSDictionary*)font { + UIFont *result; + + CGFloat size = [[font valueForKey:@"fontSize"] floatValue]; + + UIFontDescriptorSymbolicTraits symbolicTraits = 0; + if ([[font valueForKey:@"isBold"] boolValue]) { + symbolicTraits = symbolicTraits | UIFontDescriptorTraitBold; + } + if ([[font valueForKey:@"isItalic"] boolValue]) { + symbolicTraits = symbolicTraits | UIFontDescriptorTraitItalic; + } + + NSDictionary *fontDescriptorTraits = @{ + UIFontSymbolicTrait : @(symbolicTraits), + UIFontWeightTrait : [font valueForKey:@"fontWeight"] + }; + + for (NSString *family in [NSArray arrayWithArray:[font valueForKey:@"fontFamily"]]) { + NSString *fontFamily = family; + if ([family.lowercaseString isEqualToString:@"serif"]) { + fontFamily = @"Times New Roman"; + } else if ([family.lowercaseString isEqualToString:@"monospace"]) { + fontFamily = @"Courier New"; + } + if (!fontFamily || [fontFamily isEqualToString:@"sans-serif"] || [fontFamily isEqualToString:@"system"]) { + result = [NativeScriptUtils getSystemFont:size weight:(UIFontWeight)[[font valueForKey:@"fontWeight"] floatValue] italic:[[font valueForKey:@"isItalic"] boolValue] symbolicTraits:symbolicTraits]; + break; + } else { + + UIFontDescriptor *descriptor = [UIFontDescriptor fontDescriptorWithFontAttributes:@{ + UIFontDescriptorFamilyAttribute: fontFamily, + UIFontDescriptorTraitsAttribute: fontDescriptorTraits + }]; + + result = [UIFont fontWithDescriptor:descriptor size:size]; + + BOOL actualItalic = result.fontDescriptor.symbolicTraits & UIFontDescriptorTraitItalic; + if ([[font valueForKey:@"isItalic"] boolValue] && !actualItalic) { + // The font we got is not actually italic so emulate that with a matrix + result = [UIFont fontWithDescriptor:[descriptor fontDescriptorWithMatrix:CGAffineTransformMake(1, 0, 0.2, 1, 0, 0)] size:size]; + } + + // Check if the resolved font has the correct font-family + // If not - fallback to the next font-family + if ([result.familyName isEqualToString:fontFamily]) { + break; + } else { + result = nil; + } + } + } + + // Couldn't resolve font - fallback to the system font + if (result == nil) { + result = [NativeScriptUtils getSystemFont:size weight:(UIFontWeight)[[font valueForKey:@"fontWeight"] floatValue] italic:[[font valueForKey:@"isItalic"] boolValue] symbolicTraits:symbolicTraits]; + } + + return result; +} + ++(NSMutableAttributedString*)createMutableStringWithDetails:(NSDictionary*)details { + NSMutableAttributedString *mas = [[NSMutableAttributedString alloc] init]; + for (NSDictionary *detail in [NSArray arrayWithArray:[details valueForKey:@"spans"]]) { + NSMutableAttributedString *attrString = [NativeScriptUtils createMutableStringForSpan:[detail objectForKey:@"text"] font:[detail objectForKey:@"iosFont"] color:[detail objectForKey:@"color"] backgroundColor:[detail objectForKey:@"backgroundColor"] textDecoration:[detail objectForKey:@"textDecoration"] baselineOffset:[[detail valueForKey:@"baselineOffset"] floatValue]]; + [mas insertAttributedString:attrString atIndex:[[detail valueForKey:@"index"] intValue]]; + } + return mas; +} + ++(NSMutableAttributedString*)createMutableStringForSpan:(NSString*)text font:(UIFont*)font color:(UIColor*)color backgroundColor:(UIColor*)backgroundColor textDecoration:(NSString*)textDecoration baselineOffset:(CGFloat)baselineOffset { + NSMutableDictionary *attrDict = [[NSMutableDictionary alloc] init]; + attrDict[NSFontAttributeName] = font; + + if (color != nil) { + attrDict[NSForegroundColorAttributeName] = color; + } + + if (backgroundColor != nil) { + attrDict[NSBackgroundColorAttributeName] = backgroundColor; + } + + if (textDecoration != nil) { + if ([textDecoration rangeOfString:@"underline"].location != NSNotFound) { + attrDict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleSingle]; + } + + if ([textDecoration rangeOfString:@"line-through"].location != NSNotFound) { + attrDict[NSStrikethroughStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleSingle]; + } + } + + attrDict[NSBaselineOffsetAttributeName] = [NSNumber numberWithInt:baselineOffset]; + + return [[NSMutableAttributedString alloc] initWithString:text attributes:attrDict]; + +} + ++(UIImage*)scaleImage:(UIImage*)image width:(CGFloat)width height:(CGFloat)height scaleFactor:(CGFloat)scaleFactor { + UIImage *resultImage; + @autoreleasepool { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, scaleFactor); + [image drawInRect:CGRectMake(0, 0, width, height)]; + resultImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + return resultImage; +} + ++(NSData*)getImageData:(UIImage*)image format:(NSString*)format quality:(CGFloat)quality { + NSData *data; + @autoreleasepool { + if ([format.lowercaseString isEqualToString:@"png"]) { + data = UIImagePNGRepresentation(image); + } else { + data = UIImageJPEGRepresentation(image, quality); + } + } + return data; +} + +@end diff --git a/packages/core/platforms/ios/src/UIView+NativeScript.h b/packages/core/platforms/ios/src/UIView+NativeScript.h new file mode 100644 index 000000000..aae428144 --- /dev/null +++ b/packages/core/platforms/ios/src/UIView+NativeScript.h @@ -0,0 +1,13 @@ +// +// UIView+NativeScript.h +// +// Created by Nathan Walker on 2/02/2022. +#include + +@interface UIView (NativeScript) + +- (void)nativeScriptSetTextDecorationAndTransform:(NSString*)text textDecoration:(NSString*)textDecoration letterSpacing:(CGFloat)letterSpacing lineHeight:(CGFloat)lineHeight; + +-(void)nativeScriptSetFormattedTextDecorationAndTransform:(NSDictionary*)details; + +@end diff --git a/packages/core/platforms/ios/src/UIView+NativeScript.m b/packages/core/platforms/ios/src/UIView+NativeScript.m new file mode 100644 index 000000000..e811f6d0f --- /dev/null +++ b/packages/core/platforms/ios/src/UIView+NativeScript.m @@ -0,0 +1,127 @@ +#import +#import "UIView+NativeScript.h" +#import "NativeScriptUtils.h" + +@implementation UIView (NativeScript) +- (void)nativeScriptSetTextDecorationAndTransform:(NSString*)text textDecoration:(NSString*)textDecoration letterSpacing:(CGFloat)letterSpacing lineHeight:(CGFloat)lineHeight { + NSMutableDictionary *attrDict = [[NSMutableDictionary alloc] init]; + + if ([textDecoration rangeOfString:@"underline"].location != NSNotFound) { + attrDict[NSUnderlineStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleSingle]; + } + + if ([textDecoration rangeOfString:@"line-through"].location != NSNotFound) { + attrDict[NSStrikethroughStyleAttributeName] = [NSNumber numberWithInt:NSUnderlineStyleSingle]; + } + BOOL isTextType = [self isKindOfClass:[UITextField class]] || [self isKindOfClass:[UITextView class]] | [self isKindOfClass:[UILabel class]] | [self isKindOfClass:[UIButton class]]; + + if (letterSpacing != 0 && isTextType && ((UITextView*)self).font != nil) { + NSNumber *kern = [NSNumber numberWithDouble:letterSpacing * ((UITextView*)self).font.pointSize]; + attrDict[NSKernAttributeName] = kern; + if ([self isKindOfClass:[UITextField class]]) { + [((UITextField*)self).defaultTextAttributes setValue:kern forKey:NSKernAttributeName]; + } + } + + BOOL isTextView = [self isKindOfClass:[UITextView class]]; + if (lineHeight > 0) { + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.lineSpacing = lineHeight; + // make sure a possible previously set text alignment setting is not lost when line height is specified + if ([self isKindOfClass:[UIButton class]]) { + paragraphStyle.alignment = ((UIButton*)self).titleLabel.textAlignment; + } else { + paragraphStyle.alignment = ((UILabel*)self).textAlignment; + } + + if ([self isKindOfClass:[UILabel class]]) { + // make sure a possible previously set line break mode is not lost when line height is specified + paragraphStyle.lineBreakMode = ((UILabel*)self).lineBreakMode; + } + attrDict[NSParagraphStyleAttributeName] = paragraphStyle; + } else if (isTextView) { + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = ((UITextView*)self).textAlignment; + attrDict[NSParagraphStyleAttributeName] = paragraphStyle; + } + + if (attrDict.count > 0 || isTextView) { + if (isTextView && ((UITextView*)self).font) { + // UITextView's font seems to change inside. + attrDict[NSFontAttributeName] = ((UITextView*)self).font; + } + + NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:text]; + [result setAttributes:attrDict range:(NSRange){ + 0, + text.length + }]; + + if ([self isKindOfClass:[UIButton class]]) { + [(UIButton*)self setAttributedTitle:result forState:UIControlStateNormal]; + } else { + ((UITextView*)self).attributedText = result; + } + } else { + if ([self isKindOfClass:[UIButton class]]) { + // Clear attributedText or title won't be affected. + [(UIButton*)self setAttributedTitle:nil forState:UIControlStateNormal]; + [(UIButton*)self setTitle:text forState:UIControlStateNormal]; + } else { + // Clear attributedText or text won't be affected. + ((UILabel*)self).attributedText = nil; + ((UILabel*)self).text = text; + } + } +} + +-(void)nativeScriptSetFormattedTextDecorationAndTransform:(NSDictionary*)details { + CGFloat letterSpacing = [[details valueForKey:@"letterSpacing"] doubleValue]; + CGFloat lineHeight = [[details valueForKey:@"lineHeight"] doubleValue]; + NSMutableAttributedString *attrText = [NativeScriptUtils createMutableStringWithDetails:details]; + if (letterSpacing != 0) { + NSNumber *kern = [NSNumber numberWithDouble:letterSpacing * ((UITextView*)self).font.pointSize]; + [attrText addAttribute:NSKernAttributeName value:kern range:(NSRange){ + 0, + attrText.length + } ]; + } + + if (lineHeight > 0) { + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.minimumLineHeight = lineHeight; + // make sure a possible previously set text alignment setting is not lost when line height is specified + if ([self isKindOfClass:[UIButton class]]) { + paragraphStyle.alignment = ((UIButton*)self).titleLabel.textAlignment; + } else { + paragraphStyle.alignment = ((UILabel*)self).textAlignment; + } + + if ([self isKindOfClass:[UILabel class]]) { + // make sure a possible previously set line break mode is not lost when line height is specified + paragraphStyle.lineBreakMode = ((UILabel*)self).lineBreakMode; + } + [attrText addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:(NSRange){ + 0, + attrText.length + }]; + } else if ([self isKindOfClass:[UITextView class]]) { + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.alignment = ((UITextView*)self).textAlignment; + [attrText addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:(NSRange){ + 0, + attrText.length + }]; + } + + if ([self isKindOfClass:[UIButton class]]) { + [(UIButton*)self setAttributedTitle:attrText forState:UIControlStateNormal]; + } else { + if (@available(iOS 13.0, *)) { + ((UILabel*)self).textColor = [UIColor labelColor]; + } + + ((UILabel*)self).attributedText = attrText; + } +} +@end diff --git a/packages/core/platforms/ios/src/module.modulemap b/packages/core/platforms/ios/src/module.modulemap index bba0f5d84..dd044dfcc 100644 --- a/packages/core/platforms/ios/src/module.modulemap +++ b/packages/core/platforms/ios/src/module.modulemap @@ -2,3 +2,13 @@ module NativeScriptEmbedder { header "NativeScriptEmbedder.h" export * } + +module NativeScriptUtils { + header "NativeScriptUtils.h" + export * +} + +module UIViewNativeScript { + header "UIView+NativeScript.h" + export * +} \ No newline at end of file diff --git a/packages/core/platforms/ios/typings/objc!NativeScriptUtils.d.ts b/packages/core/platforms/ios/typings/objc!NativeScriptUtils.d.ts new file mode 100644 index 000000000..d77b2074a --- /dev/null +++ b/packages/core/platforms/ios/typings/objc!NativeScriptUtils.d.ts @@ -0,0 +1,19 @@ + +declare class NativeScriptUtils extends NSObject { + + static alloc(): NativeScriptUtils; // inherited from NSObject + + static createMutableStringForSpanFontColorBackgroundColorTextDecorationBaselineOffset(text: string, font: UIFont, color: UIColor, backgroundColor: UIColor, textDecoration: string, baselineOffset: number): NSMutableAttributedString; + + static createMutableStringWithDetails(details: NSDictionary): NSMutableAttributedString; + + static createUIFont(font: NSDictionary): UIFont; + + static getImageDataFormatQuality(image: UIImage, format: string, quality: number): NSData; + + static getSystemFontWeightItalicSymbolicTraits(size: number, weight: number, italic: boolean, symbolicTraits: number): UIFont; + + static new(): NativeScriptUtils; // inherited from NSObject + + static scaleImageWidthHeightScaleFactor(image: UIImage, width: number, height: number, scaleFactor: number): UIImage; +} diff --git a/packages/core/references.d.ts b/packages/core/references.d.ts index 70be1dec6..829600632 100644 --- a/packages/core/references.d.ts +++ b/packages/core/references.d.ts @@ -1,4 +1,5 @@ /// /// /// +/// /// diff --git a/packages/core/ui/styling/font-common.ts b/packages/core/ui/styling/font-common.ts index bef98e656..782cfb013 100644 --- a/packages/core/ui/styling/font-common.ts +++ b/packages/core/ui/styling/font-common.ts @@ -17,8 +17,8 @@ export abstract class Font implements FontDefinition { protected constructor(public readonly fontFamily: string, public readonly fontSize: number, public readonly fontStyle: FontStyleType, public readonly fontWeight: FontWeightType, public readonly fontScale: number) {} - public abstract getAndroidTypeface(): any /* android.graphics.Typeface */; - public abstract getUIFont(defaultFont: any /* UIFont */): any /* UIFont */; + public abstract getAndroidTypeface(): any; /* android.graphics.Typeface */ + public abstract getUIFont(defaultFont: any /* UIFont */): any; /* UIFont */ public abstract withFontFamily(family: string): Font; public abstract withFontStyle(style: string): Font; public abstract withFontWeight(weight: string): Font; @@ -64,20 +64,14 @@ export namespace FontWeight { } export function parseFontFamily(value: string): Array { - const result = new Array(); if (!value) { - return result; + return []; } - const split = value.split(','); - for (let i = 0; i < split.length; i++) { - const str = split[i].trim().replace(/['"]+/g, ''); - if (str) { - result.push(str); - } - } - - return result; + return value + .split(',') + .map((v) => (v || '').trim().replace(/['"]+/g, '')) + .filter((v) => !!v); } export namespace genericFontFamilies { diff --git a/packages/core/ui/styling/font.ios.ts b/packages/core/ui/styling/font.ios.ts index 7350232b5..5fc7267fc 100644 --- a/packages/core/ui/styling/font.ios.ts +++ b/packages/core/ui/styling/font.ios.ts @@ -1,15 +1,8 @@ -import { Font as FontBase, parseFontFamily, genericFontFamilies, FontStyle, FontWeight, FontStyleType, FontWeightType } from './font-common'; +import { Font as FontBase, parseFontFamily, FontStyle, FontWeight, FontStyleType, FontWeightType } from './font-common'; import { Trace } from '../../trace'; -import { Device } from '../../platform'; import * as fs from '../../file-system'; export * from './font-common'; -const EMULATE_OBLIQUE = true; -const OBLIQUE_TRANSFORM = CGAffineTransformMake(1, 0, 0.2, 1, 0, 0); -const DEFAULT_SERIF = 'Times New Roman'; -const DEFAULT_MONOSPACE = 'Courier New'; -const SUPPORT_FONT_WEIGHTS = parseFloat(Device.osVersion) >= 10.0; - export class Font extends FontBase { public static default = new Font(undefined, undefined, FontStyle.NORMAL, FontWeight.NORMAL, 1); @@ -40,11 +33,15 @@ export class Font extends FontBase { } public getUIFont(defaultFont: UIFont): UIFont { - if (!this._uiFont) { - this._uiFont = createUIFont(this, defaultFont); - } - - return this._uiFont; + return new WeakRef( + NativeScriptUtils.createUIFont({ + fontFamily: parseFontFamily(this.fontFamily), + fontSize: this.fontSize || defaultFont.pointSize, + fontWeight: getNativeFontWeight(this.fontWeight), + isBold: this.isBold, + isItalic: this.isItalic, + }) + ).get(); } public getAndroidTypeface(): android.graphics.Typeface { @@ -52,27 +49,6 @@ export class Font extends FontBase { } } -function getFontFamilyRespectingGenericFonts(fontFamily: string): string { - if (!fontFamily) { - return fontFamily; - } - - switch (fontFamily.toLowerCase()) { - case genericFontFamilies.serif: - return DEFAULT_SERIF; - - case genericFontFamilies.monospace: - return DEFAULT_MONOSPACE; - - default: - return fontFamily; - } -} - -function shouldUseSystemFont(fontFamily: string) { - return !fontFamily || fontFamily === genericFontFamilies.sansSerif || fontFamily === genericFontFamilies.system; -} - function getNativeFontWeight(fontWeight: FontWeightType): number { switch (fontWeight) { case FontWeight.THIN: @@ -98,84 +74,10 @@ function getNativeFontWeight(fontWeight: FontWeightType): number { case FontWeight.BLACK: return UIFontWeightBlack; default: - throw new Error(`Invalid font weight: "${fontWeight}"`); + console.log(`Invalid font weight: "${fontWeight}"`); } } -function getSystemFont(size: number, nativeWeight: number, italic: boolean, symbolicTraits: number): UIFont { - let result = UIFont.systemFontOfSizeWeight(size, nativeWeight); - if (italic) { - const descriptor = result.fontDescriptor.fontDescriptorWithSymbolicTraits(symbolicTraits); - result = UIFont.fontWithDescriptorSize(descriptor, size); - } - - return result; -} - -function createUIFont(font: Font, defaultFont: UIFont): UIFont { - let result: UIFont; - const size = font.fontSize || defaultFont.pointSize; - const nativeWeight = getNativeFontWeight(font.fontWeight); - const fontFamilies = parseFontFamily(font.fontFamily); - - let symbolicTraits = 0; - if (font.isBold) { - symbolicTraits |= UIFontDescriptorSymbolicTraits.TraitBold; - } - if (font.isItalic) { - symbolicTraits |= UIFontDescriptorSymbolicTraits.TraitItalic; - } - - const fontDescriptorTraits = { - [UIFontSymbolicTrait]: symbolicTraits, - }; - - // IOS versions below 10 will not return the correct font when UIFontWeightTrait is set - // In this case - rely on UIFontSymbolicTrait only - if (SUPPORT_FONT_WEIGHTS) { - fontDescriptorTraits[UIFontWeightTrait] = nativeWeight; - } - - for (let i = 0; i < fontFamilies.length; i++) { - const fontFamily = getFontFamilyRespectingGenericFonts(fontFamilies[i]); - - if (shouldUseSystemFont(fontFamily)) { - result = getSystemFont(size, nativeWeight, font.isItalic, symbolicTraits); - break; - } else { - const fontAttributes = { - [UIFontDescriptorFamilyAttribute]: fontFamily, - [UIFontDescriptorTraitsAttribute]: fontDescriptorTraits, - }; - - let descriptor = UIFontDescriptor.fontDescriptorWithFontAttributes(fontAttributes); - result = UIFont.fontWithDescriptorSize(descriptor, size); - - const actualItalic = result.fontDescriptor.symbolicTraits & UIFontDescriptorSymbolicTraits.TraitItalic; - if (font.isItalic && !actualItalic && EMULATE_OBLIQUE) { - // The font we got is not actually italic so emulate that with a matrix - descriptor = descriptor.fontDescriptorWithMatrix(OBLIQUE_TRANSFORM); - result = UIFont.fontWithDescriptorSize(descriptor, size); - } - - // Check if the resolved font has the correct font-family - // If not - fallback to the next font-family - if (result.familyName === fontFamily) { - break; - } else { - result = null; - } - } - } - - // Couldn't resolve font - fallback to the system font - if (!result) { - result = getSystemFont(size, nativeWeight, font.isItalic, symbolicTraits); - } - - return result; -} - export namespace ios { export function registerFont(fontFile: string) { let filePath = fs.path.join(fs.knownFolders.currentApp().path, 'fonts', fontFile); diff --git a/packages/core/ui/text-base/index.ios.ts b/packages/core/ui/text-base/index.ios.ts index 5a1514fb9..4e8c302f2 100644 --- a/packages/core/ui/text-base/index.ios.ts +++ b/packages/core/ui/text-base/index.ios.ts @@ -173,7 +173,7 @@ export class TextBase extends TextBaseCommon { break; case 'justify': nativeView.textAlignment = NSTextAlignment.Justified; - break; + break; } } @@ -197,6 +197,15 @@ export class TextBase extends TextBaseCommon { this._setShadow(value); } + _setColor(color: UIColor): void { + if (this.nativeTextViewProtected instanceof UIButton) { + this.nativeTextViewProtected.setTitleColorForState(color, UIControlState.Normal); + this.nativeTextViewProtected.titleLabel.textColor = color; + } else { + this.nativeTextViewProtected.textColor = color; + } + } + _setNativeText(reset = false): void { if (reset) { const nativeView = this.nativeTextViewProtected; @@ -214,144 +223,102 @@ export class TextBase extends TextBaseCommon { } if (this.formattedText) { - this.setFormattedTextDecorationAndTransform(); + (this.nativeTextViewProtected).nativeScriptSetFormattedTextDecorationAndTransform(this.getFormattedStringDetails(this.formattedText)); } else { - this.setTextDecorationAndTransform(); - } - } + // console.log('setTextDecorationAndTransform...') + const text = getTransformedText(isNullOrUndefined(this.text) ? '' : `${this.text}`, this.textTransform); + (this.nativeTextViewProtected).nativeScriptSetTextDecorationAndTransformTextDecorationLetterSpacingLineHeight(text, this.style.textDecoration || '', this.style.letterSpacing !== 0 ? this.style.letterSpacing : 0, this.style.lineHeight ? this.style.lineHeight : 0); - _setColor(color: UIColor): void { - if (this.nativeTextViewProtected instanceof UIButton) { - this.nativeTextViewProtected.setTitleColorForState(color, UIControlState.Normal); - this.nativeTextViewProtected.titleLabel.textColor = color; - } else { - this.nativeTextViewProtected.textColor = color; + if (!this.style?.color && majorVersion >= 13 && UIColor.labelColor) { + this._setColor(UIColor.labelColor); + } } } createFormattedTextNative(value: FormattedString) { - return this.createNSMutableAttributedString(value); - } - setFormattedTextDecorationAndTransform() { - const attrText = this.createFormattedTextNative(this.formattedText); - // TODO: letterSpacing should be applied per Span. - if (this.letterSpacing !== 0) { - attrText.addAttributeValueRange(NSKernAttributeName, this.letterSpacing * this.nativeTextViewProtected.font.pointSize, { location: 0, length: attrText.length }); - } - - if (this.style.lineHeight) { - const paragraphStyle = NSMutableParagraphStyle.alloc().init(); - paragraphStyle.minimumLineHeight = this.lineHeight; - // make sure a possible previously set text alignment setting is not lost when line height is specified - if (this.nativeTextViewProtected instanceof UIButton) { - paragraphStyle.alignment = (this.nativeTextViewProtected).titleLabel.textAlignment; - } else { - paragraphStyle.alignment = (this.nativeTextViewProtected).textAlignment; - } - - if (this.nativeTextViewProtected instanceof UILabel) { - // make sure a possible previously set line break mode is not lost when line height is specified - paragraphStyle.lineBreakMode = this.nativeTextViewProtected.lineBreakMode; - } - attrText.addAttributeValueRange(NSParagraphStyleAttributeName, paragraphStyle, { location: 0, length: attrText.length }); - } else if (this.nativeTextViewProtected instanceof UITextView) { - const paragraphStyle = NSMutableParagraphStyle.alloc().init(); - paragraphStyle.alignment = (this.nativeTextViewProtected).textAlignment; - attrText.addAttributeValueRange(NSParagraphStyleAttributeName, paragraphStyle, { location: 0, length: attrText.length }); - } - - if (this.nativeTextViewProtected instanceof UIButton) { - this.nativeTextViewProtected.setAttributedTitleForState(attrText, UIControlState.Normal); - } else { - if (majorVersion >= 13 && UIColor.labelColor) { - this.nativeTextViewProtected.textColor = UIColor.labelColor; - } - - this.nativeTextViewProtected.attributedText = attrText; - } + return NativeScriptUtils.createMutableStringWithDetails(this.getFormattedStringDetails(value)); } - setTextDecorationAndTransform() { - const style = this.style; - const dict = new Map(); - switch (style.textDecoration) { - case 'none': - break; - case 'underline': - dict.set(NSUnderlineStyleAttributeName, NSUnderlineStyle.Single); - break; - case 'line-through': - dict.set(NSStrikethroughStyleAttributeName, NSUnderlineStyle.Single); - break; - case 'underline line-through': - dict.set(NSUnderlineStyleAttributeName, NSUnderlineStyle.Single); - dict.set(NSStrikethroughStyleAttributeName, NSUnderlineStyle.Single); - break; - default: - throw new Error(`Invalid text decoration value: ${style.textDecoration}. Valid values are: 'none', 'underline', 'line-through', 'underline line-through'.`); - } + getFormattedStringDetails(formattedString: FormattedString) { + const details = { + spans: [], + }; + this._spanRanges = []; + if (formattedString && formattedString.parent) { + for (let i = 0, spanStart = 0, length = formattedString.spans.length; i < length; i++) { + const span = formattedString.spans.getItem(i); + const text = span.text; + const textTransform = (formattedString.parent).textTransform; + let spanText = isNullOrUndefined(text) ? '' : `${text}`; + if (textTransform !== 'none' && textTransform !== 'initial') { + spanText = getTransformedText(spanText, textTransform); + } - if (style.letterSpacing !== 0 && this.nativeTextViewProtected.font) { - const kern = style.letterSpacing * this.nativeTextViewProtected.font.pointSize; - dict.set(NSKernAttributeName, kern); - if (this.nativeTextViewProtected instanceof UITextField) { - this.nativeTextViewProtected.defaultTextAttributes.setValueForKey(kern, NSKernAttributeName); + details.spans.push(this.createMutableStringDetails(span, spanText, spanStart)); + this._spanRanges.push({ + location: spanStart, + length: spanText.length, + }); + spanStart += spanText.length; } } + return details; + } - const isTextView = this.nativeTextViewProtected instanceof UITextView; - if (style.lineHeight) { - const paragraphStyle = NSMutableParagraphStyle.alloc().init(); - paragraphStyle.lineSpacing = style.lineHeight; - // make sure a possible previously set text alignment setting is not lost when line height is specified - if (this.nativeTextViewProtected instanceof UIButton) { - paragraphStyle.alignment = (this.nativeTextViewProtected).titleLabel.textAlignment; - } else { - paragraphStyle.alignment = (this.nativeTextViewProtected).textAlignment; - } + createMutableStringDetails(span: Span, text: string, index?: number): any { + const font = new Font(span.style.fontFamily, span.style.fontSize, span.style.fontStyle, span.style.fontWeight); + const iosFont = font.getUIFont(this.nativeTextViewProtected.font); - if (this.nativeTextViewProtected instanceof UILabel) { - // make sure a possible previously set line break mode is not lost when line height is specified - paragraphStyle.lineBreakMode = this.nativeTextViewProtected.lineBreakMode; - } - dict.set(NSParagraphStyleAttributeName, paragraphStyle); - } else if (isTextView) { - const paragraphStyle = NSMutableParagraphStyle.alloc().init(); - paragraphStyle.alignment = (this.nativeTextViewProtected).textAlignment; - dict.set(NSParagraphStyleAttributeName, paragraphStyle); + const backgroundColor = (span.style.backgroundColor || (span.parent).backgroundColor || (span.parent.parent).backgroundColor); + return { + text, + iosFont, + color: span.color ? span.color.ios : null, + backgroundColor: backgroundColor ? backgroundColor.ios : null, + textDecoration: getClosestPropertyValue(textDecorationProperty, span), + letterSpacing: this.letterSpacing || 0, + lineHeight: this.lineHeight || 0, //this.style?.lineHeight ? this.style.lineHeight : 0, + baselineOffset: this.getBaselineOffset(iosFont, span.style.verticalAlignment), + index, + }; + } + + createMutableStringForSpan(span: Span, text: string): NSMutableAttributedString { + const details = this.createMutableStringDetails(span, text); + return NativeScriptUtils.createMutableStringForSpanFontColorBackgroundColorTextDecorationBaselineOffset(details.text, details.iosFont, details.color, details.backgroundColor, details.textDecoration, details.baselineOffset); + } + + getBaselineOffset(font: UIFont, align?: CoreTypes.VerticalAlignmentTextType): number { + if (!align || ['stretch', 'baseline'].includes(align)) { + return 0; } - const source = getTransformedText(isNullOrUndefined(this.text) ? '' : `${this.text}`, this.textTransform); - if (dict.size > 0 || isTextView) { - if (isTextView && this.nativeTextViewProtected.font) { - // UITextView's font seems to change inside. - dict.set(NSFontAttributeName, this.nativeTextViewProtected.font); - } - - const result = NSMutableAttributedString.alloc().initWithString(source); - result.setAttributesRange(dict, { - location: 0, - length: source.length, - }); - if (this.nativeTextViewProtected instanceof UIButton) { - this.nativeTextViewProtected.setAttributedTitleForState(result, UIControlState.Normal); - } else { - this.nativeTextViewProtected.attributedText = result; - } - } else { - if (this.nativeTextViewProtected instanceof UIButton) { - // Clear attributedText or title won't be affected. - this.nativeTextViewProtected.setAttributedTitleForState(null, UIControlState.Normal); - this.nativeTextViewProtected.setTitleForState(source, UIControlState.Normal); - } else { - // Clear attributedText or text won't be affected. - this.nativeTextViewProtected.attributedText = undefined; - this.nativeTextViewProtected.text = source; - } + if (align === 'top') { + return -this.fontSize - font.descender - font.ascender - font.leading / 2; } - if (!style.color && majorVersion >= 13 && UIColor.labelColor) { - this._setColor(UIColor.labelColor); + if (align === 'bottom') { + return font.descender + font.leading / 2; + } + + if (align === 'text-top') { + return -this.fontSize - font.descender - font.ascender; + } + + if (align === 'text-bottom') { + return font.descender; + } + + if (align === 'middle') { + return (font.descender - font.ascender) / 2 - font.descender; + } + + if (align === 'sup') { + return -this.fontSize * 0.4; + } + + if (align === 'sub') { + return (font.descender - font.ascender) * 0.4; } } @@ -390,108 +357,6 @@ export class TextBase extends TextBaseCommon { // layer.shouldRasterize = true; // } } - - createNSMutableAttributedString(formattedString: FormattedString): NSMutableAttributedString { - const mas = NSMutableAttributedString.alloc().init(); - this._spanRanges = []; - if (formattedString && formattedString.parent) { - for (let i = 0, spanStart = 0, length = formattedString.spans.length; i < length; i++) { - const span = formattedString.spans.getItem(i); - const text = span.text; - const textTransform = (formattedString.parent).textTransform; - let spanText = isNullOrUndefined(text) ? '' : `${text}`; - if (textTransform !== 'none' && textTransform !== 'initial') { - spanText = getTransformedText(spanText, textTransform); - } - - const nsAttributedString = this.createMutableStringForSpan(span, spanText); - mas.insertAttributedStringAtIndex(nsAttributedString, spanStart); - this._spanRanges.push({ - location: spanStart, - length: spanText.length, - }); - spanStart += spanText.length; - } - } - - return mas; - } - - getBaselineOffset(font: UIFont, align?: CoreTypes.VerticalAlignmentTextType): number { - if (!align || ['stretch', 'baseline'].includes(align)) { - return 0; - } - - if (align === 'top') { - return -this.fontSize - font.descender - font.ascender - font.leading / 2; - } - - if (align === 'bottom') { - return font.descender + font.leading / 2; - } - - if (align === 'text-top') { - return -this.fontSize - font.descender - font.ascender; - } - - if (align === 'text-bottom') { - return font.descender; - } - - if (align === 'middle') { - return (font.descender - font.ascender) / 2 - font.descender; - } - - if (align === 'sup') { - return -this.fontSize * 0.4; - } - - if (align === 'sub') { - return (font.descender - font.ascender) * 0.4; - } - } - - createMutableStringForSpan(span: Span, text: string): NSMutableAttributedString { - const viewFont = this.nativeTextViewProtected.font; - const attrDict = <{ key: string; value: any }>{}; - const style = span.style; - const align = style.verticalAlignment; - - const font = new Font(style.fontFamily, style.fontSize, style.fontStyle, style.fontWeight); - const iosFont = font.getUIFont(viewFont); - - attrDict[NSFontAttributeName] = iosFont; - - if (span.color) { - attrDict[NSForegroundColorAttributeName] = span.color.ios; - } - - // We don't use isSet function here because defaultValue for backgroundColor is null. - const backgroundColor = (style.backgroundColor || (span.parent).backgroundColor || (span.parent.parent).backgroundColor); - if (backgroundColor) { - attrDict[NSBackgroundColorAttributeName] = backgroundColor.ios; - } - - const textDecoration: CoreTypes.TextDecorationType = getClosestPropertyValue(textDecorationProperty, span); - - if (textDecoration) { - const underline = textDecoration.indexOf('underline') !== -1; - if (underline) { - attrDict[NSUnderlineStyleAttributeName] = underline; - } - - const strikethrough = textDecoration.indexOf('line-through') !== -1; - if (strikethrough) { - attrDict[NSStrikethroughStyleAttributeName] = strikethrough; - } - } - - if (align) { - attrDict[NSBaselineOffsetAttributeName] = this.getBaselineOffset(iosFont, align); - } - - return NSMutableAttributedString.alloc().initWithStringAttributes(text, attrDict); - } } export function getTransformedText(text: string, textTransform: CoreTypes.TextTransformType): string {