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

@ -1,4 +1,4 @@
import { Page, Observable, EventData } from '@nativescript/core';
import { Page, Observable, EventData, Label, Color } from '@nativescript/core';
let page: Page;
@ -7,4 +7,23 @@ export function navigatingTo(args: EventData) {
page.bindingContext = new SampleData();
}
export class SampleData extends Observable {}
export class SampleData extends Observable {
strokeLabel: Label;
loadedStrokeLabel(args) {
this.strokeLabel = args.object;
}
toggleStrokeStyle() {
if (this.strokeLabel.style.textStroke) {
this.strokeLabel.style.color = new Color('black');
this.strokeLabel.style.textStroke = null;
} else {
this.strokeLabel.style.color = new Color('white');
this.strokeLabel.style.textStroke = {
color: new Color('black'),
width: { value: 2, unit: 'px' },
};
}
}
}

View File

@ -25,6 +25,9 @@
<GridLayout marginTop="10" borderWidth="1" borderColor="#efefef" height="60" paddingLeft="5">
<Button text="Test Button text-overflow: initial, this should be long sentence and truncated in the middle with ellipsis." textOverflow="initial" whiteSpace="nowrap" />
</GridLayout>
<GridLayout marginTop="10" height="60" paddingLeft="5" tap="{{toggleStrokeStyle}}">
<Label text=" text-stroke" style="text-stroke: 2px black; color: #fff; font-size: 35; font-weight: bold; font-family:Arial, Helvetica, sans-serif" loaded="{{loadedStrokeLabel}}"/>
</GridLayout>
<Label text="maxLines 2" fontWeight="bold" marginTop="10" />
<Label
text="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."

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