mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-06 17:28:29 +08:00
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`
This commit is contained in:

committed by
GitHub

parent
03cca58712
commit
966dccd0f9
@ -84,12 +84,10 @@ function initializeEditTextListeners(): void {
|
||||
}
|
||||
|
||||
export abstract class EditableTextBase extends EditableTextBaseCommon {
|
||||
/* tslint:disable */
|
||||
_dirtyTextAccumulator: string;
|
||||
/* tslint:enable */
|
||||
|
||||
nativeViewProtected: android.widget.EditText;
|
||||
nativeTextViewProtected: android.widget.EditText;
|
||||
|
||||
private _dirtyTextAccumulator: string;
|
||||
private _keyListenerCache: android.text.method.KeyListener;
|
||||
private _inputType: number;
|
||||
|
||||
@ -120,12 +118,16 @@ export abstract class EditableTextBase extends EditableTextBaseCommon {
|
||||
|
||||
public disposeNativeView(): void {
|
||||
const editText = this.nativeTextViewProtected;
|
||||
|
||||
editText.removeTextChangedListener((<any>editText).listener);
|
||||
editText.setOnFocusChangeListener(null);
|
||||
editText.setOnEditorActionListener(null);
|
||||
(<any>editText).listener.owner = null;
|
||||
(<any>editText).listener = null;
|
||||
this._keyListenerCache = null;
|
||||
this._dirtyTextAccumulator = undefined;
|
||||
this._inputType = 0;
|
||||
|
||||
super.disposeNativeView();
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ export class HtmlView extends HtmlViewBase {
|
||||
|
||||
// Remove extra padding
|
||||
this.nativeViewProtected.textContainer.lineFragmentPadding = 0;
|
||||
this.nativeViewProtected.textContainerInset = (UIEdgeInsets as any).zero;
|
||||
this.nativeViewProtected.textContainerInset = UIEdgeInsetsZero;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -19,6 +19,8 @@ enum FixedSize {
|
||||
@CSSType('Label')
|
||||
export class Label extends TextBase implements LabelDefinition {
|
||||
nativeViewProtected: TNSLabel;
|
||||
nativeTextViewProtected: TNSLabel;
|
||||
|
||||
private _fixedSize: FixedSize;
|
||||
|
||||
public createNativeView() {
|
||||
@ -28,6 +30,11 @@ export class Label extends TextBase implements LabelDefinition {
|
||||
return view;
|
||||
}
|
||||
|
||||
public disposeNativeView(): void {
|
||||
super.disposeNativeView();
|
||||
this._fixedSize = null;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
get ios(): TNSLabel {
|
||||
return this.nativeTextViewProtected;
|
||||
@ -102,7 +109,7 @@ export class Label extends TextBase implements LabelDefinition {
|
||||
}
|
||||
|
||||
private _measureNativeView(width: number, widthMode: number, height: number, heightMode: number): { width: number; height: number } {
|
||||
const view = <UILabel>this.nativeTextViewProtected;
|
||||
const view = this.nativeTextViewProtected;
|
||||
|
||||
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;
|
||||
|
||||
@ -123,7 +130,8 @@ export class Label extends TextBase implements LabelDefinition {
|
||||
private adjustLineBreak() {
|
||||
const whiteSpace = this.whiteSpace;
|
||||
const textOverflow = this.textOverflow;
|
||||
const nativeView = this.nativeViewProtected;
|
||||
const nativeView = this.nativeTextViewProtected;
|
||||
|
||||
switch (whiteSpace) {
|
||||
case 'normal':
|
||||
nativeView.lineBreakMode = NSLineBreakMode.ByWordWrapping;
|
||||
@ -158,7 +166,7 @@ export class Label extends TextBase implements LabelDefinition {
|
||||
const cgColor = color ? color.CGColor : null;
|
||||
nativeView.layer.backgroundColor = cgColor;
|
||||
},
|
||||
true
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ export class ScrollView extends ScrollViewBase {
|
||||
nativeViewProtected: org.nativescript.widgets.VerticalScrollView | org.nativescript.widgets.HorizontalScrollView;
|
||||
private _androidViewId = -1;
|
||||
private handler: android.view.ViewTreeObserver.OnScrollChangedListener;
|
||||
private scrollChangeHandler: androidx.core.widget.NestedScrollView.OnScrollChangeListener;
|
||||
|
||||
get horizontalOffset(): number {
|
||||
const nativeView = this.nativeViewProtected;
|
||||
|
@ -73,7 +73,7 @@ class UISearchBarImpl extends UISearchBar {
|
||||
export class SearchBar extends SearchBarBase {
|
||||
nativeViewProtected: UISearchBar;
|
||||
private _delegate;
|
||||
private __textField: UITextField;
|
||||
private _textField: UITextField;
|
||||
|
||||
createNativeView() {
|
||||
return UISearchBarImpl.new();
|
||||
@ -87,6 +87,7 @@ export class SearchBar extends SearchBarBase {
|
||||
|
||||
disposeNativeView() {
|
||||
this._delegate = null;
|
||||
this._textField = null;
|
||||
super.disposeNativeView();
|
||||
}
|
||||
|
||||
@ -94,26 +95,26 @@ export class SearchBar extends SearchBarBase {
|
||||
(<UIResponder>this.ios).resignFirstResponder();
|
||||
}
|
||||
|
||||
private _getTextField(): UITextField {
|
||||
if (!this._textField) {
|
||||
this._textField = this.ios.valueForKey('searchField');
|
||||
}
|
||||
|
||||
return this._textField;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
get ios(): UISearchBar {
|
||||
return this.nativeViewProtected;
|
||||
}
|
||||
|
||||
get _textField(): UITextField {
|
||||
if (!this.__textField) {
|
||||
this.__textField = this.ios.valueForKey('searchField');
|
||||
}
|
||||
|
||||
return this.__textField;
|
||||
}
|
||||
|
||||
[isEnabledProperty.setNative](value: boolean) {
|
||||
const nativeView = this.nativeViewProtected;
|
||||
if (nativeView instanceof UIControl) {
|
||||
nativeView.enabled = value;
|
||||
}
|
||||
|
||||
const textField = this._textField;
|
||||
const textField = this._getTextField();
|
||||
if (textField) {
|
||||
textField.enabled = value;
|
||||
}
|
||||
@ -128,7 +129,7 @@ export class SearchBar extends SearchBarBase {
|
||||
}
|
||||
|
||||
[colorProperty.getDefault](): UIColor {
|
||||
const sf = this._textField;
|
||||
const sf = this._getTextField();
|
||||
if (sf) {
|
||||
return sf.textColor;
|
||||
}
|
||||
@ -136,7 +137,7 @@ export class SearchBar extends SearchBarBase {
|
||||
return null;
|
||||
}
|
||||
[colorProperty.setNative](value: UIColor | Color) {
|
||||
const sf = this._textField;
|
||||
const sf = this._getTextField();
|
||||
const color = value instanceof Color ? value.ios : value;
|
||||
if (sf) {
|
||||
sf.textColor = color;
|
||||
@ -145,12 +146,12 @@ export class SearchBar extends SearchBarBase {
|
||||
}
|
||||
|
||||
[fontInternalProperty.getDefault](): UIFont {
|
||||
const sf = this._textField;
|
||||
const sf = this._getTextField();
|
||||
|
||||
return sf ? sf.font : null;
|
||||
}
|
||||
[fontInternalProperty.setNative](value: UIFont | Font) {
|
||||
const sf = this._textField;
|
||||
const sf = this._getTextField();
|
||||
if (sf) {
|
||||
sf.font = value instanceof Font ? value.getUIFont(sf.font) : value;
|
||||
}
|
||||
@ -179,7 +180,7 @@ export class SearchBar extends SearchBarBase {
|
||||
}
|
||||
|
||||
[textFieldBackgroundColorProperty.getDefault](): UIColor {
|
||||
const textField = this._textField;
|
||||
const textField = this._getTextField();
|
||||
if (textField) {
|
||||
return textField.backgroundColor;
|
||||
}
|
||||
@ -188,7 +189,7 @@ export class SearchBar extends SearchBarBase {
|
||||
}
|
||||
[textFieldBackgroundColorProperty.setNative](value: Color | UIColor) {
|
||||
const color = value instanceof Color ? value.ios : value;
|
||||
const textField = this._textField;
|
||||
const textField = this._getTextField();
|
||||
if (textField) {
|
||||
textField.backgroundColor = color;
|
||||
}
|
||||
@ -219,6 +220,6 @@ export class SearchBar extends SearchBarBase {
|
||||
attributes[NSForegroundColorAttributeName] = this.textFieldHintColor.ios;
|
||||
}
|
||||
const attributedPlaceholder = NSAttributedString.alloc().initWithStringAttributes(stringValue, attributes);
|
||||
this._textField.attributedPlaceholder = attributedPlaceholder;
|
||||
this._getTextField().attributedPlaceholder = attributedPlaceholder;
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export * from './text-base-common';
|
||||
let TextTransformation: TextTransformation;
|
||||
|
||||
export interface TextTransformation {
|
||||
new (owner: TextBase): any /* android.text.method.TransformationMethod */;
|
||||
new (owner: TextBase): android.text.method.TransformationMethod;
|
||||
}
|
||||
|
||||
function initializeTextTransformation(): void {
|
||||
@ -169,9 +169,8 @@ function initializeBaselineAdjustedSpan(): void {
|
||||
}
|
||||
|
||||
export class TextBase extends TextBaseCommon {
|
||||
nativeViewProtected: org.nativescript.widgets.StyleableTextView;
|
||||
// @ts-ignore
|
||||
nativeTextViewProtected: org.nativescript.widgets.StyleableTextView;
|
||||
public nativeViewProtected: org.nativescript.widgets.StyleableTextView;
|
||||
|
||||
private _defaultTransformationMethod: android.text.method.TransformationMethod;
|
||||
private _paintFlags: number;
|
||||
private _minHeight: number;
|
||||
@ -181,6 +180,10 @@ export class TextBase extends TextBaseCommon {
|
||||
private _tappable = false;
|
||||
private _defaultMovementMethod: android.text.method.MovementMethod;
|
||||
|
||||
get nativeTextViewProtected(): org.nativescript.widgets.StyleableTextView {
|
||||
return super.nativeTextViewProtected;
|
||||
}
|
||||
|
||||
public initNativeView(): void {
|
||||
super.initNativeView();
|
||||
initializeTextTransformation();
|
||||
@ -193,6 +196,19 @@ export class TextBase extends TextBaseCommon {
|
||||
this._maxLines = nativeView.getMaxLines();
|
||||
}
|
||||
|
||||
public disposeNativeView(): void {
|
||||
super.disposeNativeView();
|
||||
|
||||
this._tappable = false;
|
||||
this._defaultTransformationMethod = null;
|
||||
this._defaultMovementMethod = null;
|
||||
this._paintFlags = 0;
|
||||
this._minHeight = 0;
|
||||
this._maxHeight = 0;
|
||||
this._minLines = 0;
|
||||
this._maxLines = 0;
|
||||
}
|
||||
|
||||
public resetNativeView(): void {
|
||||
super.resetNativeView();
|
||||
const nativeView = this.nativeTextViewProtected;
|
||||
@ -502,13 +518,13 @@ export class TextBase extends TextBaseCommon {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.nativeTextViewProtected.setTextStroke(Length.toDevicePixels(this.style.textStroke.width), this.style.textStroke.color.android, this.style.color.android);
|
||||
} else if (this.nativeTextViewProtected.setTextStroke) {
|
||||
// reset
|
||||
this.nativeViewProtected.setTextStroke(0, 0, 0);
|
||||
this.nativeTextViewProtected.setTextStroke(0, 0, 0);
|
||||
}
|
||||
|
||||
this.nativeTextViewProtected.setText(<any>transformedText);
|
||||
this.nativeTextViewProtected.setText(transformedText);
|
||||
}
|
||||
|
||||
_setTappableState(tappable: boolean) {
|
||||
@ -608,10 +624,8 @@ function setSpanModifiers(ssb: android.text.SpannableStringBuilder, span: Span,
|
||||
ssb.setSpan(new android.text.style.ForegroundColorSpan(color.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
const backgroundColor: Color = getClosestPropertyValue(<any>backgroundColorProperty, span);
|
||||
|
||||
if (backgroundColor) {
|
||||
ssb.setSpan(new android.text.style.BackgroundColorSpan(backgroundColor.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
if (spanStyle.backgroundColor) {
|
||||
ssb.setSpan(new android.text.style.BackgroundColorSpan(spanStyle.backgroundColor.android), start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
const textDecoration: CoreTypes.TextDecorationType = getClosestPropertyValue(textDecorationProperty, span);
|
||||
|
@ -34,7 +34,7 @@ class UILabelClickHandlerImpl extends NSObject {
|
||||
public linkTap(tapGesture: UITapGestureRecognizer) {
|
||||
const owner = this._owner?.deref();
|
||||
if (owner) {
|
||||
const label = <UILabel>owner.nativeTextViewProtected;
|
||||
const nativeView = owner.nativeTextViewProtected instanceof UIButton ? owner.nativeTextViewProtected.titleLabel : owner.nativeTextViewProtected;
|
||||
|
||||
// This offset along with setting paragraph style alignment will achieve perfect horizontal alignment for NSTextContainer
|
||||
let offsetXMultiplier: number;
|
||||
@ -53,18 +53,27 @@ class UILabelClickHandlerImpl extends NSObject {
|
||||
|
||||
const layoutManager = NSLayoutManager.alloc().init();
|
||||
const textContainer = NSTextContainer.alloc().initWithSize(CGSizeZero);
|
||||
const textStorage = NSTextStorage.alloc().initWithAttributedString(owner.nativeTextViewProtected['attributedText']);
|
||||
const textStorage = NSTextStorage.alloc().initWithAttributedString(nativeView.attributedText);
|
||||
|
||||
layoutManager.addTextContainer(textContainer);
|
||||
textStorage.addLayoutManager(layoutManager);
|
||||
|
||||
textContainer.lineFragmentPadding = 0;
|
||||
textContainer.lineBreakMode = label.lineBreakMode;
|
||||
textContainer.maximumNumberOfLines = label.numberOfLines;
|
||||
const labelSize = label.bounds.size;
|
||||
|
||||
if (nativeView instanceof UITextView) {
|
||||
textContainer.lineBreakMode = nativeView.textContainer.lineBreakMode;
|
||||
textContainer.maximumNumberOfLines = nativeView.textContainer.maximumNumberOfLines;
|
||||
} else {
|
||||
if (!(nativeView instanceof UITextField)) {
|
||||
textContainer.lineBreakMode = nativeView.lineBreakMode;
|
||||
textContainer.maximumNumberOfLines = nativeView.numberOfLines;
|
||||
}
|
||||
}
|
||||
|
||||
const labelSize = nativeView.bounds.size;
|
||||
textContainer.size = labelSize;
|
||||
|
||||
const locationOfTouchInLabel = tapGesture.locationInView(label);
|
||||
const locationOfTouchInLabel = tapGesture.locationInView(nativeView);
|
||||
const textBoundingBox = layoutManager.usedRectForTextContainer(textContainer);
|
||||
|
||||
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 {
|
||||
|
||||
export class TextBase extends TextBaseCommon {
|
||||
public nativeViewProtected: UITextField | UITextView | UILabel | UIButton;
|
||||
// @ts-ignore
|
||||
public nativeTextViewProtected: UITextField | UITextView | UILabel | UIButton;
|
||||
private _tappable = false;
|
||||
private _tapGestureRecognizer: UITapGestureRecognizer;
|
||||
public _spanRanges: NSRange[];
|
||||
|
||||
private _tappable = false;
|
||||
private _linkTapHandler: UILabelClickHandlerImpl;
|
||||
private _tapGestureRecognizer: UITapGestureRecognizer;
|
||||
|
||||
get nativeTextViewProtected(): UITextField | UITextView | UILabel | UIButton {
|
||||
return super.nativeTextViewProtected;
|
||||
}
|
||||
|
||||
public initNativeView(): void {
|
||||
super.initNativeView();
|
||||
this._setTappableState(false);
|
||||
}
|
||||
|
||||
public disposeNativeView(): void {
|
||||
super.disposeNativeView();
|
||||
|
||||
this._tappable = false;
|
||||
this._linkTapHandler = null;
|
||||
this._tapGestureRecognizer = null;
|
||||
}
|
||||
|
||||
_setTappableState(tappable: boolean) {
|
||||
if (this._tappable !== tappable) {
|
||||
const nativeTextView = this.nativeTextViewProtected;
|
||||
|
||||
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;
|
||||
// Associate handler with menuItem or it will get collected by JSC
|
||||
this._linkTapHandler = tapHandler;
|
||||
|
||||
this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(tapHandler, 'linkTap');
|
||||
this.nativeViewProtected.userInteractionEnabled = true;
|
||||
this.nativeViewProtected.addGestureRecognizer(this._tapGestureRecognizer);
|
||||
this._tapGestureRecognizer = UITapGestureRecognizer.alloc().initWithTargetAction(this._linkTapHandler, 'linkTap');
|
||||
nativeTextView.addGestureRecognizer(this._tapGestureRecognizer);
|
||||
} else {
|
||||
this.nativeViewProtected.userInteractionEnabled = false;
|
||||
this.nativeViewProtected.removeGestureRecognizer(this._tapGestureRecognizer);
|
||||
nativeTextView.removeGestureRecognizer(this._tapGestureRecognizer);
|
||||
|
||||
this._linkTapHandler = null;
|
||||
this._tapGestureRecognizer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import { Style } from '../styling/style';
|
||||
import { Observable } from '../../data/observable';
|
||||
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';
|
||||
|
||||
|
@ -114,7 +114,9 @@ class UITextFieldImpl extends UITextField {
|
||||
|
||||
export class TextField extends TextFieldBase {
|
||||
nativeViewProtected: UITextField;
|
||||
|
||||
private _delegate: UITextFieldDelegateImpl;
|
||||
private _firstEdit: boolean;
|
||||
|
||||
createNativeView() {
|
||||
return UITextFieldImpl.initWithOwner(new WeakRef(this));
|
||||
@ -128,6 +130,7 @@ export class TextField extends TextFieldBase {
|
||||
|
||||
disposeNativeView() {
|
||||
this._delegate = null;
|
||||
this._firstEdit = false;
|
||||
super.disposeNativeView();
|
||||
}
|
||||
|
||||
@ -136,10 +139,8 @@ export class TextField extends TextFieldBase {
|
||||
return this.nativeViewProtected;
|
||||
}
|
||||
|
||||
private firstEdit: boolean;
|
||||
|
||||
public textFieldShouldBeginEditing(textField: UITextField): boolean {
|
||||
this.firstEdit = true;
|
||||
this._firstEdit = true;
|
||||
|
||||
return this.editable;
|
||||
}
|
||||
@ -157,7 +158,7 @@ export class TextField extends TextFieldBase {
|
||||
}
|
||||
|
||||
public textFieldShouldClear(textField: UITextField) {
|
||||
this.firstEdit = false;
|
||||
this._firstEdit = false;
|
||||
textProperty.nativeValueChange(this, '');
|
||||
|
||||
return true;
|
||||
@ -192,7 +193,7 @@ export class TextField extends TextFieldBase {
|
||||
if (this.updateTextTrigger === 'textChanged') {
|
||||
if (this.valueFormatter) {
|
||||
// format/replace
|
||||
let currentValue = textField.text;
|
||||
const currentValue = textField.text;
|
||||
let nativeValueChange = `${textField.text}${replacementString}`;
|
||||
if (replacementString === '') {
|
||||
// clearing when empty
|
||||
@ -207,7 +208,7 @@ export class TextField extends TextFieldBase {
|
||||
// 1. secureTextEntry with firstEdit should not replace
|
||||
// 2. emoji's should not replace value
|
||||
// 3. convenient keyboard shortcuts should not replace value (eg, '.com')
|
||||
const shouldReplaceString = (textField.secureTextEntry && this.firstEdit) || (delta > 1 && !isEmoji(replacementString) && delta !== replacementString.length);
|
||||
const shouldReplaceString = (textField.secureTextEntry && this._firstEdit) || (delta > 1 && !isEmoji(replacementString) && delta !== replacementString.length);
|
||||
if (shouldReplaceString) {
|
||||
textProperty.nativeValueChange(this, replacementString);
|
||||
} else {
|
||||
@ -226,7 +227,7 @@ export class TextField extends TextFieldBase {
|
||||
// if the textfield is in auto size we need to request a layout to take the new text width into account
|
||||
this.requestLayout();
|
||||
}
|
||||
this.firstEdit = false;
|
||||
this._firstEdit = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -88,7 +88,8 @@ export class TextView extends TextViewBaseCommon {
|
||||
nativeViewProtected: UITextView;
|
||||
nativeTextViewProtected: UITextView;
|
||||
private _delegate: UITextViewDelegateImpl;
|
||||
_isShowingHint: boolean;
|
||||
private _isShowingHint: boolean;
|
||||
|
||||
public _isEditing: boolean;
|
||||
|
||||
private _hintColor = majorVersion <= 12 || !UIColor.placeholderTextColor ? UIColor.blackColor.colorWithAlphaComponent(0.22) : UIColor.placeholderTextColor;
|
||||
|
Reference in New Issue
Block a user