diff --git a/packages/core/platforms/ios/src/UIView+NativeScript.m b/packages/core/platforms/ios/src/UIView+NativeScript.m index f288aeaa55..75c9260c4b 100644 --- a/packages/core/platforms/ios/src/UIView+NativeScript.m +++ b/packages/core/platforms/ios/src/UIView+NativeScript.m @@ -15,23 +15,45 @@ - (void)nativeScriptSetTextDecorationAndTransform:(NSString*)text textDecoration } 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]; + if (letterSpacing != 0 && isTextType) { + NSNumber *kern = nil; + + if ([self isKindOfClass:[UIButton class]]) { + if (((UIButton*)self).titleLabel.font != nil) { + kern = [NSNumber numberWithDouble:letterSpacing * ((UIButton*)self).titleLabel.font.pointSize]; + } + } else { + if (((UITextView*)self).font != nil) { + kern = [NSNumber numberWithDouble:letterSpacing * ((UITextView*)self).font.pointSize]; + } + } + + if (kern != nil) { + attrDict[NSKernAttributeName] = kern; + if ([self isKindOfClass:[UITextField class]]) { + [((UITextField*)self).defaultTextAttributes setValue:kern forKey:NSKernAttributeName]; + } } } BOOL isTextView = [self isKindOfClass:[UITextView class]]; - if (lineHeight > 0) { + // TODO: Find a good alternative of lineSpacing that works well with layout as it doesn't accept values less than the standard height + 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; + + if (((UIButton*)self).titleLabel.font != nil) { + paragraphStyle.lineSpacing = fmax(lineHeight - ((UIButton*)self).titleLabel.font.lineHeight, 0); + } } else { paragraphStyle.alignment = ((UILabel*)self).textAlignment; + + if (((UILabel*)self).font != nil) { + paragraphStyle.lineSpacing = fmax(lineHeight - ((UILabel*)self).font.lineHeight, 0); + } } if ([self isKindOfClass:[UILabel class]]) { @@ -78,7 +100,14 @@ - (void)nativeScriptSetTextDecorationAndTransform:(NSString*)text textDecoration -(void)nativeScriptSetFormattedTextDecorationAndTransform:(NSDictionary*)details letterSpacing:(CGFloat)letterSpacing lineHeight:(CGFloat)lineHeight { NSMutableAttributedString *attrText = [NativeScriptUtils createMutableStringWithDetails:details]; if (letterSpacing != 0) { - NSNumber *kern = [NSNumber numberWithDouble:letterSpacing * ((UITextView*)self).font.pointSize]; + NSNumber *kern = nil; + + if ([self isKindOfClass:[UIButton class]]) { + kern = [NSNumber numberWithDouble:letterSpacing * ((UIButton*)self).titleLabel.font.pointSize]; + } else { + kern = [NSNumber numberWithDouble:letterSpacing * ((UITextView*)self).font.pointSize]; + } + [attrText addAttribute:NSKernAttributeName value:kern range:(NSRange){ 0, attrText.length @@ -86,15 +115,24 @@ -(void)nativeScriptSetFormattedTextDecorationAndTransform:(NSDictionary*)details } BOOL isLabel = [self isKindOfClass:[UILabel class]]; - if (lineHeight > 0) { + // TODO: Find a good alternative of lineSpacing that works well with layout as it doesn't accept values less than the standard height + 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; + + if (((UIButton*)self).titleLabel.font != nil) { + paragraphStyle.lineSpacing = fmax(lineHeight - ((UIButton*)self).titleLabel.font.lineHeight, 0); + } } else { // Paragraph alignment is also important for tappable spans as NSTextContainer takes it into account paragraphStyle.alignment = ((UILabel*)self).textAlignment; + + if (((UILabel*)self).font != nil) { + paragraphStyle.lineSpacing = fmax(lineHeight - ((UILabel*)self).font.lineHeight, 0); + } } if (isLabel) { diff --git a/packages/core/ui/html-view/index.android.ts b/packages/core/ui/html-view/index.android.ts index 3865cb594c..93caa23c60 100644 --- a/packages/core/ui/html-view/index.android.ts +++ b/packages/core/ui/html-view/index.android.ts @@ -1,4 +1,5 @@ import { Color } from '../../color'; +import { layout } from '../../utils'; import { SDK_VERSION } from '../../utils/constants'; import { Font } from '../styling/font'; import { colorProperty, fontSizeProperty, fontInternalProperty } from '../styling/style-properties'; @@ -86,14 +87,10 @@ export class HtmlView extends HtmlViewBase { this.nativeViewProtected.setTypeface(font); } - [fontSizeProperty.getDefault](): { nativeSize: number } { - return { nativeSize: this.nativeViewProtected.getTextSize() }; + [fontSizeProperty.getDefault](): number { + return this.nativeViewProtected.getTextSize() / layout.getDisplayDensity(); } - [fontSizeProperty.setNative](value: number | { nativeSize: number }) { - if (typeof value === 'number') { - this.nativeViewProtected.setTextSize(value); - } else { - this.nativeViewProtected.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, value.nativeSize); - } + [fontSizeProperty.setNative](value: number) { + this.nativeViewProtected.setTextSize(value); } } diff --git a/packages/core/ui/search-bar/index.android.ts b/packages/core/ui/search-bar/index.android.ts index dead0caf69..1c4a4b7ff6 100644 --- a/packages/core/ui/search-bar/index.android.ts +++ b/packages/core/ui/search-bar/index.android.ts @@ -1,7 +1,7 @@ import { Font } from '../styling/font'; import { SearchBarBase, textProperty, hintProperty, textFieldHintColorProperty, textFieldBackgroundColorProperty } from './search-bar-common'; import { isUserInteractionEnabledProperty, isEnabledProperty } from '../core/view'; -import { ad } from '../../utils'; +import { ad, layout } from '../../utils'; import { Color } from '../../color'; import { colorProperty, backgroundColorProperty, backgroundInternalProperty, fontInternalProperty, fontSizeProperty } from '../styling/style-properties'; @@ -199,15 +199,11 @@ export class SearchBar extends SearchBarBase { textView.setTextColor(color); } - [fontSizeProperty.getDefault](): { nativeSize: number } { - return { nativeSize: this._getTextView().getTextSize() }; + [fontSizeProperty.getDefault](): number { + return this._getTextView().getTextSize() / layout.getDisplayDensity(); } - [fontSizeProperty.setNative](value: number | { nativeSize: number }) { - if (typeof value === 'number') { - this._getTextView().setTextSize(value); - } else { - this._getTextView().setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, value.nativeSize); - } + [fontSizeProperty.setNative](value: number) { + this._getTextView().setTextSize(value); } [fontInternalProperty.getDefault](): android.graphics.Typeface { diff --git a/packages/core/ui/segmented-bar/index.android.ts b/packages/core/ui/segmented-bar/index.android.ts index 99448baf56..d84d864726 100644 --- a/packages/core/ui/segmented-bar/index.android.ts +++ b/packages/core/ui/segmented-bar/index.android.ts @@ -140,15 +140,11 @@ export class SegmentedBarItem extends SegmentedBarItemBase { this.nativeViewProtected.setTextColor(color); } - [fontSizeProperty.getDefault](): { nativeSize: number } { - return { nativeSize: this.nativeViewProtected.getTextSize() }; + [fontSizeProperty.getDefault](): number { + return this.nativeViewProtected.getTextSize() / layout.getDisplayDensity(); } - [fontSizeProperty.setNative](value: number | { nativeSize: number }) { - if (typeof value === 'number') { - this.nativeViewProtected.setTextSize(value); - } else { - this.nativeViewProtected.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, value.nativeSize); - } + [fontSizeProperty.setNative](value: number) { + this.nativeViewProtected.setTextSize(value); } [fontInternalProperty.getDefault](): android.graphics.Typeface { diff --git a/packages/core/ui/styling/style-properties.d.ts b/packages/core/ui/styling/style-properties.d.ts index 61f99e3d0a..ab19aac513 100644 --- a/packages/core/ui/styling/style-properties.d.ts +++ b/packages/core/ui/styling/style-properties.d.ts @@ -77,7 +77,6 @@ export const minWidthProperty: CssProperty; export const widthProperty: CssAnimationProperty; export const heightProperty: CssAnimationProperty; -export const lineHeightProperty: CssProperty; export const marginProperty: ShorthandProperty; export const marginLeftProperty: CssProperty; export const marginRightProperty: CssProperty; diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts index 30839455a6..8030d289d9 100644 --- a/packages/core/ui/styling/style/index.ts +++ b/packages/core/ui/styling/style/index.ts @@ -168,7 +168,7 @@ export class Style extends Observable implements StyleDefinition { public visibility: CoreTypes.VisibilityType; public letterSpacing: number; - public lineHeight: number; + public lineHeight: CoreTypes.PercentLengthType; public textAlignment: CoreTypes.TextAlignmentType; public textDecoration: CoreTypes.TextDecorationType; public textTransform: CoreTypes.TextTransformType; diff --git a/packages/core/ui/tab-view/index.android.ts b/packages/core/ui/tab-view/index.android.ts index cc8405311a..f66e33b643 100644 --- a/packages/core/ui/tab-view/index.android.ts +++ b/packages/core/ui/tab-view/index.android.ts @@ -392,15 +392,11 @@ export class TabViewItem extends TabViewItemBase { return tabFragment.getChildFragmentManager(); } - [fontSizeProperty.getDefault](): { nativeSize: number } { - return { nativeSize: this.nativeViewProtected.getTextSize() }; + [fontSizeProperty.getDefault](): number { + return this.nativeViewProtected.getTextSize() / layout.getDisplayDensity(); } - [fontSizeProperty.setNative](value: number | { nativeSize: number }) { - if (typeof value === 'number') { - this.nativeViewProtected.setTextSize(value); - } else { - this.nativeViewProtected.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, value.nativeSize); - } + [fontSizeProperty.setNative](value: number) { + this.nativeViewProtected.setTextSize(value); } [fontInternalProperty.getDefault](): android.graphics.Typeface { @@ -494,7 +490,7 @@ export class TabView extends TabViewBase { JSON.stringify([ { value: 1, type: 0 /* org.nativescript.widgets.GridUnitType.auto */ }, { value: 1, type: 2 /* org.nativescript.widgets.GridUnitType.star */ }, - ]) + ]), ); viewPager.setLayoutParams(lp); @@ -506,7 +502,7 @@ export class TabView extends TabViewBase { JSON.stringify([ { value: 1, type: 2 /* org.nativescript.widgets.GridUnitType.star */ }, { value: 1, type: 0 /* org.nativescript.widgets.GridUnitType.auto */ }, - ]) + ]), ); tabLayout.setLayoutParams(lp); viewPager.setSwipePageEnabled(false); @@ -771,12 +767,8 @@ export class TabView extends TabViewBase { [tabTextFontSizeProperty.getDefault](): number { return this._tabLayout.getTabTextFontSize(); } - [tabTextFontSizeProperty.setNative](value: number | { nativeSize: number }) { - if (typeof value === 'number') { - this._tabLayout.setTabTextFontSize(value); - } else { - this._tabLayout.setTabTextFontSize(value.nativeSize); - } + [tabTextFontSizeProperty.setNative](value: number) { + this._tabLayout.setTabTextFontSize(value); } [tabTextColorProperty.getDefault](): number { diff --git a/packages/core/ui/tab-view/index.ios.ts b/packages/core/ui/tab-view/index.ios.ts index ece8ae0963..5262a624d1 100644 --- a/packages/core/ui/tab-view/index.ios.ts +++ b/packages/core/ui/tab-view/index.ios.ts @@ -580,7 +580,7 @@ export class TabView extends TabViewBase { [tabTextFontSizeProperty.getDefault](): number { return null; } - [tabTextFontSizeProperty.setNative](value: number | { nativeSize: number }) { + [tabTextFontSizeProperty.setNative](value: number) { this._updateIOSTabBarColorsAndFonts(); } diff --git a/packages/core/ui/text-base/index.android.ts b/packages/core/ui/text-base/index.android.ts index cb538617bd..2114784862 100644 --- a/packages/core/ui/text-base/index.android.ts +++ b/packages/core/ui/text-base/index.android.ts @@ -4,10 +4,9 @@ import { ShadowCSSValues } from '../styling/css-shadow'; // Requires import { Font } from '../styling/font'; -import { backgroundColorProperty } from '../styling/style-properties'; import { TextBaseCommon, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textProperty, textTransformProperty, textShadowProperty, textStrokeProperty, letterSpacingProperty, whiteSpaceProperty, lineHeightProperty, isBold, resetSymbol } from './text-base-common'; import { Color } from '../../color'; -import { colorProperty, fontSizeProperty, fontInternalProperty, paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, Length } from '../styling/style-properties'; +import { backgroundColorProperty, colorProperty, fontSizeProperty, fontInternalProperty, paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, Length } from '../styling/style-properties'; import { StrokeCSSValues } from '../styling/css-stroke'; import { FormattedString } from './formatted-string'; import { Span } from './span'; @@ -360,24 +359,49 @@ export class TextBase extends TextBaseCommon { } } - [fontSizeProperty.getDefault](): { nativeSize: number } { - return { nativeSize: this.nativeTextViewProtected.getTextSize() }; + [fontSizeProperty.getDefault](): number { + return this.nativeTextViewProtected.getTextSize() / layout.getDisplayDensity(); } - [fontSizeProperty.setNative](value: number | { nativeSize: number }) { - if (!this.formattedText || typeof value !== 'number') { - if (typeof value === 'number') { - this.nativeTextViewProtected.setTextSize(value); - } else { - this.nativeTextViewProtected.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, value.nativeSize); - } + [fontSizeProperty.setNative](value: number) { + if (!this.formattedText) { + this.nativeTextViewProtected.setTextSize(value); } } - [lineHeightProperty.getDefault](): number { - return this.nativeTextViewProtected.getLineSpacingExtra() / layout.getDisplayDensity(); + [lineHeightProperty.getDefault](): CoreTypes.PercentLengthType { + return { value: this.nativeTextViewProtected.getLineHeight(), unit: 'px' }; } - [lineHeightProperty.setNative](value: number) { - this.nativeTextViewProtected.setLineSpacing(value * layout.getDisplayDensity(), 1); + [lineHeightProperty.setNative](value: CoreTypes.PercentLengthType) { + const lengthType = value; + + if (lengthType == null || typeof lengthType === 'string') { + // Method setLineHeight calls this one internally so it's enough to do the cleanup + this.nativeTextViewProtected.setLineSpacing(0, 1); + } else { + let finalValue: number; + + if (typeof lengthType === 'number') { + finalValue = Length.toDevicePixels(lengthType, -1); + } else if (lengthType.unit === '%') { + const fontHeight = this.nativeTextViewProtected.getPaint().getFontMetricsInt(null); + finalValue = lengthType.value * fontHeight; + } else { + finalValue = Length.toDevicePixels(lengthType, -1); + } + + // Method setLineHeight throws in case of a negative value + finalValue = Math.max(finalValue, 0); + + if (SDK_VERSION >= 28) { + this.nativeTextViewProtected.setLineHeight(finalValue); + } else { + const fontHeight = this.nativeTextViewProtected.getPaint().getFontMetricsInt(null); + // Actual line spacing is the diff of line height and font height + const lineSpacing = finalValue - fontHeight; + + this.nativeTextViewProtected.setLineSpacing(lineSpacing, 1); + } + } } [fontInternalProperty.getDefault](): android.graphics.Typeface { diff --git a/packages/core/ui/text-base/index.d.ts b/packages/core/ui/text-base/index.d.ts index 8dde2ad28b..71c7945916 100644 --- a/packages/core/ui/text-base/index.d.ts +++ b/packages/core/ui/text-base/index.d.ts @@ -50,7 +50,7 @@ export class TextBase extends View implements AddChildFromBuilder { * * @nsProperty */ - lineHeight: number; + lineHeight: CoreTypes.PercentLengthType; /** * Gets or sets text-alignment style property. @@ -200,7 +200,7 @@ export const textStrokeProperty: CssProperty; export const whiteSpaceProperty: CssProperty; export const textOverflowProperty: CssProperty; export const letterSpacingProperty: CssProperty; -export const lineHeightProperty: CssProperty; +export const lineHeightProperty: InheritedCssProperty; //Used by tab view export function getTransformedText(text: string, textTransform: CoreTypes.TextTransformType): string; diff --git a/packages/core/ui/text-base/index.ios.ts b/packages/core/ui/text-base/index.ios.ts index fd0d42dd74..b847143b1f 100644 --- a/packages/core/ui/text-base/index.ios.ts +++ b/packages/core/ui/text-base/index.ios.ts @@ -256,7 +256,13 @@ export class TextBase extends TextBaseCommon { this._setNativeText(); } - [lineHeightProperty.setNative](value: number) { + [lineHeightProperty.getDefault](): CoreTypes.PercentLengthType { + const nativeTextView = this.nativeTextViewProtected; + const nativeFont = nativeTextView instanceof UIButton ? nativeTextView.titleLabel.font : nativeTextView.font; + return nativeFont?.lineHeight ?? 0; + } + + [lineHeightProperty.setNative](value: CoreTypes.PercentLengthType) { this._setNativeText(); } @@ -318,18 +324,20 @@ export class TextBase extends TextBaseCommon { } const letterSpacing = this.style.letterSpacing ? this.style.letterSpacing : 0; - const lineHeight = this.style.lineHeight ? this.style.lineHeight : 0; + const lineHeight = this._calculateLineHeight(); + if (this.formattedText) { this.nativeTextViewProtected.nativeScriptSetFormattedTextDecorationAndTransformLetterSpacingLineHeight(this.getFormattedStringDetails(this.formattedText) as any, letterSpacing, lineHeight); } else { - // console.log('setTextDecorationAndTransform...') const text = getTransformedText(isNullOrUndefined(this.text) ? '' : `${this.text}`, this.textTransform); this.nativeTextViewProtected.nativeScriptSetTextDecorationAndTransformTextDecorationLetterSpacingLineHeight(text, this.style.textDecoration || '', letterSpacing, lineHeight); + } - if (!this.style?.color && majorVersion >= 13 && UIColor.labelColor) { - this._setColor(UIColor.labelColor); - } + // Apply this default to both regular and formatted text, otherwise UIButton will use its own default + if (!this.style?.color && majorVersion >= 13 && UIColor.labelColor) { + this._setColor(UIColor.labelColor); } + if (this.style?.textStroke) { this.nativeTextViewProtected.nativeScriptSetFormattedTextStrokeColor(Length.toDevicePixels(this.style.textStroke.width, 0), this.style.textStroke.color.ios); } @@ -422,6 +430,28 @@ export class TextBase extends TextBaseCommon { } } + private _calculateLineHeight(): number { + const lengthType = this.style.lineHeight; + const nativeTextView = this.nativeTextViewProtected; + const nativeFont = nativeTextView instanceof UIButton ? nativeTextView.titleLabel.font : nativeTextView.font; + // Get font lineHeight value to simulate what android does with getFontMetricsInt + const fontHeight = nativeFont?.lineHeight ?? 0; + + let lineHeight: number; + + if (lengthType == null || typeof lengthType === 'string') { + lineHeight = -1; // This indicates that line-height is normal + } else if (typeof lengthType === 'number') { + lineHeight = lengthType; + } else if (lengthType.unit === '%') { + lineHeight = lengthType.value * fontHeight; + } else { + lineHeight = layout.toDeviceIndependentPixels(Length.toDevicePixels(lengthType, -1)); + } + + return lineHeight; + } + _setShadow(value: ShadowCSSValues): void { const layer: CALayer = this.nativeTextViewProtected.layer; diff --git a/packages/core/ui/text-base/text-base-common.ts b/packages/core/ui/text-base/text-base-common.ts index ab192d37ec..16b7b2fb7b 100644 --- a/packages/core/ui/text-base/text-base-common.ts +++ b/packages/core/ui/text-base/text-base-common.ts @@ -15,6 +15,7 @@ import { TextBase as TextBaseDefinition } from '.'; import { Color } from '../../color'; import { ShadowCSSValues, parseCSSShadow } from '../styling/css-shadow'; import { StrokeCSSValues, parseCSSStroke } from '../styling/css-stroke'; +import { Length, PercentLength } from '../styling/style-properties'; const CHILD_SPAN = 'Span'; const CHILD_FORMATTED_TEXT = 'formattedText'; @@ -86,10 +87,10 @@ export abstract class TextBaseCommon extends View implements TextBaseDefinition this.style.letterSpacing = value; } - get lineHeight(): number { + get lineHeight(): CoreTypes.PercentLengthType { return this.style.lineHeight; } - set lineHeight(value: number) { + set lineHeight(value: CoreTypes.PercentLengthType) { this.style.lineHeight = value; } @@ -361,11 +362,12 @@ export const letterSpacingProperty = new InheritedCssProperty({ }); letterSpacingProperty.register(Style); -export const lineHeightProperty = new InheritedCssProperty({ +export const lineHeightProperty = new InheritedCssProperty({ name: 'lineHeight', cssName: 'line-height', affectsLayout: __APPLE__, - valueConverter: (v) => parseFloat(v), + equalityComparer: Length.equals, + valueConverter: PercentLength.parse, }); lineHeightProperty.register(Style);