8000 feat(text-base): Add Span vertical-align support (#8257) · NativeScript/NativeScript@faa0181 · GitHub
[go: up one dir, main page]

Skip to content

Commit faa0181

Browse files
authored
feat(text-base): Add Span vertical-align support (#8257)
1 parent e2a9af2 commit faa0181

File tree

5 files changed

+161
-85
lines changed

5 files changed

+161
-85
lines changed

nativescript-core/ui/styling/style-properties.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,13 +358,18 @@ export namespace HorizontalAlignment {
358358
export const horizontalAlignmentProperty = new CssProperty<Style, HorizontalAlignment>({ name: "horizontalAlignment", cssName: "horizontal-align", defaultValue: HorizontalAlignment.STRETCH, affectsLayout: isIOS, valueConverter: HorizontalAlignment.parse });
359359
horizontalAlignmentProperty.register(Style);
360360

361-
export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch";
361+
export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch" | "text-top" | "text-bottom" | "super" | "sub" | "baseline";
362362
export namespace VerticalAlignment {
363363
export const TOP: "top" = "top";
364364
export const MIDDLE: "middle" = "middle";
365365
export const BOTTOM: "bottom" = "bottom";
366366
export const STRETCH: "stretch" = "stretch";
367-
export const isValid = makeValidator<VerticalAlignment>(TOP, MIDDLE, BOTTOM, STRETCH);
367+
export const TEXTTOP: "text-top" = "text-top";
368+
export const TEXTBOTTOM: "text-bottom" = "text-bottom";
369+
export const SUPER: "super" = "super";
370+
export const SUB: "sub" = "sub";
371+
export const BASELINE: "baseline" = "baseline";
372+
export const isValid = makeValidator<VerticalAlignment>(TOP, MIDDLE, BOTTOM, STRETCH, TEXTTOP, TEXTBOTTOM, SUPER, SUB, BASELINE);
368373
export const parse = (value: string) => value.toLowerCase() === "center" ? MIDDLE : parseStrict(value);
369374
const parseStrict = makeParser<VerticalAlignment>(isValid);
370375
}

nativescript-core/ui/text-base/span.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class Span extends ViewBase implements SpanDefinition {
6565
}
6666
set text(value: string) {
6767
if (this._text !== value) {
68-
this._text = value;
68+
this._text = value && value.replace("\\n", "\n").replace("\\t", "\t");
6969
this.notifyPropertyChange("text", value);
7070
}
7171
}

nativescript-core/ui/text-base/text-base-common.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,18 @@ function onFormattedTextPropertyChanged(textBase: TextBaseCommon, oldValue: Form
201201
}
202202
}
203203

204+
export function getClosestPropertyValue<T>(property: CssProperty<any, T>, span: Span): T {
205+
if (property.isSet(span.style)) {
206+
return span.style[property.name];
207+
} else if (property.isSet(span.parent.style)) {
208+
// parent is FormattedString
209+
return span.parent.style[property.name];
210+
} else if (property.isSet(span.parent.parent.style)) {
211+
// parent.parent is TextBase
212+
return span.parent.parent.style[property.name];
213+
}
214+
}
215+
204216
const textAlignmentConverter = makeParser<TextAlignment>(makeValidator<TextAlignment>("initial", "left", "center", "right"));
205217
export const textAlignmentProperty = new InheritedCssProperty<Style, TextAlignment>({ name: "textAlignment", cssName: "text-align", defaultValue: "initial", valueConverter: textAlignmentConverter });
206218
textAlignmentProperty.register(Style);
@@ -217,10 +229,10 @@ const textDecorationConverter = makeParser<TextDecoration>(makeValidator<TextDec
217229
export const textDecorationProperty = new CssProperty<Style, TextDecoration>({ name: "textDecoration", cssName: "text-decoration", defaultValue: "none", valueConverter: textDecorationConverter });
218230
textDecorationProperty.register(Style);
219231

220-
export const letterSpacingProperty = new CssProperty<Style, number>({ name: "letterSpacing", cssName: "letter-spacing", defaultValue: 0, affectsLayout: isIOS, valueConverter: v => parseFloat(v) });
232+
export const letterSpacingProperty = new InheritedCssProperty<Style, number>({ name: "letterSpacing", cssName: "letter-spacing", defaultValue: 0, affectsLayout: isIOS, valueConverter: v => parseFloat(v) });
221233
letterSpacingProperty.register(Style);
222234

223-
export const lineHeightProperty = new CssProperty<Style, number>({ name: "lineHeight", cssName: "line-height", affectsLayout: isIOS, valueConverter: v => parseFloat(v) });
235+
export const lineHeightProperty = new InheritedCssProperty<Style, number>({ name: "lineHeight", cssName: "line-height", affectsLayout: isIOS, valueConverter: v => parseFloat(v) });
224236
lineHeightProperty.register(Style);
225237

226238
export const resetSymbol = Symbol("textPropertyDefault");

nativescript-core/ui/text-base/text-base.android.ts

Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Types
2-
import { TextTransformation, TextDecoration, TextAlignment, TextTransform, WhiteSpace } from "./text-base-common";
2+
import { TextTransformation, TextDecoration, TextAlignment, TextTransform, WhiteSpace, getClosestPropertyValue } from "./text-base-common";
33

44
// Requires
55
import { Font } from "../styling/font";
@@ -33,7 +33,7 @@ function initializeTextTransformation(): void {
3333
// NOTE: Do we need to transform the new text here?
3434
const formattedText = this.textBase.formattedText;
3535
if (formattedText) {
36-
return createSpannableStringBuilder(formattedText);
36+
return createSpannableStringBuilder(formattedText, (<android.widget.TextView>view).getTextSize());
3737
}
3838
else {
3939
const text = this.textBase.text;
@@ -170,7 +170,7 @@ export class TextBase extends TextBaseCommon {
170170
return;
171171
}
172172

173-
const spannableStringBuilder = createSpannableStringBuilder(value);
173+
const spannableStringBuilder = createSpannableStringBuilder(value, this.style.fontSize);
174174
nativeView.setText(<any>spannableStringBuilder);
175175
this._setTappableState(isStringTappable(value));
176176

@@ -265,10 +265,11 @@ export class TextBase extends TextBaseCommon {
265265
}
266266

267267
[lineHeightProperty.getDefault](): number {
268-
return this.nativeTextViewProtected.getLineSpacingExtra() / layout.getDisplayDensity();
268+
return this.nativeTextViewProtected.getLineHeight() / layout.getDisplayDensity();
269269
}
270270
[lineHeightProperty.setNative](value: number) {
271-
this.nativeTextViewProtected.setLineSpacing(value * layout.getDisplayDensity(), 1);
271+
const fontHeight = this.nativeTextViewProtected.getPaint().getFontMetricsInt(null);
272+
this.nativeTextViewProtected.setLineSpacing(Math.max(value - fontHeight, 0) * layout.getDisplayDensity(), 1);
272273
}
273274

274275
[fontInternalProperty.getDefault](): android.graphics.Typeface {
@@ -348,7 +349,7 @@ export class TextBase extends TextBaseCommon {
348349

349350
let transformedText: any;
350351
if (this.formattedText) {
351-
transformedText = createSpannableStringBuilder(this.formattedText);
352+
transformedText = createSpannableStringBuilder(this.formattedText, this.style.fontSize);
352353
} else {
353354
const text = this.text;
354355
const stringValue = (text === null || text === undefined) ? "" : text.toString();
@@ -415,7 +416,7 @@ function isStringTappable(formattedString: FormattedString) {
415416
return false;
416417
}
417418

418-
function createSpannableStringBuilder(formattedString: FormattedString): android.text.SpannableStringBuilder {
419+
function createSpannableStringBuilder(formattedString: FormattedString, defaultFontSize: number): android.text.SpannableStringBuilder {
419420
if (!formattedString || !formattedString.parent) {
420421
return null;
421422
}
@@ -433,18 +434,75 @@ function createSpannableStringBuilder(formattedString: FormattedString): android
433434
spanLength = spanText.length;
434435
if (spanLength > 0) {
435436
ssb.insert(spanStart, spanText);
436-
setSpanModifiers(ssb, span, spanStart, spanStart + spanLength);
437+
setSpanModifiers(ssb, span, spanStart, spanStart + spanLength, defaultFontSize);
437438
spanStart += spanLength;
438439
}
439440
}
440441

441442
return ssb;
442443
}
443444

444-
function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, start: number, end: number): void {
445+
class BaselineAdjustedSpan extends android.text.style.MetricAffectingSpan {
446+
fontSize: number;
447+
align: string | number = "baseline";
448+
449+
constructor(fontSize: number, align?: string | number) {
450+
super();
451+
452+
this.align = align;
453+
this.fontSize = fontSize;
454+
}
455+
456+
updateDrawState(paint: android.text.TextPaint) {
457+
this.updateState(paint);
458+
}
459+
460+
updateMeasureState(paint: android.text.TextPaint) {
461+
this.updateState(paint);
462+
}
463+
464+
updateState(paint: android.text.TextPaint) {
465+
const metrics = paint.getFontMetrics();
466+
467+
if (!this.align || this.align === "baseline") {
468+
return;
469+
}
470+
471+
if (this.align === "top") {
472+
return paint.baselineShift = -this.fontSize - metrics.bottom - metrics.top;
473+
}
474+
475+
if (this.align === "bottom") {
476+
return paint.baselineShift = metrics.bottom;
477+
}
478+
479+
if (this.align === "text-top") {
480+
return paint.baselineShift = -this.fontSize - metrics.descent - metrics.ascent;
481+
}
482+
483+
if (this.align === "text-bottom") {
484+
return paint.baselineShift = metrics.bottom - metrics.descent;
485+
}
486+
487+
if (this.align === "middle") {
488+
return paint.baselineShift = (metrics.descent - metrics.ascent) / 2 - metrics.descent;
489+
}
490+
491+
if (this.align === "super") {
492+
return paint.baselineShift = -this.fontSize * .4;
493+
}
494+
495+
if (this.align === "sub") {
496+
return paint.baselineShift = (metrics.descent - metrics.ascent) * .4;
497+
}
498+
}
499+
}
500+
501+
function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, start: number, end: number, defaultFontSize: number): void {
445502
const spanStyle = span.style;
446503
const bold = isBold(spanStyle.fontWeight);
447504
const italic = spanStyle.fontStyle === "italic";
505+
const align = spanStyle.verticalAlignment;
448506

449507
if (bold && italic) {
450508
ssb.setSpan(new android.text.style.StyleSpan(android.graphics.Typeface.BOLD_ITALIC), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -474,45 +532,30 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span,
474532
ssb.setSpan(new android.text.style.ForegroundColorSpan(color.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
475533
}
476534

477-
let backgroundColor: Color;
478-
if (backgroundColorProperty.isSet(spanStyle)) {
479-
backgroundColor = spanStyle.backgroundColor;
480-
} else if (backgroundColorProperty.isSet(span.parent.style)) {
481-
// parent is FormattedString
482-
backgroundColor = span.parent.style.backgroundColor;
483-
} else if (backgroundColorProperty.isSet(span.parent.parent.style)) {
484-
// parent.parent is TextBase
485-
backgroundColor = span.parent.parent.style.backgroundColor;
486-
}
535+
const backgroundColor: Color = getClosestPropertyValue(backgroundColorProperty, span);
487536

488537
if (backgroundColor) {
489538
ssb.setSpan(new android.text.style.BackgroundColorSpan(backgroundColor.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
490539
}
491540

492-
let valueSource: typeof spanStyle;
493-
if (textDecorationProperty.isSet(spanStyle)) {
494-
valueSource = spanStyle;
495-
} else if (textDecorationProperty.isSet(span.parent.style)) {
496-
// span.parent is FormattedString
497-
valueSource = span.parent.style;
498-
} else if (textDecorationProperty.isSet(span.parent.parent.style)) {
499-
// span.parent.parent is TextBase
500-
valueSource = span.parent.parent.style;
501-
}
541+
const textDecoration: TextDecoration = getClosestPropertyValue(textDecorationProperty, span);
502542

503-
if (valueSource) {
504-
const textDecorations = valueSource.textDecoration;
505-
const underline = textDecorations.indexOf("underline") !== -1;
543+
if (textDecoration) {
544+
const underline = textDecoration.indexOf("underline") !== -1;
506545
if (underline) {
507546
ssb.setSpan(new android.text.style.UnderlineSpan(), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
508547
}
509548

510-
const strikethrough = textDecorations.indexOf("line-through") !== -1;
549+
const strikethrough = textDecoration.indexOf("line-through") !== -1;
511550
if (strikethrough) {
512551
ssb.setSpan(new android.text.style.StrikethroughSpan(), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
513552
}
514553
}
515554

555+
if (align) {
556+
ssb.setSpan(new BaselineAdjustedSpan(defaultFontSize * layout.getDisplayDensity(), align), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
557+
}
558+
516559
const tappable = span.tappable;
517560
if (tappable) {
518561
initializeClickableSpan();

0 commit comments

Comments
 (0)
0