Skip to content

Commit 966dccd

Browse files
authored
feat(core): textbase span interaction and styling improvements (#10682)
- Added `linkTap` event support for other iOS views that nest spans - Prevent android span from setting parent background color to itself since it doesn't react to changes of that property. Unless background color is specified to the span directly, it's going to be transparent - Added few missing `nativeTextViewProtected` references - Improved view disposal for classes that inherit from TextBase as they had leftovers after android activity recreation - Removed 2 assignments of `userInteractionEnabled` from TextBase as they were unneeded and had conflicts with `isUserInteractionEnabled` property. Core already sets that property to true for the views that need it in `createNativeView` call - `HTMLView` will remove extra padding using the documented `UIEdgeInsetsZero`
1 parent 03cca58 commit 966dccd

File tree

10 files changed

+112
-63
lines changed

10 files changed

+112
-63
lines changed

packages/core/ui/editable-text-base/index.android.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,10 @@ function initializeEditTextListeners(): void {
8484
}
8585

8686
export abstract class EditableTextBase extends EditableTextBaseCommon {
87-
/* tslint:disable */
88-
_dirtyTextAccumulator: string;
89-
/* tslint:enable */
90-
9187
nativeViewProtected: android.widget.EditText;
9288
nativeTextViewProtected: android.widget.EditText;
89+
90+
private _dirtyTextAccumulator: string;
9391
private _keyListenerCache: android.text.method.KeyListener;
9492
private _inputType: number;
9593

@@ -120,12 +118,16 @@ export abstract class EditableTextBase extends EditableTextBaseCommon {
120118

121119
public disposeNativeView(): void {
122120
const editText = this.nativeTextViewProtected;
121+
123122
editText.removeTextChangedListener((<any>editText).listener);
124123
editText.setOnFocusChangeListener(null);
125124
editText.setOnEditorActionListener(null);
126125
(<any>editText).listener.owner = null;
127126
(<any>editText).listener = null;
128127
this._keyListenerCache = null;
128+
this._dirtyTextAccumulator = undefined;
129+
this._inputType = 0;
130+
129131
super.disposeNativeView();
130132
}
131133

packages/core/ui/html-view/index.ios.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class HtmlView extends HtmlViewBase {
2929

3030
// Remove extra padding
3131
this.nativeViewProtected.textContainer.lineFragmentPadding = 0;
32-
this.nativeViewProtected.textContainerInset = (UIEdgeInsets as any).zero;
32+
this.nativeViewProtected.textContainerInset = UIEdgeInsetsZero;
3333
}
3434

3535
// @ts-ignore

packages/core/ui/label/index.ios.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ enum FixedSize {
1919
@CSSType('Label')
2020
export class Label extends TextBase implements LabelDefinition {
2121
nativeViewProtected: TNSLabel;
22+
nativeTextViewProtected: TNSLabel;
23+
2224
private _fixedSize: FixedSize;
2325

2426
public createNativeView() {
@@ -28,6 +30,11 @@ export class Label extends TextBase implements LabelDefinition {
2830
return view;
2931
}
3032

33+
public disposeNativeView(): void {
34+
super.disposeNativeView();
35+
this._fixedSize = null;
36+
}
37+
3138
// @ts-ignore
3239
get ios(): TNSLabel {
3340
return this.nativeTextViewProtected;
@@ -102,7 +109,7 @@ export class Label extends TextBase implements LabelDefinition {
102109
}
103110

104111
private _measureNativeView(width: number, widthMode: number, height: number, heightMode: number): { width: number; height: number } {
105-
const view = <UILabel>this.nativeTextViewProtected;
112+
const view = this.nativeTextViewProtected;
106113

107114
const nativeSize = view.textRectForBoundsLimitedToNumberOfLines(CGRectMake(0, 0, widthMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : layout.toDeviceIndependentPixels(width), heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : layout.toDeviceIndependentPixels(height)), view.numberOfLines).size;
108115

@@ -123,7 +130,8 @@ export class Label extends TextBase implements LabelDefinition {
123130
private adjustLineBreak() {
124131
const whiteSpace = this.whiteSpace;
125132
const textOverflow = this.textOverflow;
126-
const nativeView = this.nativeViewProtected;
133+
const nativeView = this.nativeTextViewProtected;
134+
127135
switch (whiteSpace) {
128136
case 'normal':
129137
nativeView.lineBreakMode = NSLineBreakMode.ByWordWrapping;
@@ -158,7 +166,7 @@ export class Label extends TextBase implements LabelDefinition {
158166
const cgColor = color ? color.CGColor : null;
159167
nativeView.layer.backgroundColor = cgColor;
160168
},
161-
true
169+
true,
162170
);
163171
}
164172
}

packages/core/ui/scroll-view/index.android.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export class ScrollView extends ScrollViewBase {
99
nativeViewProtected: org.nativescript.widgets.VerticalScrollView | org.nativescript.widgets.HorizontalScrollView;
1010
private _androidViewId = -1;
1111
private handler: android.view.ViewTreeObserver.OnScrollChangedListener;
12-
private scrollChangeHandler: androidx.core.widget.NestedScrollView.OnScrollChangeListener;
1312

1413
get horizontalOffset(): number {
1514
const nativeView = this.nativeViewProtected;

packages/core/ui/search-bar/index.ios.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class UISearchBarImpl extends UISearchBar {
7373
export class SearchBar extends SearchBarBase {
7474
nativeViewProtected: UISearchBar;
7575
private _delegate;
76-
private __textField: UITextField;
76+
private _textField: UITextField;
7777

7878
createNativeView() {
7979
return UISearchBarImpl.new();
@@ -87,33 +87,34 @@ export class SearchBar extends SearchBarBase {
8787

8888
disposeNativeView() {
8989
this._delegate = null;
90+
this._textField = null;
9091
super.disposeNativeView();
9192
}
9293

9394
public dismissSoftInput() {
9495
(<UIResponder>this.ios).resignFirstResponder();
9596
}
9697

98+
private _getTextField(): UITextField {
99+
if (!this._textField) {
100+
this._textField = this.ios.valueForKey('searchField');
101+
}
102+
103+
return this._textField;
104+
}
105+
97106
// @ts-ignore
98107
get ios(): UISearchBar {
99108
return this.nativeViewProtected;
100109
}
101110

102-
get _textField(): UITextField {
103-
if (!this.__textField) {
104-
this.__textField = this.ios.valueForKey('searchField');
105-
}
106-
107-
return this.__textField;
108-
}
109-
110111
[isEnabledProperty.setNative](value: boolean) {
111112
const nativeView = this.nativeViewProtected;
112113
if (nativeView instanceof UIControl) {
113114
nativeView.enabled = value;
114115
}
115116

116-
const textField = this._textField;
117+
const textField = this._getTextField();
117118
if (textField) {
118119
textField.enabled = value;
119120
}
@@ -128,15 +129,15 @@ export class SearchBar extends SearchBarBase {
128129
}
129130

130131
[colorProperty.getDefault](): UIColor {
131-
const sf = this._textField;
132+
const sf = this._getTextField();
132133
if (sf) {
133134
return sf.textColor;
134135
}
135136

136137
return null;
137138
}
138139
[colorProperty.setNative](value: UIColor | Color) {
139-
const sf = this._textField;
140+
const sf = this._getTextField();
140141
const color = value instanceof Color ? value.ios : value;
141142
if (sf) {
142143
sf.textColor = color;
@@ -145,12 +146,12 @@ export class SearchBar extends SearchBarBase {
145146
}
146147

147148
[fontInternalProperty.getDefault](): UIFont {
148-
const sf = this._textField;
149+
const sf = this._getTextField();
149150

150151
return sf ? sf.font : null;
151152
}
152153
[fontInternalProperty.setNative](value: UIFont | Font) {
153-
const sf = this._textField;
154+
const sf = this._getTextField();
154155
if (sf) {
155156
sf.font = value instanceof Font ? value.getUIFont(sf.font) : value;
156157
}
@@ -179,7 +180,7 @@ export class SearchBar extends SearchBarBase {
179180
}
180181

181182
[textFieldBackgroundColorProperty.getDefault](): UIColor {
182-
const textField = this._textField;
183+
const textField = this._getTextField();
183184
if (textField) {
184185
return textField.backgroundColor;
185186
}
@@ -188,7 +189,7 @@ export class SearchBar extends SearchBarBase {
188189
}
189190
[textFieldBackgroundColorProperty.setNative](value: Color | UIColor) {
190191
const color = value instanceof Color ? value.ios : value;
191-
const textField = this._textField;
192+
const textField = this._getTextField();
192193
if (textField) {
193194
textField.backgroundColor = color;
194195
}
@@ -219,6 +220,6 @@ export class SearchBar extends SearchBarBase {
219220
attributes[NSForegroundColorAttributeName] = this.textFieldHintColor.ios;
220221
}
221222
const attributedPlaceholder = NSAttributedString.alloc().initWithStringAttributes(stringValue, attributes);
222-
this._textField.attributedPlaceholder = attributedPlaceholder;
223+
this._getTextField().attributedPlaceholder = attributedPlaceholder;
223224
}
224225
}

packages/core/ui/text-base/index.android.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export * from './text-base-common';
2323
let TextTransformation: TextTransformation;
2424

2525
export interface TextTransformation {
26-
new (owner: TextBase): any /* android.text.method.TransformationMethod */;
26+
new (owner: TextBase): android.text.method.TransformationMethod;
2727
}
2828

2929
function initializeTextTransformation(): void {
@@ -169,9 +169,8 @@ function initializeBaselineAdjustedSpan(): void {
169169
}
170170

171171
export class TextBase extends TextBaseCommon {
172-
nativeViewProtected: org.nativescript.widgets.StyleableTextView;
173-
// @ts-ignore
174-
nativeTextViewProtected: org.nativescript.widgets.StyleableTextView;
172+
public nativeViewProtected: org.nativescript.widgets.StyleableTextView;
173+
175174
private _defaultTransformationMethod: android.text.method.TransformationMethod;
176175
private _paintFlags: number;
177176
private _minHeight: number;
@@ -181,6 +180,10 @@ export class TextBase extends TextBaseCommon {
181180
private _tappable = false;
182181
private _defaultMovementMethod: android.text.method.MovementMethod;
183182

183+
get nativeTextViewProtected(): org.nativescript.widgets.StyleableTextView {
184+
return super.nativeTextViewProtected;
185+
}
186+
184187
public initNativeView(): void {
185188
super.initNativeView();
186189
initializeTextTransformation();
@@ -193,6 +196,19 @@ export class TextBase extends TextBaseCommon {
193196
this._maxLines = nativeView.getMaxLines();
194197
}
195198

199+
public disposeNativeView(): void {
200+
super.disposeNativeView();
201+
202+
this._tappable = false;
203+
this._defaultTransformationMethod = null;
204+
this._defaultMovementMethod = null;
205+
this._paintFlags = 0;
206+
this._minHeight = 0;
207+
this._maxHeight = 0;
208+
this._minLines = 0;
209+
this._maxLines = 0;
210+
}
211+
196212
public resetNativeView(): void {
197213
super.resetNativeView();
198214
const nativeView = this.nativeTextViewProtected;
@@ -502,13 +518,13 @@ export class TextBase extends TextBaseCommon {
502518
}
503519

504520
if (this.style?.textStroke) {
505-
this.nativeViewProtected.setTextStroke(Length.toDevicePixels(this.style.textStroke.width), this.style.textStroke.color.android, this.style.color.android);
506-
} else if (this.nativeViewProtected.setTextStroke) {
521+
this.nativeTextViewProtected.setTextStroke(Length.toDevicePixels(this.style.textStroke.width), this.style.textStroke.color.android, this.style.color.android);
522+
} else if (this.nativeTextViewProtected.setTextStroke) {
507523
// reset
508-
this.nativeViewProtected.setTextStroke(0, 0, 0);
524+
this.nativeTextViewProtected.setTextStroke(0, 0, 0);
509525
}
510526

511-
this.nativeTextViewProtected.setText(<any>transformedText);
527+
this.nativeTextViewProtected.setText(transformedText);
512528
}
513529

514530
_setTappableState(tappable: boolean) {
@@ -608,10 +624,8 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span,
608624
ssb.setSpan(new android.text.style.ForegroundColorSpan(color.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
609625
}
610626

611-
const backgroundColor: Color = getClosestPropertyValue(<any>backgroundColorProperty, span);
612-
613-
if (backgroundColor) {
614-
ssb.setSpan(new android.text.style.BackgroundColorSpan(backgroundColor.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
627+
if (spanStyle.backgroundColor) {
628+
ssb.setSpan(new android.text.style.BackgroundColorSpan(spanStyle.backgroundColor.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
615629
}
616630

617631
const textDecoration: CoreTypes.TextDecorationType = getClosestPropertyValue(textDecorationProperty, span);

packages/core/ui/text-base/index.ios.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class UILabelClickHandlerImpl extends NSObject {
3434
public linkTap(tapGesture: UITapGestureRecognizer) {
3535
const owner = this._owner?.deref();
3636
if (owner) {
37-
const label = <UILabel>owner.nativeTextViewProtected;
37+
const nativeView = owner.nativeTextViewProtected instanceof UIButton ? owner.nativeTextViewProtected.titleLabel : owner.nativeTextViewProtected;
3838

3939
// This offset along with setting paragraph style alignment will achieve perfect horizontal alignment for NSTextContainer
4040
let offsetXMultiplier: number;
@@ -53,18 +53,27 @@ class UILabelClickHandlerImpl extends NSObject {
5353

5454
const layoutManager = NSLayoutManager.alloc().init();
5555
const textContainer = NSTextContainer.alloc().initWithSize(CGSizeZero);
56-
const textStorage = NSTextStorage.alloc().initWithAttributedString(owner.nativeTextViewProtected['attributedText']);
56+
const textStorage = NSTextStorage.alloc().initWithAttributedString(nativeView.attributedText);
5757

5858
layoutManager.addTextContainer(textContainer);
5959
textStorage.addLayoutManager(layoutManager);
6060

6161
textContainer.lineFragmentPadding = 0;
62-
textContainer.lineBreakMode = label.lineBreakMode;
63-
textContainer.maximumNumberOfLines = label.numberOfLines;
64-
const labelSize = label.bounds.size;
62+
63+
if (nativeView instanceof UITextView) {
64+
textContainer.lineBreakMode = nativeView.textContainer.lineBreakMode;
65+
textContainer.maximumNumberOfLines = nativeView.textContainer.maximumNumberOfLines;
66+
} else {
67+
if (!(nativeView instanceof UITextField)) {
68+
textContainer.lineBreakMode = nativeView.lineBreakMode;
69+
textContainer.maximumNumberOfLines = nativeView.numberOfLines;
70+
}
71+
}
72+
73+
const labelSize = nativeView.bounds.size;
6574
textContainer.size = labelSize;
6675

67-
const locationOfTouchInLabel = tapGesture.locationInView(label);
76+
const locationOfTouchInLabel = tapGesture.locationInView(nativeView);
6877
const textBoundingBox = layoutManager.usedRectForTextContainer(textContainer);
6978

7079
const textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * offsetXMultiplier - textBoundingBox.origin.x, (labelSize.height - textBoundingBox.size.height) * offsetYMultiplier - textBoundingBox.origin.y);
@@ -117,31 +126,46 @@ class UILabelClickHandlerImpl extends NSObject {
117126

118127
export class TextBase extends TextBaseCommon {
119128
public nativeViewProtected: UITextField | UITextView | UILabel | UIButton;
120-
// @ts-ignore
121-
public nativeTextViewProtected: UITextField | UITextView | UILabel | UIButton;
129+
public _spanRanges: NSRange[];
130+
122131
private _tappable = false;
132+
private _linkTapHandler: UILabelClickHandlerImpl;
123133
private _tapGestureRecognizer: UITapGestureRecognizer;
124-
public _spanRanges: NSRange[];
134+
135+
get nativeTextViewProtected(): UITextField | UITextView | UILabel | UIButton {
136+
return super.nativeTextViewProtected;
137+
}
125138

126139
public initNativeView(): void {
127140
super.initNativeView();
128141
this._setTappableState(false);
129142
}
130143

144+
public disposeNativeView(): void {
145+
super.disposeNativeView();
146+
147+
this._tappable = false;
148+
this._linkTapHandler = null;
149+
this._tapGestureRecognizer = null;
150+
}
151+
131152
_setTappableState(tappable: boolean) {
132153
if (this._tappable !== tappable) {
154+
const nativeTextView = this.nativeTextViewProtected;
155+
133156
this._tappable = tappable;
134157
if (this._tappable) {
135158
const tapHandler = UILabelClickHandlerImpl.initWithOwner(new WeakRef(this));
136-
// associate handler with menuItem or it will get collected by JSC.
137-
(<any>this).handler = tapHandler;
159+
// Associate handler with menuItem or it will get collected by JSC
160+
this._linkTapHandler = tapHandler;
138161

139-
this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(tapHandler, 'linkTap');
140-
this.nativeViewProtected.userInteractionEnabled = true;
141-
this.nativeViewProtected.addGestureRecognizer(this._tapGestureRecognizer);
162+
this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(this._linkTapHandler, 'linkTap');
163+
nativeTextView.addGestureRecognizer(this._tapGestureRecognizer);
142164
} else {
143-
this.nativeViewProtected.userInteractionEnabled = false;
144-
this.nativeViewProtected.removeGestureRecognizer(this._tapGestureRecognizer);
165+
nativeTextView.removeGestureRecognizer(this._tapGestureRecognizer);
166+
167+
this._linkTapHandler = null;
168+
this._tapGestureRecognizer = null;
145169
}
146170
}
147171
}

packages/core/ui/text-base/text-base-common.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { Style } from '../styling/style';
1212
import { Observable } from '../../data/observable';
1313
import { CoreTypes } from '../../core-types';
1414
import { TextBase as TextBaseDefinition } from '.';
15-
import { Color } from '../../color';
1615
import { ShadowCSSValues, parseCSSShadow } from '../styling/css-shadow';
1716
import { StrokeCSSValues, parseCSSStroke } from '../styling/css-stroke';
1817

0 commit comments

Comments
 (0)