From dbb5684153ddeb8663623869b7a1f6f060900fb8 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 26 Mar 2019 18:44:10 -0300 Subject: [PATCH 1/5] 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 7cc9258ee1..399d040a37 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 55620609d6..d65e085dd4 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 fb47d8c89d..f23ca7544d 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) { From 6ee8fc0ce7b704653134fb21cb8e00a5bde148b9 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 26 Mar 2019 18:58:59 -0300 Subject: [PATCH 2/5] test: clickable-span test page --- apps/app/ui-tests-app/css/clickable-span.ts | 9 +++++++++ apps/app/ui-tests-app/css/clickable-span.xml | 17 +++++++++++++++++ apps/app/ui-tests-app/css/main-page.ts | 1 + 3 files changed, 27 insertions(+) create mode 100644 apps/app/ui-tests-app/css/clickable-span.ts create mode 100644 apps/app/ui-tests-app/css/clickable-span.xml diff --git a/apps/app/ui-tests-app/css/clickable-span.ts b/apps/app/ui-tests-app/css/clickable-span.ts new file mode 100644 index 0000000000..bb77c8b605 --- /dev/null +++ b/apps/app/ui-tests-app/css/clickable-span.ts @@ -0,0 +1,9 @@ +import { EventData, TextBase } from "tns-core-modules/ui/text-base"; + +export function foxTap(args: EventData) { + console.log("foxTap"); +} + +export function dogTap(args: EventData) { + console.log("dogTap"); +} diff --git a/apps/app/ui-tests-app/css/clickable-span.xml b/apps/app/ui-tests-app/css/clickable-span.xml new file mode 100644 index 0000000000..6d90f09058 --- /dev/null +++ b/apps/app/ui-tests-app/css/clickable-span.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/apps/app/ui-tests-app/css/main-page.ts b/apps/app/ui-tests-app/css/main-page.ts index 464da8189e..b43b013258 100644 --- a/apps/app/ui-tests-app/css/main-page.ts +++ b/apps/app/ui-tests-app/css/main-page.ts @@ -44,5 +44,6 @@ export function loadExamples() { examples.set("background-shorthand", "css/background-shorthand"); examples.set("background-image-linear-gradient", "css/background-image-linear-gradient"); examples.set("background-image", "css/background-image"); + examples.set("clickable-span", "css/clickable-span"); return examples; } \ No newline at end of file From d8b1c4ca45b76aea0aac867821a15523276e7993 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Tue, 26 Mar 2019 19:22:21 -0300 Subject: [PATCH 3/5] remove console.log --- tns-core-modules/text/span.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tns-core-modules/text/span.ts b/tns-core-modules/text/span.ts index d65e085dd4..aeaf1427ff 100644 --- a/tns-core-modules/text/span.ts +++ b/tns-core-modules/text/span.ts @@ -75,7 +75,6 @@ export class Span extends ViewBase implements SpanDefinition { } addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) { - console.log(arg); super.addEventListener(arg, callback, thisArg); this._setClickable(this.hasListeners(Span.linkClickEvent)); } From af0d1402962c02b87ab52b5f622cab1bed99104c Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 29 Mar 2019 12:07:42 -0300 Subject: [PATCH 4/5] use _emit instead of notify --- tns-core-modules/ui/text-base/text-base.android.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f23ca7544d..9be79f0e7f 100644 --- a/tns-core-modules/ui/text-base/text-base.android.ts +++ b/tns-core-modules/ui/text-base/text-base.android.ts @@ -70,7 +70,7 @@ function initializeClickableSpan(): void { onClick(view: android.view.View): void { const owner = this.owner.get(); if (owner) { - owner.notify({ eventName: Span.linkClickEvent, object: owner }); + owner._emit(Span.linkClickEvent); } view.clearFocus(); view.invalidate(); From 8fc42ff917d3cb04cec836ea7747a496475b2a23 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 6 May 2019 20:20:03 -0300 Subject: [PATCH 5/5] rename clickable to tappable in Span --- apps/app/ui-tests-app/css/main-page.ts | 2 +- .../{clickable-span.ts => tappable-span.ts} | 0 .../{clickable-span.xml => tappable-span.xml} | 4 ++-- tns-core-modules/text/span.d.ts | 8 +++---- tns-core-modules/text/span.ts | 20 ++++++++-------- .../ui/text-base/text-base.android.ts | 24 +++++++++---------- 6 files changed, 29 insertions(+), 29 deletions(-) rename apps/app/ui-tests-app/css/{clickable-span.ts => tappable-span.ts} (100%) rename apps/app/ui-tests-app/css/{clickable-span.xml => tappable-span.xml} (92%) diff --git a/apps/app/ui-tests-app/css/main-page.ts b/apps/app/ui-tests-app/css/main-page.ts index b43b013258..617bab3d84 100644 --- a/apps/app/ui-tests-app/css/main-page.ts +++ b/apps/app/ui-tests-app/css/main-page.ts @@ -44,6 +44,6 @@ export function loadExamples() { examples.set("background-shorthand", "css/background-shorthand"); examples.set("background-image-linear-gradient", "css/background-image-linear-gradient"); examples.set("background-image", "css/background-image"); - examples.set("clickable-span", "css/clickable-span"); + examples.set("tappable-span", "css/tappable-span"); return examples; } \ No newline at end of file diff --git a/apps/app/ui-tests-app/css/clickable-span.ts b/apps/app/ui-tests-app/css/tappable-span.ts similarity index 100% rename from apps/app/ui-tests-app/css/clickable-span.ts rename to apps/app/ui-tests-app/css/tappable-span.ts diff --git a/apps/app/ui-tests-app/css/clickable-span.xml b/apps/app/ui-tests-app/css/tappable-span.xml similarity index 92% rename from apps/app/ui-tests-app/css/clickable-span.xml rename to apps/app/ui-tests-app/css/tappable-span.xml index 6d90f09058..0730daf9ad 100644 --- a/apps/app/ui-tests-app/css/clickable-span.xml +++ b/apps/app/ui-tests-app/css/tappable-span.xml @@ -5,9 +5,9 @@ - + - + diff --git a/tns-core-modules/text/span.d.ts b/tns-core-modules/text/span.d.ts index 399d040a37..9b2855e6ff 100644 --- a/tns-core-modules/text/span.d.ts +++ b/tns-core-modules/text/span.d.ts @@ -52,14 +52,14 @@ export class Span extends ViewBase { public text: string; /** - * String value used when hooking to linkClicked event. + * String value used when hooking to linkTap event. */ - public static linkClickedEvent: string; + public static linkTapEvent: string; /** - * Gets if the span is clickable or not. + * Gets if the span is tappable or not. */ - public readonly clickable: boolean; + public readonly tappable: boolean; //@private /** diff --git a/tns-core-modules/text/span.ts b/tns-core-modules/text/span.ts index aeaf1427ff..b184683c68 100644 --- a/tns-core-modules/text/span.ts +++ b/tns-core-modules/text/span.ts @@ -5,9 +5,9 @@ import { FontStyle, FontWeight, } from "../ui/styling/font"; import { TextDecoration, EventData } from "../ui/text-base"; export class Span extends ViewBase implements SpanDefinition { - static linkClickEvent = "linkClick"; + static linkTapEvent = "linkTap"; private _text: string; - private _clickable: boolean = false; + private _tappable: boolean = false; get fontFamily(): string { return this.style.fontFamily; @@ -70,28 +70,28 @@ export class Span extends ViewBase implements SpanDefinition { } } - get clickable(): boolean { - return this._clickable; + get tappable(): boolean { + return this._tappable; } addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) { super.addEventListener(arg, callback, thisArg); - this._setClickable(this.hasListeners(Span.linkClickEvent)); + this._setTappable(this.hasListeners(Span.linkTapEvent)); } removeEventListener(arg: string, callback?: any, thisArg?: any) { super.removeEventListener(arg, callback, thisArg); - this._setClickable(this.hasListeners(Span.linkClickEvent)); + this._setTappable(this.hasListeners(Span.linkTapEvent)); } _setTextInternal(value: string): void { this._text = value; } - private _setClickable(value: boolean): void { - if (this._clickable !== value) { - this._clickable = value; - this.notifyPropertyChange("clickable", value); + private _setTappable(value: boolean): void { + if (this._tappable !== value) { + this._tappable = value; + this.notifyPropertyChange("tappable", 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 37db6e22f7..9008a71760 100644 --- a/tns-core-modules/ui/text-base/text-base.android.ts +++ b/tns-core-modules/ui/text-base/text-base.android.ts @@ -72,7 +72,7 @@ function initializeClickableSpan(): void { onClick(view: android.view.View): void { const owner = this.owner.get(); if (owner) { - owner._emit(Span.linkClickEvent); + owner._emit(Span.linkTapEvent); } view.clearFocus(); view.invalidate(); @@ -94,7 +94,7 @@ export class TextBase extends TextBaseCommon { private _maxHeight: number; private _minLines: number; private _maxLines: number; - private _clickable: boolean = false; + private _tappable: boolean = false; private _defaultMovementMethod: android.text.method.MovementMethod; public initNativeView(): void { @@ -148,7 +148,7 @@ export class TextBase extends TextBaseCommon { if (!reset && this.formattedText) { return; } - this._setClickableState(false); + this._setTappableState(false); this._setNativeText(reset); } @@ -169,7 +169,7 @@ export class TextBase extends TextBaseCommon { const spannableStringBuilder = createSpannableStringBuilder(value); nativeView.setText(spannableStringBuilder); - this._setClickableState(isStringClickable(value)); + this._setTappableState(isStringTappable(value)); textProperty.nativeValueChange(this, (value === null || value === undefined) ? "" : value.toString()); @@ -353,10 +353,10 @@ export class TextBase extends TextBaseCommon { this.nativeTextViewProtected.setText(transformedText); } - _setClickableState(clickable: boolean) { - if (this._clickable !== clickable) { - this._clickable = clickable; - if (this._clickable) { + _setTappableState(tappable: boolean) { + if (this._tappable !== tappable) { + this._tappable = tappable; + if (this._tappable) { this.nativeViewProtected.setMovementMethod(android.text.method.LinkMovementMethod.getInstance()); this.nativeViewProtected.setHighlightColor(null); } @@ -396,13 +396,13 @@ export function getTransformedText(text: string, textTransform: TextTransform): } } -function isStringClickable(formattedString: FormattedString) { +function isStringTappable(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) { + if (span.tappable) { return true; } } @@ -507,8 +507,8 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span, } } - const clickable = span.clickable; - if (clickable) { + const tappable = span.tappable; + if (tappable) { initializeClickableSpan(); ssb.setSpan(new ClickableSpan(span), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); }