From dbb5684153ddeb8663623869b7a1f6f060900fb8 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 26 Mar 2019 18:44:10 -0300 Subject: [PATCH] feat(android): clickable span Initial support for clickable span on Android --- tns-core-modules/text/span.d.ts | 10 +++ tns-core-modules/text/span.ts | 26 ++++++- .../ui/text-base/text-base.android.ts | 72 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/tns-core-modules/text/span.d.ts b/tns-core-modules/text/span.d.ts index 7cc9258ee..399d040a3 100644 --- a/tns-core-modules/text/span.d.ts +++ b/tns-core-modules/text/span.d.ts @@ -50,6 +50,16 @@ export class Span extends ViewBase { * Gets or sets the text for the span. */ public text: string; + + /** + * String value used when hooking to linkClicked event. + */ + public static linkClickedEvent: string; + + /** + * Gets if the span is clickable or not. + */ + public readonly clickable: boolean; //@private /** diff --git a/tns-core-modules/text/span.ts b/tns-core-modules/text/span.ts index 55620609d..d65e085dd 100644 --- a/tns-core-modules/text/span.ts +++ b/tns-core-modules/text/span.ts @@ -2,10 +2,12 @@ import { Span as SpanDefinition } from "./span"; import { ViewBase } from "../ui/core/view"; import { FontStyle, FontWeight, } from "../ui/styling/font"; -import { TextDecoration } from "../ui/text-base"; +import { TextDecoration, EventData } from "../ui/text-base"; export class Span extends ViewBase implements SpanDefinition { + static linkClickEvent = "linkClick"; private _text: string; + private _clickable: boolean = false; get fontFamily(): string { return this.style.fontFamily; @@ -68,7 +70,29 @@ export class Span extends ViewBase implements SpanDefinition { } } + get clickable(): boolean { + return this._clickable; + } + + addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) { + console.log(arg); + super.addEventListener(arg, callback, thisArg); + this._setClickable(this.hasListeners(Span.linkClickEvent)); + } + + removeEventListener(arg: string, callback?: any, thisArg?: any) { + super.removeEventListener(arg, callback, thisArg); + this._setClickable(this.hasListeners(Span.linkClickEvent)); + } + _setTextInternal(value: string): void { this._text = value; } + + private _setClickable(value: boolean): void { + if (this._clickable !== value) { + this._clickable = value; + this.notifyPropertyChange("clickable", value); + } + } } diff --git a/tns-core-modules/ui/text-base/text-base.android.ts b/tns-core-modules/ui/text-base/text-base.android.ts index fb47d8c89..f23ca7544 100644 --- a/tns-core-modules/ui/text-base/text-base.android.ts +++ b/tns-core-modules/ui/text-base/text-base.android.ts @@ -48,6 +48,41 @@ function initializeTextTransformation(): void { TextTransformation = TextTransformationImpl; } +interface ClickableSpan { + new (owner: Span): android.text.style.ClickableSpan; +} + +let ClickableSpan: ClickableSpan; + +function initializeClickableSpan(): void { + if (ClickableSpan) { + return; + } + + class ClickableSpanImpl extends android.text.style.ClickableSpan { + owner: WeakRef; + + constructor(owner: Span) { + super(); + this.owner = new WeakRef(owner); + return global.__native(this); + } + onClick(view: android.view.View): void { + const owner = this.owner.get(); + if (owner) { + owner.notify({ eventName: Span.linkClickEvent, object: owner }); + } + view.clearFocus(); + view.invalidate(); + } + updateDrawState(tp: android.text.TextPaint): void { + // don't style as link + } + } + + ClickableSpan = ClickableSpanImpl; +} + export class TextBase extends TextBaseCommon { nativeViewProtected: android.widget.TextView; nativeTextViewProtected: android.widget.TextView; @@ -57,12 +92,15 @@ export class TextBase extends TextBaseCommon { private _maxHeight: number; private _minLines: number; private _maxLines: number; + private _clickable: boolean = false; + private _defaultMovementMethod: android.text.method.MovementMethod; public initNativeView(): void { super.initNativeView(); initializeTextTransformation(); const nativeView = this.nativeTextViewProtected; this._defaultTransformationMethod = nativeView.getTransformationMethod(); + this._defaultMovementMethod = this.nativeView.getMovementMethod(); this._minHeight = nativeView.getMinHeight(); this._maxHeight = nativeView.getMaxHeight(); this._minLines = nativeView.getMinLines(); @@ -108,6 +146,7 @@ export class TextBase extends TextBaseCommon { if (!reset && this.formattedText) { return; } + this._setClickableState(false); this._setNativeText(reset); } @@ -128,6 +167,7 @@ export class TextBase extends TextBaseCommon { const spannableStringBuilder = createSpannableStringBuilder(value); nativeView.setText(spannableStringBuilder); + this._setClickableState(isStringClickable(value)); textProperty.nativeValueChange(this, (value === null || value === undefined) ? "" : value.toString()); @@ -310,6 +350,19 @@ export class TextBase extends TextBaseCommon { this.nativeTextViewProtected.setText(transformedText); } + + _setClickableState(clickable: boolean) { + if (this._clickable !== clickable) { + this._clickable = clickable; + if (this._clickable) { + this.nativeViewProtected.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); + this.nativeViewProtected.setHighlightColor(null); + } + else { + this.nativeViewProtected.setMovementMethod(this._defaultMovementMethod); + } + } + } } function getCapitalizedString(str: string): string { @@ -341,6 +394,19 @@ export function getTransformedText(text: string, textTransform: TextTransform): } } +function isStringClickable(formattedString: FormattedString) { + if (!formattedString) { + return false; + } + for (let i = 0, length = formattedString.spans.length; i < length; i++) { + const span = formattedString.spans.getItem(i); + if (span.clickable) { + return true; + } + } + return false; +} + function createSpannableStringBuilder(formattedString: FormattedString): android.text.SpannableStringBuilder { if (!formattedString) { return null; @@ -439,6 +505,12 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, } } + const clickable = span.clickable; + if (clickable) { + initializeClickableSpan(); + ssb.setSpan(new ClickableSpan(span), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + // TODO: Implement letterSpacing for Span here. // const letterSpacing = formattedString.parent.style.letterSpacing; // if (letterSpacing > 0) {