mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat: TappableSpan support (#8256)
* feat(android): clickable span Initial support for clickable span on Android * test: clickable-span test page * remove console.log * use _emit instead of notify * rename clickable to tappable in Span * updated NativeScript.api.md * chore: fixing tslint errors * chore: fixed witespacing * moved and improved test page * feat: tappable span iOS implementation Co-authored-by: Eduardo Speroni <edusperoni@gmail.com>
This commit is contained in:
@@ -1936,9 +1936,13 @@ export class Span extends ViewBase {
|
||||
|
||||
public fontWeight: FontWeight;
|
||||
|
||||
public static linkTapEvent: string;
|
||||
|
||||
// (undocumented)
|
||||
_setTextInternal(value: string): void;
|
||||
|
||||
public readonly tappable: boolean;
|
||||
|
||||
public text: string;
|
||||
|
||||
public textDecoration: TextDecoration;
|
||||
|
||||
@@ -18,6 +18,7 @@ export function loadExamples() {
|
||||
examples.set("issue-4287", "button/issue-4287-page");
|
||||
examples.set("issue-4385", "button/issue-4385-page");
|
||||
examples.set("highlight-4740", "button/highlight-4740/highlight-4740-page");
|
||||
examples.set("tappable-span", "button/tappable-span-page");
|
||||
|
||||
return examples;
|
||||
}
|
||||
|
||||
9
nativescript-core/ui/text-base/span.d.ts
vendored
9
nativescript-core/ui/text-base/span.d.ts
vendored
@@ -50,6 +50,15 @@ export class Span extends ViewBase {
|
||||
* Gets or sets the text for the span.
|
||||
*/
|
||||
public text: string;
|
||||
/**
|
||||
* String value used when hooking to linkTap event.
|
||||
*/
|
||||
public static linkTapEvent: string;
|
||||
|
||||
/**
|
||||
* Gets if the span is tappable or not.
|
||||
*/
|
||||
public readonly tappable: boolean;
|
||||
|
||||
//@private
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import { Span as SpanDefinition } from "./span";
|
||||
import { ViewBase } from "../core/view";
|
||||
import { FontStyle, FontWeight, } from "../styling/font";
|
||||
import { TextDecoration } from "../text-base";
|
||||
import { TextDecoration, EventData } from "../text-base";
|
||||
|
||||
export class Span extends ViewBase implements SpanDefinition {
|
||||
static linkTapEvent = "linkTap";
|
||||
private _text: string;
|
||||
private _tappable: boolean = false;
|
||||
|
||||
get fontFamily(): string {
|
||||
return this.style.fontFamily;
|
||||
@@ -68,7 +70,28 @@ export class Span extends ViewBase implements SpanDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
get tappable(): boolean {
|
||||
return this._tappable;
|
||||
}
|
||||
|
||||
addEventListener(arg: string, callback: (data: EventData) => void, thisArg?: any) {
|
||||
super.addEventListener(arg, callback, thisArg);
|
||||
this._setTappable(this.hasListeners(Span.linkTapEvent));
|
||||
}
|
||||
|
||||
removeEventListener(arg: string, callback?: any, thisArg?: any) {
|
||||
super.removeEventListener(arg, callback, thisArg);
|
||||
this._setTappable(this.hasListeners(Span.linkTapEvent));
|
||||
}
|
||||
|
||||
_setTextInternal(value: string): void {
|
||||
this._text = value;
|
||||
}
|
||||
|
||||
private _setTappable(value: boolean): void {
|
||||
if (this._tappable !== value) {
|
||||
this._tappable = value;
|
||||
this.notifyPropertyChange("tappable", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,42 @@ 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<Span>;
|
||||
|
||||
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._emit(Span.linkTapEvent);
|
||||
}
|
||||
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;
|
||||
@@ -60,12 +96,15 @@ export class TextBase extends TextBaseCommon {
|
||||
private _maxHeight: number;
|
||||
private _minLines: number;
|
||||
private _maxLines: number;
|
||||
private _tappable: 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();
|
||||
@@ -112,6 +151,8 @@ export class TextBase extends TextBaseCommon {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setTappableState(false);
|
||||
|
||||
this._setNativeText(reset);
|
||||
}
|
||||
|
||||
@@ -131,6 +172,7 @@ export class TextBase extends TextBaseCommon {
|
||||
|
||||
const spannableStringBuilder = createSpannableStringBuilder(value);
|
||||
nativeView.setText(<any>spannableStringBuilder);
|
||||
this._setTappableState(isStringTappable(value));
|
||||
|
||||
textProperty.nativeValueChange(this, (value === null || value === undefined) ? "" : value.toString());
|
||||
|
||||
@@ -315,6 +357,19 @@ export class TextBase extends TextBaseCommon {
|
||||
|
||||
this.nativeTextViewProtected.setText(<any>transformedText);
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
else {
|
||||
this.nativeViewProtected.setMovementMethod(this._defaultMovementMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCapitalizedString(str: string): string {
|
||||
@@ -346,6 +401,20 @@ export function getTransformedText(text: string, textTransform: TextTransform):
|
||||
}
|
||||
}
|
||||
|
||||
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.tappable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function createSpannableStringBuilder(formattedString: FormattedString): android.text.SpannableStringBuilder {
|
||||
if (!formattedString || !formattedString.parent) {
|
||||
return null;
|
||||
@@ -444,6 +513,12 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span,
|
||||
}
|
||||
}
|
||||
|
||||
const tappable = span.tappable;
|
||||
if (tappable) {
|
||||
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) {
|
||||
|
||||
@@ -15,10 +15,101 @@ export * from "./text-base-common";
|
||||
|
||||
const majorVersion = ios.MajorVersion;
|
||||
|
||||
class UILabelClickHandlerImpl extends NSObject {
|
||||
private _owner: WeakRef<TextBase>;
|
||||
|
||||
public static initWithOwner(owner: WeakRef<TextBase>): UILabelClickHandlerImpl {
|
||||
let handler = <UILabelClickHandlerImpl>UILabelClickHandlerImpl.new();
|
||||
handler._owner = owner;
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
public linkTap(tapGesture: UITapGestureRecognizer) {
|
||||
let owner = this._owner.get();
|
||||
if (owner) {
|
||||
// https://stackoverflow.com/a/35789589
|
||||
let label = <UILabel>owner.nativeTextViewProtected;
|
||||
let layoutManager = NSLayoutManager.alloc().init();
|
||||
let textContainer = NSTextContainer.alloc().initWithSize(CGSizeZero);
|
||||
let textStorage = NSTextStorage.alloc().initWithAttributedString(owner.nativeTextViewProtected["attributedText"]);
|
||||
|
||||
layoutManager.addTextContainer(textContainer);
|
||||
textStorage.addLayoutManager(layoutManager);
|
||||
|
||||
textContainer.lineFragmentPadding = 0;
|
||||
textContainer.lineBreakMode = label.lineBreakMode;
|
||||
textContainer.maximumNumberOfLines = label.numberOfLines;
|
||||
let labelSize = label.bounds.size;
|
||||
textContainer.size = labelSize;
|
||||
|
||||
let locationOfTouchInLabel = tapGesture.locationInView(label);
|
||||
let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer);
|
||||
|
||||
let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
|
||||
(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
|
||||
|
||||
let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
|
||||
locationOfTouchInLabel.y - textContainerOffset.y);
|
||||
|
||||
let indexOfCharacter = layoutManager.characterIndexForPointInTextContainerFractionOfDistanceBetweenInsertionPoints(
|
||||
locationOfTouchInTextContainer, textContainer, null);
|
||||
|
||||
let span: Span = null;
|
||||
// try to find the corresponding span using the spanRanges
|
||||
for (let i = 0; i < owner._spanRanges.length; i++) {
|
||||
let range = owner._spanRanges[i];
|
||||
if ((range.location <= indexOfCharacter) && (range.location + range.length) > indexOfCharacter) {
|
||||
if (owner.formattedText && owner.formattedText.spans.length > i) {
|
||||
span = owner.formattedText.spans.getItem(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (span && span.tappable) {
|
||||
// if the span is found and tappable emit the linkTap event
|
||||
span._emit(Span.linkTapEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjCExposedMethods = {
|
||||
"linkTap": { returns: interop.types.void, params: [interop.types.id] }
|
||||
};
|
||||
}
|
||||
|
||||
export class TextBase extends TextBaseCommon {
|
||||
|
||||
public nativeViewProtected: UITextField | UITextView | UILabel | UIButton;
|
||||
public nativeTextViewProtected: UITextField | UITextView | UILabel | UIButton;
|
||||
private _tappable: boolean = false;
|
||||
private _tapGestureRecognizer: UITapGestureRecognizer;
|
||||
public _spanRanges: NSRange[];
|
||||
|
||||
public initNativeView(): void {
|
||||
super.initNativeView();
|
||||
this._setTappableState(false);
|
||||
}
|
||||
|
||||
_setTappableState(tappable: boolean) {
|
||||
if (this._tappable !== tappable) {
|
||||
this._tappable = tappable;
|
||||
if (this._tappable) {
|
||||
const tapHandler = UILabelClickHandlerImpl.initWithOwner(new WeakRef(this));
|
||||
// associate handler with menuItem or it will get collected by JSC.
|
||||
(<any>this).handler = tapHandler;
|
||||
|
||||
this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(tapHandler, "linkTap");
|
||||
this.nativeViewProtected.userInteractionEnabled = true;
|
||||
this.nativeViewProtected.addGestureRecognizer(this._tapGestureRecognizer);
|
||||
}
|
||||
else {
|
||||
this.nativeViewProtected.userInteractionEnabled = false;
|
||||
this.nativeViewProtected.removeGestureRecognizer(this._tapGestureRecognizer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[textProperty.getDefault](): number | symbol {
|
||||
return resetSymbol;
|
||||
@@ -35,6 +126,7 @@ export class TextBase extends TextBaseCommon {
|
||||
|
||||
[formattedTextProperty.setNative](value: FormattedString) {
|
||||
this._setNativeText();
|
||||
this._setTappableState(isStringTappable(value));
|
||||
textProperty.nativeValueChange(this, !value ? "" : value.toString());
|
||||
this._requestLayoutOnTextChanged();
|
||||
}
|
||||
@@ -253,6 +345,7 @@ export class TextBase extends TextBaseCommon {
|
||||
|
||||
createNSMutableAttributedString(formattedString: FormattedString): NSMutableAttributedString {
|
||||
let mas = NSMutableAttributedString.alloc().init();
|
||||
this._spanRanges = [];
|
||||
if (formattedString && formattedString.parent) {
|
||||
for (let i = 0, spanStart = 0, length = formattedString.spans.length; i < length; i++) {
|
||||
const span = formattedString.spans.getItem(i);
|
||||
@@ -265,6 +358,7 @@ export class TextBase extends TextBaseCommon {
|
||||
|
||||
const nsAttributedString = this.createMutableStringForSpan(span, spanText);
|
||||
mas.insertAttributedStringAtIndex(nsAttributedString, spanStart);
|
||||
this._spanRanges.push({location: spanStart, length: spanText.length});
|
||||
spanStart += spanText.length;
|
||||
}
|
||||
}
|
||||
@@ -349,3 +443,17 @@ export function getTransformedText(text: string, textTransform: TextTransform):
|
||||
function NSStringFromNSAttributedString(source: NSAttributedString | string): NSString {
|
||||
return NSString.stringWithString(source instanceof NSAttributedString && source.string || <string>source);
|
||||
}
|
||||
|
||||
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.tappable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user