Skip to content

Commit

Permalink
feat(text-base): Add Span vertical-align support (#8257)
Browse files Browse the repository at this point in the history
  • Loading branch information
bundyo authored Jun 6, 2020
1 parent e2a9af2 commit faa0181
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 85 deletions.
9 changes: 7 additions & 2 deletions nativescript-core/ui/styling/style-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,13 +358,18 @@ export namespace HorizontalAlignment {
export const horizontalAlignmentProperty = new CssProperty<Style, HorizontalAlignment>({ name: "horizontalAlignment", cssName: "horizontal-align", defaultValue: HorizontalAlignment.STRETCH, affectsLayout: isIOS, valueConverter: HorizontalAlignment.parse });
horizontalAlignmentProperty.register(Style);

export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch";
export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch" | "text-top" | "text-bottom" | "super" | "sub" | "baseline";
export namespace VerticalAlignment {
export const TOP: "top" = "top";
export const MIDDLE: "middle" = "middle";
export const BOTTOM: "bottom" = "bottom";
export const STRETCH: "stretch" = "stretch";
export const isValid = makeValidator<VerticalAlignment>(TOP, MIDDLE, BOTTOM, STRETCH);
export const TEXTTOP: "text-top" = "text-top";
export const TEXTBOTTOM: "text-bottom" = "text-bottom";
export const SUPER: "super" = "super";
export const SUB: "sub" = "sub";
export const BASELINE: "baseline" = "baseline";
export const isValid = makeValidator<VerticalAlignment>(TOP, MIDDLE, BOTTOM, STRETCH, TEXTTOP, TEXTBOTTOM, SUPER, SUB, BASELINE);
export const parse = (value: string) => value.toLowerCase() === "center" ? MIDDLE : parseStrict(value);
const parseStrict = makeParser<VerticalAlignment>(isValid);
}
Expand Down
2 changes: 1 addition & 1 deletion nativescript-core/ui/text-base/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class Span extends ViewBase implements SpanDefinition {
}
set text(value: string) {
if (this._text !== value) {
this._text = value;
this._text = value && value.replace("\\n", "\n").replace("\\t", "\t");
this.notifyPropertyChange("text", value);
}
}
Expand Down
16 changes: 14 additions & 2 deletions nativescript-core/ui/text-base/text-base-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ function onFormattedTextPropertyChanged(textBase: TextBaseCommon, oldValue: Form
}
}

export function getClosestPropertyValue<T>(property: CssProperty<any, T>, span: Span): T {
if (property.isSet(span.style)) {
return span.style[property.name];
} else if (property.isSet(span.parent.style)) {
// parent is FormattedString
return span.parent.style[property.name];
} else if (property.isSet(span.parent.parent.style)) {
// parent.parent is TextBase
return span.parent.parent.style[property.name];
}
}

const textAlignmentConverter = makeParser<TextAlignment>(makeValidator<TextAlignment>("initial", "left", "center", "right"));
export const textAlignmentProperty = new InheritedCssProperty<Style, TextAlignment>({ name: "textAlignment", cssName: "text-align", defaultValue: "initial", valueConverter: textAlignmentConverter });
textAlignmentProperty.register(Style);
Expand All @@ -217,10 +229,10 @@ const textDecorationConverter = makeParser<TextDecoration>(makeValidator<TextDec
export const textDecorationProperty = new CssProperty<Style, TextDecoration>({ name: "textDecoration", cssName: "text-decoration", defaultValue: "none", valueConverter: textDecorationConverter });
textDecorationProperty.register(Style);

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

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

export const resetSymbol = Symbol("textPropertyDefault");
109 changes: 76 additions & 33 deletions nativescript-core/ui/text-base/text-base.android.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Types
import { TextTransformation, TextDecoration, TextAlignment, TextTransform, WhiteSpace } from "./text-base-common";
import { TextTransformation, TextDecoration, TextAlignment, TextTransform, WhiteSpace, getClosestPropertyValue } from "./text-base-common";

// Requires
import { Font } from "../styling/font";
Expand Down Expand Up @@ -33,7 +33,7 @@ function initializeTextTransformation(): void {
// NOTE: Do we need to transform the new text here?
const formattedText = this.textBase.formattedText;
if (formattedText) {
return createSpannableStringBuilder(formattedText);
return createSpannableStringBuilder(formattedText, (<android.widget.TextView>view).getTextSize());
}
else {
const text = this.textBase.text;
Expand Down Expand Up @@ -170,7 +170,7 @@ export class TextBase extends TextBaseCommon {
return;
}

const spannableStringBuilder = createSpannableStringBuilder(value);
const spannableStringBuilder = createSpannableStringBuilder(value, this.style.fontSize);
nativeView.setText(<any>spannableStringBuilder);
this._setTappableState(isStringTappable(value));

Expand Down Expand Up @@ -265,10 +265,11 @@ export class TextBase extends TextBaseCommon {
}

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

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

let transformedText: any;
if (this.formattedText) {
transformedText = createSpannableStringBuilder(this.formattedText);
transformedText = createSpannableStringBuilder(this.formattedText, this.style.fontSize);
} else {
const text = this.text;
const stringValue = (text === null || text === undefined) ? "" : text.toString();
Expand Down Expand Up @@ -415,7 +416,7 @@ function isStringTappable(formattedString: FormattedString) {
return false;
}

function createSpannableStringBuilder(formattedString: FormattedString): android.text.SpannableStringBuilder {
function createSpannableStringBuilder(formattedString: FormattedString, defaultFontSize: number): android.text.SpannableStringBuilder {
if (!formattedString || !formattedString.parent) {
return null;
}
Expand All @@ -433,18 +434,75 @@ function createSpannableStringBuilder(formattedString: FormattedString): android
spanLength = spanText.length;
if (spanLength > 0) {
ssb.insert(spanStart, spanText);
setSpanModifiers(ssb, span, spanStart, spanStart + spanLength);
setSpanModifiers(ssb, span, spanStart, spanStart + spanLength, defaultFontSize);
spanStart += spanLength;
}
}

return ssb;
}

function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, start: number, end: number): void {
class BaselineAdjustedSpan extends android.text.style.MetricAffectingSpan {
fontSize: number;
align: string | number = "baseline";

constructor(fontSize: number, align?: string | number) {
super();

this.align = align;
this.fontSize = fontSize;
}

updateDrawState(paint: android.text.TextPaint) {
this.updateState(paint);
}

updateMeasureState(paint: android.text.TextPaint) {
this.updateState(paint);
}

updateState(paint: android.text.TextPaint) {
const metrics = paint.getFontMetrics();

if (!this.align || this.align === "baseline") {
return;
}

if (this.align === "top") {
return paint.baselineShift = -this.fontSize - metrics.bottom - metrics.top;
}

if (this.align === "bottom") {
return paint.baselineShift = metrics.bottom;
}

if (this.align === "text-top") {
return paint.baselineShift = -this.fontSize - metrics.descent - metrics.ascent;
}

if (this.align === "text-bottom") {
return paint.baselineShift = metrics.bottom - metrics.descent;
}

if (this.align === "middle") {
return paint.baselineShift = (metrics.descent - metrics.ascent) / 2 - metrics.descent;
}

if (this.align === "super") {
return paint.baselineShift = -this.fontSize * .4;
}

if (this.align === "sub") {
return paint.baselineShift = (metrics.descent - metrics.ascent) * .4;
}
}
}

function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, start: number, end: number, defaultFontSize: number): void {
const spanStyle = span.style;
const bold = isBold(spanStyle.fontWeight);
const italic = spanStyle.fontStyle === "italic";
const align = spanStyle.verticalAlignment;

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

let backgroundColor: Color;
if (backgroundColorProperty.isSet(spanStyle)) {
backgroundColor = spanStyle.backgroundColor;
} else if (backgroundColorProperty.isSet(span.parent.style)) {
// parent is FormattedString
backgroundColor = span.parent.style.backgroundColor;
} else if (backgroundColorProperty.isSet(span.parent.parent.style)) {
// parent.parent is TextBase
backgroundColor = span.parent.parent.style.backgroundColor;
}
const backgroundColor: Color = getClosestPropertyValue(backgroundColorProperty, span);

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

let valueSource: typeof spanStyle;
if (textDecorationProperty.isSet(spanStyle)) {
valueSource = spanStyle;
} else if (textDecorationProperty.isSet(span.parent.style)) {
// span.parent is FormattedString
valueSource = span.parent.style;
} else if (textDecorationProperty.isSet(span.parent.parent.style)) {
// span.parent.parent is TextBase
valueSource = span.parent.parent.style;
}
const textDecoration: TextDecoration = getClosestPropertyValue(textDecorationProperty, span);

if (valueSource) {
const textDecorations = valueSource.textDecoration;
const underline = textDecorations.indexOf("underline") !== -1;
if (textDecoration) {
const underline = textDecoration.indexOf("underline") !== -1;
if (underline) {
ssb.setSpan(new android.text.style.UnderlineSpan(), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

const strikethrough = textDecorations.indexOf("line-through") !== -1;
const strikethrough = textDecoration.indexOf("line-through") !== -1;
if (strikethrough) {
ssb.setSpan(new android.text.style.StrikethroughSpan(), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}

if (align) {
ssb.setSpan(new BaselineAdjustedSpan(defaultFontSize * layout.getDisplayDensity(), align), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

const tappable = span.tappable;
if (tappable) {
initializeClickableSpan();
Expand Down
Loading

0 comments on commit faa0181

Please sign in to comment.