Nathan Walker
2023-10-09 12:00:21 -07:00
committed by GitHub
parent 93e24783a1
commit d6478237ec
18 changed files with 230 additions and 76 deletions

View File

@ -10,4 +10,6 @@
-(void)nativeScriptSetFormattedTextDecorationAndTransform:(NSDictionary*)details letterSpacing:(CGFloat)letterSpacing lineHeight:(CGFloat)lineHeight;
-(void)nativeScriptSetFormattedTextStroke:(CGFloat)width color:(UIColor*)color;
@end

View File

@ -130,4 +130,19 @@
((UILabel*)self).attributedText = attrText;
}
}
-(void)nativeScriptSetFormattedTextStroke:(CGFloat)width color:(UIColor*)color {
if (width > 0) {
NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] initWithAttributedString:((UILabel*)self).attributedText];
[attrText addAttribute:NSStrokeWidthAttributeName value:[NSNumber numberWithFloat:width] range:(NSRange){
0,
attrText.length
}];
[attrText addAttribute:NSStrokeColorAttributeName value:color range:(NSRange){
0,
attrText.length
}];
((UILabel*)self).attributedText = attrText;
}
}
@end

View File

@ -68,7 +68,7 @@ export { CSSHelper } from './styling/css-selector';
export { Switch } from './switch';
export { TabView, TabViewItem } from './tab-view';
export { TextBase, getTransformedText, letterSpacingProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, whiteSpaceProperty, textOverflowProperty, lineHeightProperty } from './text-base';
export { TextBase, getTransformedText, letterSpacingProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, textStrokeProperty, whiteSpaceProperty, textOverflowProperty, lineHeightProperty } from './text-base';
export { FormattedString } from './text-base/formatted-string';
export { Span } from './text-base/span';
export { TextField } from './text-field';

View File

