feat(text-base): Add Span vertical-align support (#8257)

This commit is contained in:
Bundyo (Kamen Bundev)
2020-06-07 00:03:05 +03:00
committed by GitHub
parent e2a9af2bc7
commit faa0181b9c
5 changed files with 165 additions and 89 deletions

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
@ -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");

View File

@ -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";
@ -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;
@ -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));
@ -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 {
@ -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();
@ -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;
}
@ -433,7 +434,7 @@ 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;
}
}
@ -441,10 +442,67 @@ function createSpannableStringBuilder(formattedString: FormattedString): android
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);
@ -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();

View File

@ -1,15 +1,16 @@
// Types
import { TextDecoration, TextAlignment, TextTransform } from "./text-base-common";
import { TextDecoration, TextAlignment, TextTransform, layout, getClosestPropertyValue } from "./text-base-common";
// Requires
import { Font } from "../styling/font";
import {
TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty,
textTransformProperty, letterSpacingProperty, colorProperty, fontInternalProperty, lineHeightProperty,
FormattedString, Span, Color, isBold, resetSymbol
FormattedString, Span, Color, resetSymbol
} from "./text-base-common";
import { isString } from "../../utils/types";
import { ios } from "../../utils/utils";
import { Property } from "../core/properties/properties";
export * from "./text-base-common";
@ -229,7 +230,7 @@ export class TextBase extends TextBaseCommon {
if (this.style.lineHeight) {
const paragraphStyle = NSMutableParagraphStyle.alloc().init();
paragraphStyle.lineSpacing = this.lineHeight;
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 = (<UIButton>this.nativeTextViewProtected).titleLabel.textAlignment;
@ -280,12 +281,15 @@ export class TextBase extends TextBaseCommon {
throw new Error(`Invalid text decoration value: ${style.textDecoration}. Valid values are: 'none', 'underline', 'line-through', 'underline line-through'.`);
}
if (style.letterSpacing !== 0) {
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.setObjectForKey(kern, NSKernAttributeName);
}
}
if (style.color) {
dict.set(NSForegroundColorAttributeName, style.color.ios);
} else if (majorVersion >= 13 && UIColor.labelColor) {
dict.set(NSForegroundColorAttributeName, UIColor.labelColor);
}
const isTextView = this.nativeTextViewProtected instanceof UITextView;
@ -310,19 +314,9 @@ export class TextBase extends TextBaseCommon {
dict.set(NSParagraphStyleAttributeName, paragraphStyle);
}
const source = getTransformedText(this.text ? this.text.toString() : "", this.textTransform);
if (dict.size > 0 || isTextView) {
if (style.color) {
dict.set(NSForegroundColorAttributeName, style.color.ios);
} else if (majorVersion >= 13 && UIColor.labelColor) {
dict.set(NSForegroundColorAttributeName, UIColor.labelColor);
}
}
const text = this.text;
const string = (text === undefined || text === null) ? "" : text.toString();
const source = getTransformedText(string, this.textTransform);
if (dict.size > 0 || isTextView) {
if (isTextView) {
if (isTextView && this.nativeTextViewProtected.font) {
// UITextView's font seems to change inside.
dict.set(NSFontAttributeName, this.nativeTextViewProtected.font);
}
@ -370,59 +364,81 @@ export class TextBase extends TextBaseCommon {
return mas;
}
createMutableStringForSpan(span: Span, text: string): NSMutableAttributedString {
const viewFont = this.nativeTextViewProtected.font;
let attrDict = <{ key: string, value: any }>{};
const style = span.style;
const bold = isBold(style.fontWeight);
const italic = style.fontStyle === "italic";
let fontFamily = span.fontFamily;
let fontSize = span.fontSize;
if (bold || italic || fontFamily || fontSize) {
let font = new Font(style.fontFamily, style.fontSize, style.fontStyle, style.fontWeight);
let iosFont = font.getUIFont(viewFont);
attrDict[NSFontAttributeName] = iosFont;
getBaselineOffset(font: UIFont, align?: string | number): number {
if (!align || align === "baseline") {
return 0;
}
const color = span.color;
if (color) {
attrDict[NSForegroundColorAttributeName] = color.ios;
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 === "super") {
return -this.fontSize * .4;
}
if (align === "sub") {
return (font.descender - font.ascender) * .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 = <Color>(style.backgroundColor
|| (<FormattedString>span.parent).backgroundColor
|| (<TextBase>(<FormattedString>span.parent).parent).backgroundColor);
|| (<TextBase>span.parent.parent).backgroundColor);
if (backgroundColor) {
attrDict[NSBackgroundColorAttributeName] = backgroundColor.ios;
}
let valueSource: typeof style;
if (textDecorationProperty.isSet(style)) {
valueSource = style;
} 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) {
attrDict[NSUnderlineStyleAttributeName] = underline;
}
const strikethrough = textDecorations.indexOf("line-through") !== -1;
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, <any>attrDict);
}
}