Skip to content

Commit ddc90c7

Browse files
committed
fix(ios): lineHeight and letterSpacing fix
1 parent 3a48c74 commit ddc90c7

File tree

1 file changed

+64
-212
lines changed

1 file changed

+64
-212
lines changed

Diff for: src/label.ios.ts

+64-212
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
LightFormattedString,
23
VerticalTextAlignment,
34
computeBaseLineOffset,
45
createNativeAttributedString,
@@ -60,8 +61,8 @@ declare module '@nativescript/core/ui/text-base' {
6061
interface TextBase {
6162
_requestLayoutOnTextChanged();
6263
_setNativeText();
63-
createMutableStringForSpan?(span, text): NSMutableAttributedString;
64-
createNSMutableAttributedString?(formattedString: FormattedString): NSMutableAttributedString;
64+
// createMutableStringForSpan?(span, text): NSMutableAttributedString;
65+
// createNSMutableAttributedString?(formattedString: FormattedString): NSMutableAttributedString;
6566
// createNSMutableAttributedString(formattedString: FormattedString);
6667
}
6768
}
@@ -164,13 +165,13 @@ export class Label extends LabelBase {
164165
attributedString: NSMutableAttributedString;
165166
private _delegate: LabelUITextViewDelegateImpl;
166167
static DTCORETEXT_INIT = false;
167-
constructor() {
168-
super();
169-
// if (iOSUseDTCoreText && !Label.DTCORETEXT_INIT) {
170-
// Label.DTCORETEXT_INIT = true;
171-
// DTCoreTextFontDescriptor.asyncPreloadFontLookupTable();
172-
// }
173-
}
168+
// constructor() {
169+
// super();
170+
// if (iOSUseDTCoreText && !Label.DTCORETEXT_INIT) {
171+
// Label.DTCORETEXT_INIT = true;
172+
// DTCoreTextFontDescriptor.asyncPreloadFontLookupTable();
173+
// }
174+
// }
174175
public createNativeView() {
175176
const view = UITextView.new();
176177
if (!view.font) {
@@ -183,13 +184,10 @@ export class Label extends LabelBase {
183184
view.backgroundColor = UIColor.clearColor;
184185
view.userInteractionEnabled = true;
185186
view.dataDetectorTypes = UIDataDetectorTypes.All;
186-
view.textContainerInset = {
187-
top: 0,
188-
left: 0,
189-
bottom: 0,
190-
right: 0
191-
};
192-
view.textContainer.lineFragmentPadding = 0; // to remove left padding
187+
view.textContainerInset = UIEdgeInsetsZero;
188+
view.textContainer.lineFragmentPadding = 0;
189+
// ignore font leading just like UILabel does
190+
view.layoutManager.usesFontLeading = false;
193191
// view.textContainer.lineBreakMode = NSLineBreakMode.ByCharWrapping;
194192
return view;
195193
}
@@ -206,12 +204,6 @@ export class Label extends LabelBase {
206204
null
207205
);
208206
this.nativeViewProtected.attributedText = this.attributedString;
209-
// this.htmlText = null;
210-
// this.needsHTMLUpdate = false;
211-
// this.updatingHTML = false;
212-
// if (this.htmlText && this.needsHTMLUpdate) {
213-
// this.updateHTMLString();
214-
// }
215207
}
216208
public disposeNativeView() {
217209
this._delegate = null;
@@ -236,21 +228,33 @@ export class Label extends LabelBase {
236228
}
237229
computeTextHeight(size: CGSize) {
238230
const tv = this.nativeTextViewProtected;
231+
const oldtextContainerInset = tv.textContainerInset;
232+
tv.textContainerInset = UIEdgeInsetsZero;
233+
// if (tv.attributedText) {
234+
// const result = tv.attributedText.boundingRectWithSizeOptionsContext(
235+
// size,
236+
// NSStringDrawingOptions.UsesLineFragmentOrigin | NSStringDrawingOptions.UsesFontLeading,
237+
// null
238+
// );
239+
// return Math.round(CGRectGetHeight(result));
240+
// }
239241
const result = tv.sizeThatFits(size);
242+
tv.textContainerInset = oldtextContainerInset;
240243
return result.height;
241244
}
242245

243246
updateTextContainerInset(applyVerticalTextAlignment = true) {
244-
// if (!this.text) {
245-
// return;
246-
// }
247247
const tv = this.nativeTextViewProtected;
248248
const top = layout.toDeviceIndependentPixels(this.effectivePaddingTop + this.effectiveBorderTopWidth);
249249
const right = layout.toDeviceIndependentPixels(this.effectivePaddingRight + this.effectiveBorderRightWidth);
250250
const bottom = layout.toDeviceIndependentPixels(this.effectivePaddingBottom + this.effectiveBorderBottomWidth);
251251
const left = layout.toDeviceIndependentPixels(this.effectivePaddingLeft + this.effectiveBorderLeftWidth);
252-
if (!applyVerticalTextAlignment || !this.verticalTextAlignment) {
253-
this.nativeViewProtected.textContainerInset = {
252+
if (
253+
!applyVerticalTextAlignment ||
254+
!this.verticalTextAlignment ||
255+
(tv.text?.length === 0 && tv.attributedText?.length === 0)
256+
) {
257+
tv.textContainerInset = {
254258
top,
255259
left,
256260
bottom,
@@ -261,7 +265,7 @@ export class Label extends LabelBase {
261265
switch (this.verticalTextAlignment) {
262266
case 'initial': // not supported
263267
case 'top':
264-
this.nativeViewProtected.textContainerInset = {
268+
tv.textContainerInset = {
265269
top,
266270
left,
267271
bottom,
@@ -274,7 +278,7 @@ export class Label extends LabelBase {
274278
const height = this.computeTextHeight(CGSizeMake(tv.bounds.size.width, 10000));
275279
let topCorrect = (tv.bounds.size.height - top - bottom - height * tv.zoomScale) / 2.0;
276280
topCorrect = topCorrect < 0.0 ? 0.0 : topCorrect;
277-
this.nativeViewProtected.textContainerInset = {
281+
tv.textContainerInset = {
278282
top: top + topCorrect,
279283
left,
280284
bottom,
@@ -285,9 +289,9 @@ export class Label extends LabelBase {
285289

286290
case 'bottom': {
287291
const height = this.computeTextHeight(CGSizeMake(tv.bounds.size.width, 10000));
288-
let bottomCorrect = tv.bounds.size.height - bottom - height * tv.zoomScale;
292+
let bottomCorrect = tv.bounds.size.height - top - bottom - height * tv.zoomScale;
289293
bottomCorrect = bottomCorrect < 0.0 ? 0.0 : bottomCorrect;
290-
this.nativeViewProtected.textContainerInset = {
294+
tv.textContainerInset = {
291295
top: top + bottomCorrect,
292296
left,
293297
bottom,
@@ -304,86 +308,6 @@ export class Label extends LabelBase {
304308
}
305309
private _fixedSize: FixedSize;
306310

307-
// setTextDecorationAndTransform() {
308-
// const style = this.style;
309-
// const dict = new Map<string, any>();
310-
// switch (style.textDecoration) {
311-
// case 'none':
312-
// break;
313-
// case 'underline':
314-
// // TODO: Replace deprecated `StyleSingle` with `Single` after the next typings update
315-
// dict.set(NSUnderlineStyleAttributeName, NSUnderlineStyle.Single);
316-
// break;
317-
// case 'line-through':
318-
// // TODO: Replace deprecated `StyleSingle` with `Single` after the next typings update
319-
// dict.set(NSStrikethroughStyleAttributeName, NSUnderlineStyle.Single);
320-
// break;
321-
// case 'underline line-through':
322-
// // TODO: Replace deprecated `StyleSingle` with `Single` after the next typings update
323-
// dict.set(NSUnderlineStyleAttributeName, NSUnderlineStyle.Single);
324-
// dict.set(NSStrikethroughStyleAttributeName, NSUnderlineStyle.Single);
325-
// break;
326-
// default:
327-
// throw new Error(`Invalid text decoration value: ${style.textDecoration}. Valid values are: 'none', 'underline', 'line-through', 'underline line-through'.`);
328-
// }
329-
330-
// if (style.letterSpacing !== 0) {
331-
// dict.set(NSKernAttributeName, style.letterSpacing * this.nativeTextViewProtected.font.pointSize);
332-
// }
333-
334-
// if (style.lineHeight || style.whiteSpace === 'nowrap' || (style['lineBreak'] && style['lineBreak'] !== 'none')) {
335-
// const paragraphStyle = NSMutableParagraphStyle.alloc().init();
336-
// paragraphStyle.minimumLineHeight = style.lineHeight;
337-
// // make sure a possible previously set text alignment setting is not lost when line height is specified
338-
// paragraphStyle.alignment = (this.nativeTextViewProtected as UITextField | UITextView | UILabel).textAlignment;
339-
340-
// // make sure a possible previously set line break mode is not lost when line height is specified
341-
342-
// if (style['lineBreak']) {
343-
// paragraphStyle.lineBreakMode = lineBreakToLineBreakMode(style['lineBreak']);
344-
// } else if (style.whiteSpace) {
345-
// paragraphStyle.lineBreakMode = whiteSpaceToLineBreakMode(style.whiteSpace);
346-
// }
347-
// dict.set(NSParagraphStyleAttributeName, paragraphStyle);
348-
// } else if (isTextView && this.style.textAlignment !== 'initial') {
349-
// const paragraphStyle = NSMutableParagraphStyle.alloc().init();
350-
// paragraphStyle.alignment = this.nativeTextViewProtected.textAlignment;
351-
// dict.set(NSParagraphStyleAttributeName, paragraphStyle);
352-
// }
353-
354-
// if (style.color && dict.size > 0) {
355-
// // dict.set(NSForegroundColorAttributeName, style.color.ios);
356-
// }
357-
358-
// const text = this.text;
359-
// const str = text === undefined || text === null ? '' : text.toString();
360-
// const source = getTransformedText(str, this.textTransform);
361-
// if (dict.size > 0) {
362-
// if (isTextView) {
363-
// // UITextView's font seems to change inside.
364-
// dict.set(NSFontAttributeName, this.nativeTextViewProtected.font);
365-
// }
366-
367-
// const result = NSMutableAttributedString.alloc().initWithString(source);
368-
// result.setAttributesRange(dict as any, { location: 0, length: source.length });
369-
// if (this.nativeTextViewProtected instanceof UIButton) {
370-
// this.nativeTextViewProtected.setAttributedTitleForState(result, UIControlState.Normal);
371-
// } else {
372-
// this.nativeTextViewProtected.attributedText = result;
373-
// }
374-
// } else {
375-
// if (this.nativeTextViewProtected instanceof UIButton) {
376-
// // Clear attributedText or title won't be affected.
377-
// this.nativeTextViewProtected.setAttributedTitleForState(null, UIControlState.Normal);
378-
// this.nativeTextViewProtected.setTitleForState(source, UIControlState.Normal);
379-
// } else {
380-
// // Clear attributedText or text won't be affected.
381-
// this.nativeTextViewProtected.attributedText = undefined;
382-
// this.nativeTextViewProtected.text = source;
383-
// }
384-
// }
385-
// }
386-
387311
_requestLayoutOnTextChanged(): void {
388312
if (this._fixedSize === FixedSize.BOTH) {
389313
return;
@@ -498,13 +422,13 @@ export class Label extends LabelBase {
498422
// when in collectionView or pager
499423
// if this is done sync (without DTCoreText) while init the cell
500424
// it breaks the UICollectionView :s
501-
if (usingIOSDTCoreText()) {
502-
this._updateHTMLString();
503-
} else {
504-
// setTimeout(() => {
505-
this._updateHTMLString();
506-
// }, 0);
507-
}
425+
// if (usingIOSDTCoreText()) {
426+
// this._updateHTMLString();
427+
// } else {
428+
// setTimeout(() => {
429+
this._updateHTMLString();
430+
// }, 0);
431+
// }
508432
}
509433
_setColor(color) {
510434
if (this.nativeTextViewProtected instanceof UIButton) {
@@ -629,6 +553,7 @@ export class Label extends LabelBase {
629553
} else {
630554
super._setNativeText();
631555
}
556+
this.updateTextContainerInset();
632557
this._requestLayoutOnTextChanged();
633558
}
634559
setTextDecorationAndTransform() {
@@ -655,24 +580,18 @@ export class Label extends LabelBase {
655580
if (style.letterSpacing !== 0 && this.nativeTextViewProtected.font) {
656581
const kern = style.letterSpacing * this.nativeTextViewProtected.font.pointSize;
657582
dict.set(NSKernAttributeName, kern);
658-
if (this.nativeTextViewProtected instanceof UITextField) {
659-
this.nativeTextViewProtected.defaultTextAttributes.setValueForKey(kern, NSKernAttributeName);
660-
}
661583
}
662584
const isTextView = false;
663-
if (style.lineHeight) {
585+
if (style.lineHeight !== undefined) {
586+
let lineHeight = style.lineHeight;
587+
if (lineHeight === 0) {
588+
lineHeight = 0.00001;
589+
}
664590
const paragraphStyle = NSMutableParagraphStyle.alloc().init();
665-
paragraphStyle.lineSpacing = style.lineHeight;
591+
paragraphStyle.minimumLineHeight = lineHeight;
592+
paragraphStyle.maximumLineHeight = lineHeight;
666593
// make sure a possible previously set text alignment setting is not lost when line height is specified
667-
if (this.nativeTextViewProtected instanceof UIButton) {
668-
paragraphStyle.alignment = this.nativeTextViewProtected.titleLabel.textAlignment;
669-
} else {
670-
paragraphStyle.alignment = this.nativeTextViewProtected.textAlignment;
671-
}
672-
if (this.nativeTextViewProtected instanceof UILabel) {
673-
// make sure a possible previously set line break mode is not lost when line height is specified
674-
paragraphStyle.lineBreakMode = this.nativeTextViewProtected.lineBreakMode;
675-
}
594+
paragraphStyle.alignment = this.nativeTextViewProtected.textAlignment;
676595
dict.set(NSParagraphStyleAttributeName, paragraphStyle);
677596
} else if (isTextView) {
678597
const paragraphStyle = NSMutableParagraphStyle.alloc().init();
@@ -695,96 +614,29 @@ export class Label extends LabelBase {
695614
location: 0,
696615
length: source.length
697616
});
698-
if (this.nativeTextViewProtected instanceof UIButton) {
699-
this.nativeTextViewProtected.setAttributedTitleForState(result, 0 /* Normal */);
700-
} else {
701-
this.nativeTextViewProtected.attributedText = result;
702-
}
617+
this.nativeTextViewProtected.attributedText = result;
703618
} else {
704-
if (this.nativeTextViewProtected instanceof UIButton) {
705-
// Clear attributedText or title won't be affected.
706-
this.nativeTextViewProtected.setAttributedTitleForState(null, 0 /* Normal */);
707-
this.nativeTextViewProtected.setTitleForState(source, 0 /* Normal */);
708-
} else {
709-
// Clear attributedText or text won't be affected.
710-
this.nativeTextViewProtected.attributedText = undefined;
711-
this.nativeTextViewProtected.text = source;
712-
}
619+
// Clear attributedText or text won't be affected.
620+
this.nativeTextViewProtected.attributedText = undefined;
621+
this.nativeTextViewProtected.text = source;
713622
}
714623
if (!style.color && majorVersion >= 13 && UIColor.labelColor) {
715624
(this as any)._setColor(UIColor.labelColor);
716625
}
717626
}
718-
currentMaxFontSize = 0;
719-
720-
createNSMutableAttributedString(formattedString: FormattedString): NSMutableAttributedString {
721-
// we need to store the max Font size to pass it to createMutableStringForSpan
722-
const length = formattedString.spans.length;
723-
let maxFontSize = formattedString.style?.fontSize || this?.style.fontSize || 0;
724-
for (let i = 0; i < length; i++) {
725-
const s = formattedString.spans.getItem(i);
726-
if (s.style.fontSize) {
727-
maxFontSize = Math.max(maxFontSize, s.style.fontSize);
728-
}
729-
}
730-
this.currentMaxFontSize = maxFontSize;
731-
return super.createNSMutableAttributedString(formattedString);
627+
createFormattedTextNative(value: FormattedString) {
628+
return createNativeAttributedString(value, this, this.autoFontSize, this.fontSizeRatio);
732629
}
733-
createMutableStringForSpan(span: Span, text: string): NSMutableAttributedString {
734-
const viewFont = this.nativeTextViewProtected.font;
735-
const attrDict: { key: string; value: any } = {} as any;
736-
const style = span.style;
737-
738-
let align = style.verticalAlignment || (span.parent as FormattedString).style.verticalAlignment;
739-
if (!align || align === 'stretch') {
740-
align = this.verticalTextAlignment as any;
630+
setFormattedTextDecorationAndTransform() {
631+
const attrText = this.createFormattedTextNative(this.formattedText);
632+
// we override parent class behavior because we apply letterSpacing and lineHeight on a per Span basis
633+
if (majorVersion >= 13 && UIColor.labelColor) {
634+
this.nativeTextViewProtected.textColor = UIColor.labelColor;
741635
}
742-
const font = new Font(style.fontFamily, style.fontSize, style.fontStyle, style.fontWeight);
743-
const iosFont = font.getUIFont(viewFont);
744-
745-
attrDict[NSFontAttributeName] = iosFont;
746-
if (span.color) {
747-
const color = span.color instanceof Color ? span.color : new Color(span.color as any);
748-
attrDict[NSForegroundColorAttributeName] = color.ios;
749-
}
750-
751-
// We don't use isSet function here because defaultValue for backgroundColor is null.
752-
const backgroundColor: Color = style.backgroundColor || (span.parent as FormattedString).backgroundColor;
753-
if (backgroundColor) {
754-
const color = backgroundColor instanceof Color ? backgroundColor : new Color(backgroundColor);
755-
attrDict[NSBackgroundColorAttributeName] = color.ios;
756-
}
757-
758-
const textDecoration: CoreTypes.TextDecorationType = getClosestPropertyValue(textDecorationProperty, span);
759636

760-
if (textDecoration) {
761-
const underline = textDecoration.indexOf('underline') !== -1;
762-
if (underline) {
763-
attrDict[NSUnderlineStyleAttributeName] = underline;
764-
}
765-
766-
const strikethrough = textDecoration.indexOf('line-through') !== -1;
767-
if (strikethrough) {
768-
attrDict[NSStrikethroughStyleAttributeName] = strikethrough;
769-
}
770-
}
771-
772-
if (align && align !== 'stretch') {
773-
if (iosFont) {
774-
attrDict[NSBaselineOffsetAttributeName] = -computeBaseLineOffset(
775-
align,
776-
-iosFont.ascender,
777-
-iosFont.descender,
778-
-iosFont.ascender,
779-
-iosFont.descender,
780-
iosFont.pointSize,
781-
this.currentMaxFontSize
782-
);
783-
}
784-
}
785-
786-
return NSMutableAttributedString.alloc().initWithStringAttributes(text, attrDict as any);
637+
this.nativeTextViewProtected.attributedText = attrText;
787638
}
639+
788640
[paddingTopProperty.getDefault](): CoreTypes.LengthType {
789641
return {
790642
value: 0,

0 commit comments

Comments
 (0)