@ -7,12 +7,10 @@ import { CoreTypes } from '../../core-types';
export * from '../text-base';
let TextView: typeof android.widget.TextView;
@CSSType('Label')
export class Label extends TextBase implements LabelDefinition {
nativeViewProtected: android.widget.TextView;
nativeTextViewProtected: android.widget.TextView;
nativeViewProtected: org.nativescript.widgets.StyleableTextView;
nativeTextViewProtected: org.nativescript.widgets.StyleableTextView;
get textWrap(): boolean {
return this.style.whiteSpace === 'normal';
@ -27,11 +25,7 @@ export class Label extends TextBase implements LabelDefinition {
@profile
public createNativeView() {
if (!TextView) {
TextView = android.widget.TextView;
}
return new TextView(this._context);
return new org.nativescript.widgets.StyleableTextView(this._context);
}
public initNativeView(): void {

View File

@ -26,15 +26,11 @@ const LENGTH_RE = /^-?[0-9]+[a-zA-Z%]*?$/;
*/
const isLength = (v) => v === '0' || LENGTH_RE.test(v);
/**
* Parse a string into ShadowCSSValues
* Supports any valid css box/text shadow combination.
*
* inspired by https://github.com/jxnblk/css-box-shadow/blob/master/index.js (MIT License)
*
* @param value
*/
export function parseCSSShadow(value: string): ShadowCSSValues {
export function parseCSSShorthand(value: string): {
values: Array<CoreTypes.LengthType>;
color: string;
inset: boolean;
} {
const parts = value.trim().split(PARTS_RE);
const inset = parts.includes('inset');
const first = parts[0];
@ -44,15 +40,15 @@ export function parseCSSShadow(value: string): ShadowCSSValues {
return null;
}
let colorRaw = 'black';
let color = 'black';
if (first && !isLength(first) && first !== 'inset') {
colorRaw = first;
color = first;
} else if (last && !isLength(last)) {
colorRaw = last;
color = last;
}
const nums = parts
const values = parts
.filter((n) => n !== 'inset')
.filter((n) => n !== colorRaw)
.filter((n) => n !== color)
.map((val) => {
try {
return Length.parse(val);
@ -60,51 +56,30 @@ export function parseCSSShadow(value: string): ShadowCSSValues {
return CoreTypes.zeroLength;
}
});
const [offsetX, offsetY, blurRadius, spreadRadius] = nums;
return {
inset,
color,
values,
};
}
/**
* Parse a string into ShadowCSSValues
* Supports any valid css box/text shadow combination.
*
* inspired by https://github.com/jxnblk/css-box-shadow/blob/master/index.js (MIT License)
*
* @param value
*/
export function parseCSSShadow(value: string): ShadowCSSValues {
const data = parseCSSShorthand(value);
const [offsetX, offsetY, blurRadius, spreadRadius] = data.values;
return {
inset: data.inset,
offsetX: offsetX,
offsetY: offsetY,
blurRadius: blurRadius,
spreadRadius: spreadRadius,
color: new Color(colorRaw),
color: new Color(data.color),
};
}
// if (value.indexOf('rgb') > -1) {
// arr = value.split(' ');
// colorRaw = arr.pop();
// } else {
// arr = value.split(/[ ,]+/);
// colorRaw = arr.pop();
// }
// let offsetX: number;
// let offsetY: number;
// let blurRadius: number; // not currently in use
// let spreadRadius: number; // maybe rename this to just radius
// let color: Color = new Color(colorRaw);
// if (arr.length === 2) {
// offsetX = parseFloat(arr[0]);
// offsetY = parseFloat(arr[1]);
// } else if (arr.length === 3) {
// offsetX = parseFloat(arr[0]);
// offsetY = parseFloat(arr[1]);
// blurRadius = parseFloat(arr[2]);
// } else if (arr.length === 4) {
// offsetX = parseFloat(arr[0]);
// offsetY = parseFloat(arr[1]);
// blurRadius = parseFloat(arr[2]);
// spreadRadius = parseFloat(arr[3]);
// } else {
// throw new Error('Expected 3, 4 or 5 parameters. Actual: ' + value);
// }
// return {
// offsetX: offsetX,
// offsetY: offsetY,
// blurRadius: blurRadius,
// spreadRadius: spreadRadius,
// color: color,
// };

View File

@ -0,0 +1,30 @@
import { parseCSSStroke } from './css-stroke';
import { CoreTypes } from '../../core-types';
import { Length } from './style-properties';
import { Color } from '../../color';
describe('css-text-stroke', () => {
it('empty', () => {
const stroke = parseCSSStroke('');
expect(stroke.width).toBe(CoreTypes.zeroLength);
expect(stroke.color).toEqual(new Color('black'));
});
it('1px navy', () => {
const stroke = parseCSSStroke('1px navy');
expect(stroke.width).toEqual(Length.parse('1px'));
expect(stroke.color).toEqual(new Color('navy'));
});
it('5 green', () => {
const stroke = parseCSSStroke('5 green');
expect(stroke.width).toEqual(Length.parse('5'));
expect(stroke.color).toEqual(new Color('green'));
});
it('2px #999', () => {
const stroke = parseCSSStroke('2px #999');
expect(stroke.width).toEqual(Length.parse('2px'));
expect(stroke.color).toEqual(new Color('#999'));
});
});

View File

@ -0,0 +1,23 @@
import { CoreTypes } from '../../core-types';
import { Color } from '../../color';
import { parseCSSShorthand } from './css-shadow';
export interface StrokeCSSValues {
width: CoreTypes.LengthType;
color: Color;
}
/**
* Parse a string into StrokeCSSValues
* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-text-stroke
* @param value
*/
export function parseCSSStroke(value: string): StrokeCSSValues {
const data = parseCSSShorthand(value);
const [width] = data.values;
return {
width,
color: new Color(data.color),
};
}

View File

@ -11,6 +11,7 @@ import { Trace } from '../../../trace';
import { CoreTypes } from '../../../core-types';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState } from '../../../accessibility/accessibility-types';
import { ShadowCSSValues } from '../css-shadow';
import { StrokeCSSValues } from '../css-stroke';
export interface CommonLayoutParams {
width: number;
@ -171,6 +172,7 @@ export class Style extends Observable implements StyleDefinition {
public textDecoration: CoreTypes.TextDecorationType;
public textTransform: CoreTypes.TextTransformType;
public textShadow: ShadowCSSValues;
public textStroke: StrokeCSSValues;
public whiteSpace: CoreTypes.WhiteSpaceType;
public textOverflow: CoreTypes.TextOverflowType;

View File

@ -5,9 +5,10 @@ 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, letterSpacingProperty, whiteSpaceProperty, lineHeightProperty, isBold, resetSymbol } from './text-base-common';
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 { StrokeCSSValues } from '../styling/css-stroke';
import { FormattedString } from './formatted-string';
import { Span } from './span';
import { CoreTypes } from '../../core-types';
@ -15,7 +16,6 @@ import { layout } from '../../utils';
import { SDK_VERSION } from '../../utils/constants';
import { isString, isNullOrUndefined } from '../../utils/types';
import { accessibilityIdentifierProperty } from '../../accessibility/accessibility-properties';
import * as Utils from '../../utils';
import { testIDProperty } from '../../ui/core/view';
export * from './text-base-common';
@ -169,9 +169,9 @@ function initializeBaselineAdjustedSpan(): void {
}
export class TextBase extends TextBaseCommon {
nativeViewProtected: android.widget.TextView;
nativeViewProtected: org.nativescript.widgets.StyleableTextView;
// @ts-ignore
nativeTextViewProtected: android.widget.TextView;
nativeTextViewProtected: org.nativescript.widgets.StyleableTextView;
private _defaultTransformationMethod: android.text.method.TransformationMethod;
private _paintFlags: number;
private _minHeight: number;
@ -237,6 +237,9 @@ export class TextBase extends TextBaseCommon {
this._setNativeText(reset);
}
[textStrokeProperty.setNative](value: StrokeCSSValues) {
this._setNativeText();
}
createFormattedTextNative(value: FormattedString) {
return createSpannableStringBuilder(value, this.style.fontSize);
}
@ -386,7 +389,7 @@ export class TextBase extends TextBaseCommon {
}
}
[textDecorationProperty.getDefault](value: number) {
[textDecorationProperty.getDefault]() {
return (this._paintFlags = this.nativeTextViewProtected.getPaintFlags());
}
@ -410,7 +413,7 @@ export class TextBase extends TextBaseCommon {
}
}
[textShadowProperty.getDefault](value: number) {
[textShadowProperty.getDefault]() {
return {
radius: this.nativeTextViewProtected.getShadowRadius(),
offsetX: this.nativeTextViewProtected.getShadowDx(),
@ -498,6 +501,13 @@ export class TextBase extends TextBaseCommon {
transformedText = getTransformedText(stringValue, this.textTransform);
}
if (this.style?.textStroke) {
this.nativeViewProtected.setTextStroke(Length.toDevicePixels(this.style.textStroke.width), this.style.textStroke.color.android, this.style.color.android);
} else if (this.nativeViewProtected.setTextStroke) {
// reset
this.nativeViewProtected.setTextStroke(0, 0, 0);
}
this.nativeTextViewProtected.setText(<any>transformedText);
}

View File

@ -5,6 +5,7 @@ import { Length } from '../styling/style-properties';
import { Property, CssProperty, InheritedCssProperty } from '../core/properties';
import { CoreTypes } from '../../core-types';
import { ShadowCSSValues } from '../styling/css-shadow';
import { StrokeCSSValues } from '../styling/css-stroke';
export class TextBase extends View implements AddChildFromBuilder {
/**
@ -138,6 +139,7 @@ export const textAlignmentProperty: InheritedCssProperty<Style, CoreTypes.TextAl
export const textDecorationProperty: CssProperty<Style, CoreTypes.TextDecorationType>;
export const textTransformProperty: CssProperty<Style, CoreTypes.TextTransformType>;
export const textShadowProperty: CssProperty<Style, ShadowCSSValues>;
export const textStrokeProperty: CssProperty<Style, StrokeCSSValues>;
export const whiteSpaceProperty: CssProperty<Style, CoreTypes.WhiteSpaceType>;
export const textOverflowProperty: CssProperty<Style, CoreTypes.TextOverflowType>;
export const letterSpacingProperty: CssProperty<Style, number>;

View File

@ -5,11 +5,12 @@ import { ShadowCSSValues } from '../styling/css-shadow';
// Requires
import { Font } from '../styling/font';
import { iosAccessibilityAdjustsFontSizeProperty, iosAccessibilityMaxFontScaleProperty, iosAccessibilityMinFontScaleProperty } from '../../accessibility/accessibility-properties';
import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, letterSpacingProperty, lineHeightProperty, maxLinesProperty, resetSymbol } from './text-base-common';
import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, textShadowProperty, textStrokeProperty, letterSpacingProperty, lineHeightProperty, maxLinesProperty, resetSymbol } from './text-base-common';
import { Color } from '../../color';
import { FormattedString } from './formatted-string';
import { Span } from './span';
import { colorProperty, fontInternalProperty, fontScaleInternalProperty, Length } from '../styling/style-properties';
import { StrokeCSSValues } from '../styling/css-stroke';
import { isString, isNullOrUndefined } from '../../utils/types';
import { iOSNativeHelper, layout } from '../../utils';
import { Trace } from '../../trace';
@ -247,6 +248,10 @@ export class TextBase extends TextBaseCommon {
this._setNativeText();
}
[textStrokeProperty.setNative](value: StrokeCSSValues) {
this._setNativeText();
}
[letterSpacingProperty.setNative](value: number) {
this._setNativeText();
}
@ -306,16 +311,19 @@ export class TextBase extends TextBaseCommon {
const letterSpacing = this.style.letterSpacing ? this.style.letterSpacing : 0;
const lineHeight = this.style.lineHeight ? this.style.lineHeight : 0;
if (this.formattedText) {
(<any>this.nativeTextViewProtected).nativeScriptSetFormattedTextDecorationAndTransformLetterSpacingLineHeight(this.getFormattedStringDetails(this.formattedText), letterSpacing, lineHeight);
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);
(<any>this.nativeTextViewProtected).nativeScriptSetTextDecorationAndTransformTextDecorationLetterSpacingLineHeight(text, this.style.textDecoration || '', letterSpacing, lineHeight);
this.nativeTextViewProtected.nativeScriptSetTextDecorationAndTransformTextDecorationLetterSpacingLineHeight(text, this.style.textDecoration || '', letterSpacing, lineHeight);
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);
}
}
createFormattedTextNative(value: FormattedString) {

View File

@ -14,6 +14,7 @@ import { CoreTypes } from '../../core-types';
import { TextBase as TextBaseDefinition } from '.';
import { Color } from '../../color';
import { ShadowCSSValues, parseCSSShadow } from '../styling/css-shadow';
import { StrokeCSSValues, parseCSSStroke } from '../styling/css-stroke';
const CHILD_SPAN = 'Span';
const CHILD_FORMATTED_TEXT = 'formattedText';
@ -288,6 +289,16 @@ export const textShadowProperty = new CssProperty<Style, string | ShadowCSSValue
});
textShadowProperty.register(Style);
export const textStrokeProperty = new CssProperty<Style, string | StrokeCSSValues>({
name: 'textStroke',
cssName: 'text-stroke',
affectsLayout: global.isIOS,
valueConverter: (value) => {
return parseCSSStroke(value);
},
});
textStrokeProperty.register(Style);
const whiteSpaceConverter = makeParser<CoreTypes.WhiteSpaceType>(makeValidator<CoreTypes.WhiteSpaceType>('initial', 'normal', 'nowrap'));
export const whiteSpaceProperty = new CssProperty<Style, CoreTypes.WhiteSpaceType>({
name: 'whiteSpace',

View File

@ -403,6 +403,13 @@
setImageLoadedListener(listener: image.Worker.OnImageLoadedListener): void;
}
export class StyleableTextView extends android.widget.TextView {
public static class: java.lang.Class<org.nativescript.widgets.StyleableTextView>;
public onDraw(param0: globalAndroid.graphics.Canvas): void;
public setTextStroke(param0: number, param1: number, param2: number): void;
public constructor(param0: globalAndroid.content.Context);
}
export enum TabIconRenderingMode {
original,
template

View File

@ -25831,8 +25831,11 @@ declare class UIView extends UIResponder implements CALayerDelegate, NSCoding, U
layoutSubviews(): void;
nativeScriptSetFormattedTextDecorationAndTransformLetterSpacingLineHeight(details: NSDictionary<any, any>, letterSpacing: number, lineHeight: number): void;
nativeScriptSetFormattedTextStrokeColor(width: number, color: UIColor): void;
nativeScriptSetTextDecorationAndTransformTextDecorationLetterSpacingLineHeight(text: string, textDecoration: string, letterSpacing: number, lineHeight: number): void;
needsUpdateConstraints(): boolean;

View File

@ -0,0 +1,50 @@
package org.nativescript.widgets;
import android.content.Context;
import android.graphics.Canvas;
import android.text.TextPaint;
/**
* @author NathanWalker
*/
public class StyleableTextView extends androidx.appcompat.widget.AppCompatTextView {
int mTextStrokeWidth = 0;
int mTextStrokeColor = 0;
int mTextColor = 0;
public StyleableTextView(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
if (mTextStrokeWidth > 0) {
_applyStroke(canvas);
} else {
super.onDraw(canvas);
}
}
public void setTextStroke(int width, int color, int textColor) {
mTextStrokeWidth = width;
mTextStrokeColor = color;
mTextColor = textColor;
}
private void _applyStroke(Canvas canvas) {
// set paint to fill mode
TextPaint p = this.getPaint();
p.setStyle(TextPaint.Style.FILL);
// draw the fill part of text
super.onDraw(canvas);
// stroke color and width
p.setStyle(TextPaint.Style.STROKE);
p.setStrokeWidth(mTextStrokeWidth);
this.setTextColor(mTextStrokeColor);
// draw stroke
super.onDraw(canvas);
// draw original text color fill back (fallback to white)
p.setStyle(TextPaint.Style.FILL);
this.setTextColor(mTextColor != 0 ? mTextColor : 0xffffffff);
}
}