Files
NativeScript/tns-core-modules/ui/core/view/view.android.ts
Alexander Vakrilov 23757e5dfc Enable recycling of nativeView 2 (#4467)
* enable recycling of nativeView

* backgroundInternal is reset if setting new value leads to background.isEmpty() == true.

* android background.getDefault always return copy of the background. Now all controls that mutate the background can be reset to initial state (e.g. Button & ActionBar)
passing resources to copied background so it respect density.
fix properties initNativeView

* reset padding when backgroundInternal is reset.

* Fix text reset
Fix padding reset

* fix tsc errors

* fix ugly text rendering.

* Add unit tests for recycling native views
Fix several issues that came from the above tests
Fix maxLength property missing a converter callback
Remove old files

* Remove old files

* Revert backgroundInternal setter

* change the order of tests so that appium can work again

* Remove suggestion on every TextView & TextField init (strangely it is enabled after view is recycled....)

* Fix function to get parent layout if specified

* Button stateListAnimator restored when button is recycled
zIndex defaultValue is now undefined instead of NaN

* revert zIndex.setNative to always clear stateListAnimator because it was breaking one UI test (setting value=0 was returning the previous stateListAnimator)

* fix search-bar backgound-color recycling

* Fix alignments setters

* Fix imageView recycling
Fix button recycling
Fix edit-text recycling
resetNativeView is called only if recycleNativeView flag is true

* Fix incorrect merge

* Fix text-view & text-field textTransform

* Fix EditText text reset

* Fix runtime crash on ARM emulator API 21

* Fix text-base minHeight. maxHeight reset
Fix reset of isUserInteractionEnabled
2017-06-29 18:01:22 +03:00

640 lines
23 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Definitions.
import { Point, CustomLayoutView as CustomLayoutViewDefinition, dip } from ".";
import { GestureTypes, GestureEventData } from "../../gestures";
// Types.
import { Background, ad as androidBackground } from "../../styling/background";
import {
ViewCommon, layout, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty,
traceEnabled, traceWrite, traceCategories, traceNotifyEvent
} from "./view-common";
import {
Length, PercentLength, Visibility, HorizontalAlignment, VerticalAlignment,
visibilityProperty, opacityProperty, horizontalAlignmentProperty, verticalAlignmentProperty,
minWidthProperty, minHeightProperty, widthProperty, heightProperty,
marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty,
rotateProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty,
zIndexProperty, backgroundInternalProperty
} from "../../styling/style-properties";
import { profile } from "../../../profiling";
export * from "./view-common";
interface TouchListener {
new (owner: View): android.view.View.OnTouchListener;
}
let TouchListener: TouchListener;
let disableUserInteractionListener: org.nativescript.widgets.DisableUserInteractionListener;
function initializeDisabledListener(): void {
if (disableUserInteractionListener) {
return;
}
disableUserInteractionListener = new org.nativescript.widgets.DisableUserInteractionListener();
}
function initializeTouchListener(): void {
if (TouchListener) {
return;
}
@Interfaces([android.view.View.OnTouchListener])
class TouchListenerImpl extends java.lang.Object implements android.view.View.OnTouchListener {
constructor(private owner: View) {
super();
return global.__native(this);
}
onTouch(view: android.view.View, event: android.view.MotionEvent): boolean {
const owner = this.owner;
for (let type in owner._gestureObservers) {
let list = owner._gestureObservers[type];
list.forEach(element => {
element.androidOnTouchEvent(event);
});
}
let nativeView = owner.nativeView;
if (!nativeView || !nativeView.onTouchEvent) {
return false;
}
return nativeView.onTouchEvent(event);
}
}
TouchListener = TouchListenerImpl;
}
export class View extends ViewCommon {
private touchListenerIsSet: boolean;
private touchListener: android.view.View.OnTouchListener;
nativeView: android.view.View;
// TODO: Implement unobserve that detach the touchListener.
observe(type: GestureTypes, callback: (args: GestureEventData) => void, thisArg?: any): void {
super.observe(type, callback, thisArg);
if (this.isLoaded && !this.touchListenerIsSet) {
this.setOnTouchListener();
}
}
@profile
public onLoaded() {
super.onLoaded();
this.setOnTouchListener();
}
@profile
public onUnloaded() {
if (this.touchListenerIsSet) {
this.nativeView.setOnTouchListener(null);
this.touchListenerIsSet = false;
}
this._cancelAllAnimations();
super.onUnloaded();
}
private hasGestureObservers() {
return this._gestureObservers && Object.keys(this._gestureObservers).length > 0
}
private setOnTouchListener() {
if (this.nativeView && this.hasGestureObservers()) {
this.touchListenerIsSet = true;
if (this.nativeView.setClickable) {
this.nativeView.setClickable(true);
}
initializeTouchListener();
this.touchListener = this.touchListener || new TouchListener(this);
this.nativeView.setOnTouchListener(this.touchListener);
}
}
get isLayoutRequired(): boolean {
return !this.isLayoutValid;
}
get isLayoutValid(): boolean {
if (this.nativeView) {
return !this.nativeView.isLayoutRequested();
}
return false;
}
public layoutNativeView(left: number, top: number, right: number, bottom: number): void {
if (this.nativeView) {
this.nativeView.layout(left, top, right, bottom);
}
}
@profile
public requestLayout(): void {
super.requestLayout();
if (this.nativeView) {
return this.nativeView.requestLayout();
}
}
public measure(widthMeasureSpec: number, heightMeasureSpec: number): void {
super.measure(widthMeasureSpec, heightMeasureSpec);
this.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public layout(left: number, top: number, right: number, bottom: number): void {
super.layout(left, top, right, bottom);
this.onLayout(left, top, right, bottom);
}
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
let view = this.nativeView;
if (view) {
view.measure(widthMeasureSpec, heightMeasureSpec);
this.setMeasuredDimension(view.getMeasuredWidth(), view.getMeasuredHeight());
}
}
public onLayout(left: number, top: number, right: number, bottom: number): void {
let view = this.nativeView;
if (view) {
this.layoutNativeView(left, top, right, bottom);
}
}
_getCurrentLayoutBounds(): { left: number; top: number; right: number; bottom: number } {
if (this.nativeView && !this.isCollapsed) {
return {
left: this.nativeView.getLeft(),
top: this.nativeView.getTop(),
right: this.nativeView.getRight(),
bottom: this.nativeView.getBottom()
};
} else {
return { left: 0, top: 0, right: 0, bottom: 0 };
}
}
public getMeasuredWidth(): number {
if (this.nativeView) {
return this.nativeView.getMeasuredWidth();
}
return super.getMeasuredWidth();
}
public getMeasuredHeight(): number {
if (this.nativeView) {
return this.nativeView.getMeasuredHeight();
}
return super.getMeasuredHeight();
}
public focus(): boolean {
if (this.nativeView) {
return this.nativeView.requestFocus();
}
return false;
}
public getLocationInWindow(): Point {
if (!this.nativeView || !this.nativeView.getWindowToken()) {
return undefined;
}
let nativeArray = (<any>Array).create("int", 2);
this.nativeView.getLocationInWindow(nativeArray);
return {
x: layout.toDeviceIndependentPixels(nativeArray[0]),
y: layout.toDeviceIndependentPixels(nativeArray[1]),
}
}
public getLocationOnScreen(): Point {
if (!this.nativeView || !this.nativeView.getWindowToken()) {
return undefined;
}
let nativeArray = (<any>Array).create("int", 2);
this.nativeView.getLocationOnScreen(nativeArray);
return {
x: layout.toDeviceIndependentPixels(nativeArray[0]),
y: layout.toDeviceIndependentPixels(nativeArray[1]),
}
}
public getLocationRelativeTo(otherView: ViewCommon): Point {
if (!this.nativeView || !this.nativeView.getWindowToken() ||
!otherView.nativeView || !otherView.nativeView.getWindowToken() ||
this.nativeView.getWindowToken() !== otherView.nativeView.getWindowToken()) {
return undefined;
}
let myArray = (<any>Array).create("int", 2);
this.nativeView.getLocationOnScreen(myArray);
let otherArray = (<any>Array).create("int", 2);
otherView.nativeView.getLocationOnScreen(otherArray);
return {
x: layout.toDeviceIndependentPixels(myArray[0] - otherArray[0]),
y: layout.toDeviceIndependentPixels(myArray[1] - otherArray[1]),
}
}
public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number {
let result = size;
switch (specMode) {
case layout.UNSPECIFIED:
result = size;
break;
case layout.AT_MOST:
if (specSize < size) {
result = specSize | layout.MEASURED_STATE_TOO_SMALL;
}
break;
case layout.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState & layout.MEASURED_STATE_MASK);
}
[isEnabledProperty.setNative](value: boolean) {
this.nativeView.setEnabled(value);
}
[originXProperty.getDefault](): number {
return this.nativeView.getPivotX();
}
[originXProperty.setNative](value: number) {
org.nativescript.widgets.OriginPoint.setX(this.nativeView, value);
}
[originYProperty.getDefault](): number {
return this.nativeView.getPivotY();
}
[originYProperty.setNative](value: number) {
org.nativescript.widgets.OriginPoint.setY(this.nativeView, value);
}
[automationTextProperty.getDefault](): string {
return this.nativeView.getContentDescription();
}
[automationTextProperty.setNative](value: string) {
this.nativeView.setContentDescription(value);
}
[isUserInteractionEnabledProperty.setNative](value: boolean) {
if (!value) {
initializeDisabledListener();
// User interaction is disabled -- we stop it and we do not care whether someone wants to listen for gestures.
this.nativeView.setOnTouchListener(disableUserInteractionListener);
} else {
this.setOnTouchListener();
if (!this.touchListenerIsSet) {
this.nativeView.setOnTouchListener(null);
}
}
}
[visibilityProperty.getDefault](): Visibility {
let nativeVisibility = this.nativeView.getVisibility();
switch (nativeVisibility) {
case android.view.View.VISIBLE:
return "visible";
case android.view.View.INVISIBLE:
return "hidden";
case android.view.View.GONE:
return "collapse";
default:
throw new Error(`Unsupported android.view.View visibility: ${nativeVisibility}. Currently supported values are android.view.View.VISIBLE, android.view.View.INVISIBLE, android.view.View.GONE.`);
}
}
[visibilityProperty.setNative](value: Visibility) {
switch (value) {
case "visible":
this.nativeView.setVisibility(android.view.View.VISIBLE);
break;
case "hidden":
this.nativeView.setVisibility(android.view.View.INVISIBLE);
break;
case "collapse":
this.nativeView.setVisibility(android.view.View.GONE);
break;
default:
throw new Error(`Invalid visibility value: ${value}. Valid values are: visible, hidden, collapse.`);
}
}
[opacityProperty.getDefault](): number {
return this.nativeView.getAlpha();
}
[opacityProperty.setNative](value: number) {
this.nativeView.setAlpha(float(value));
}
[horizontalAlignmentProperty.getDefault](): HorizontalAlignment {
return <HorizontalAlignment>org.nativescript.widgets.ViewHelper.getHorizontalAlignment(this.nativeView);
}
[horizontalAlignmentProperty.setNative](value: HorizontalAlignment) {
const nativeView = this.nativeView;
const lp: any = nativeView.getLayoutParams() || new org.nativescript.widgets.CommonLayoutParams();
// Set only if params gravity exists.
if (lp.gravity !== undefined) {
switch (value) {
case "left":
lp.gravity = android.view.Gravity.LEFT | (lp.gravity & android.view.Gravity.VERTICAL_GRAVITY_MASK);
if (lp.weight < 0) {
lp.weight = -2;
}
break;
case "center":
lp.gravity = android.view.Gravity.CENTER_HORIZONTAL | (lp.gravity & android.view.Gravity.VERTICAL_GRAVITY_MASK);
if (lp.weight < 0) {
lp.weight = -2;
}
break;
case "right":
lp.gravity = android.view.Gravity.RIGHT | (lp.gravity & android.view.Gravity.VERTICAL_GRAVITY_MASK);
if (lp.weight < 0) {
lp.weight = -2;
}
break;
case "stretch":
lp.gravity = android.view.Gravity.FILL_HORIZONTAL | (lp.gravity & android.view.Gravity.VERTICAL_GRAVITY_MASK);
if (lp.weight < 0) {
lp.weight = -1;
}
break;
}
nativeView.setLayoutParams(lp);
}
}
[verticalAlignmentProperty.getDefault](): VerticalAlignment {
return <VerticalAlignment>org.nativescript.widgets.ViewHelper.getVerticalAlignment(this.nativeView);
}
[verticalAlignmentProperty.setNative](value: VerticalAlignment) {
const nativeView = this.nativeView;
const lp: any = nativeView.getLayoutParams() || new org.nativescript.widgets.CommonLayoutParams();
// Set only if params gravity exists.
if (lp.gravity !== undefined) {
switch (value) {
case "top":
lp.gravity = android.view.Gravity.TOP | (lp.gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK);
if (lp.height < 0) {
lp.height = -2;
}
break;
case "middle":
lp.gravity = android.view.Gravity.CENTER_VERTICAL | (lp.gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK);
if (lp.height < 0) {
lp.height = -2;
}
break;
case "bottom":
lp.gravity = android.view.Gravity.BOTTOM | (lp.gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK);
if (lp.height < 0) {
lp.height = -2;
}
break;
case "stretch":
lp.gravity = android.view.Gravity.FILL_VERTICAL | (lp.gravity & android.view.Gravity.HORIZONTAL_GRAVITY_MASK);
if (lp.height < 0) {
lp.height = -1;
}
break;
}
nativeView.setLayoutParams(lp);
}
}
[rotateProperty.setNative](value: number) {
org.nativescript.widgets.ViewHelper.setRotate(this.nativeView, float(value));
}
[scaleXProperty.setNative](value: number) {
org.nativescript.widgets.ViewHelper.setScaleX(this.nativeView, float(value));
}
[scaleYProperty.setNative](value: number) {
org.nativescript.widgets.ViewHelper.setScaleY(this.nativeView, float(value));
}
[translateXProperty.setNative](value: dip) {
org.nativescript.widgets.ViewHelper.setTranslateX(this.nativeView, layout.toDevicePixels(value));
}
[translateYProperty.setNative](value: dip) {
org.nativescript.widgets.ViewHelper.setTranslateY(this.nativeView, layout.toDevicePixels(value));
}
[zIndexProperty.getDefault](): number {
return 0;
}
[zIndexProperty.setNative](value: number) {
org.nativescript.widgets.ViewHelper.setZIndex(this.nativeView, value);
}
[backgroundInternalProperty.getDefault](): android.graphics.drawable.Drawable {
const nativeView = this.nativeView;
const drawable = nativeView.getBackground();
return drawable ? drawable.getConstantState().newDrawable(nativeView.getResources()) : null;
}
[backgroundInternalProperty.setNative](value: android.graphics.drawable.Drawable | Background) {
this._redrawNativeBackground(value);
}
[minWidthProperty.setNative](value: Length) {
if (this.parent instanceof CustomLayoutView && this.parent.nativeView) {
this.parent._setChildMinWidthNative(this);
} else {
this._setMinWidthNative(this.minWidth);
}
}
[minHeightProperty.setNative](value: Length) {
if (this.parent instanceof CustomLayoutView && this.parent.nativeView) {
this.parent._setChildMinHeightNative(this);
} else {
this._setMinHeightNative(this.minHeight);
}
}
_redrawNativeBackground(value: android.graphics.drawable.Drawable | Background): void {
if (value instanceof Background) {
androidBackground.onBackgroundOrBorderPropertyChanged(this);
} else {
const nativeView = this.nativeView;
org.nativescript.widgets.ViewHelper.setBackground(nativeView, value);
nativeView.setPadding(this._defaultPaddingLeft, this._defaultPaddingTop, this._defaultPaddingRight, this._defaultPaddingBottom);
}
}
}
export class CustomLayoutView extends View implements CustomLayoutViewDefinition {
nativeView: android.view.ViewGroup;
public createNativeView() {
return new org.nativescript.widgets.ContentLayout(this._context);
}
public _addViewToNativeVisualTree(child: ViewCommon, atIndex: number = -1): boolean {
super._addViewToNativeVisualTree(child);
if (this.nativeView && child.nativeView) {
if (traceEnabled()) {
traceWrite(`${this}.nativeView.addView(${child}.nativeView, ${atIndex})`, traceCategories.VisualTreeEvents);
}
this.nativeView.addView(child.nativeView, atIndex);
if (child instanceof View) {
this._updateNativeLayoutParams(child);
}
return true;
}
return false;
}
public _updateNativeLayoutParams(child: View): void {
this._setChildMinWidthNative(child);
this._setChildMinHeightNative(child);
}
public _setChildMinWidthNative(child: View): void {
child._setMinWidthNative(child.minWidth);
}
public _setChildMinHeightNative(child: View): void {
child._setMinHeightNative(child.minHeight);
}
public _removeViewFromNativeVisualTree(child: ViewCommon): void {
super._removeViewFromNativeVisualTree(child);
const nativeView = this.nativeView;
const childView = child.nativeView;
if (nativeView && childView) {
nativeView.removeView(childView);
if (traceEnabled()) {
traceWrite(`${nativeView}.removeView(${childView})`, traceCategories.VisualTreeEvents);
traceNotifyEvent(child, "childInLayoutRemovedFromNativeVisualTree");
}
}
}
}
type NativeSetter = { (view: android.view.View, value: number): void };
type NativeGetter = { (view: android.view.View): number };
const percentNotSupported = (view: android.view.View, value: number) => { throw new Error("PercentLength is not supported."); };
interface NativePercentLengthPropertyOptions {
getter?: string | symbol;
setter: string | symbol;
auto?: number;
getPixels?: NativeGetter;
setPixels: NativeSetter;
setPercent?: NativeSetter
}
function createNativePercentLengthProperty(options: NativePercentLengthPropertyOptions) {
const { getter, setter, auto = 0 } = options;
let setPixels, getPixels, setPercent;
if (getter) {
View.prototype[getter] = function (this: View): PercentLength {
if (options) {
setPixels = options.setPixels;
getPixels = options.getPixels;
setPercent = options.setPercent || percentNotSupported;
options = null;
}
const value = getPixels(this.nativeView);
if (value == auto) { // tslint:disable-line
return "auto";
} else {
return { value, unit: "px" };
}
}
}
if (setter) {
View.prototype[setter] = function (this: View, length: PercentLength) {
if (options) {
setPixels = options.setPixels;
getPixels = options.getPixels;
setPercent = options.setPercent || percentNotSupported;
options = null;
}
if (length == "auto") { // tslint:disable-line
setPixels(this.nativeView, auto);
} else if (typeof length === "number") {
setPixels(this.nativeView, layout.round(layout.toDevicePixels(length)));
} else if (length.unit == "dip") { // tslint:disable-line
setPixels(this.nativeView, layout.round(layout.toDevicePixels(length.value)));
} else if (length.unit == "px") { // tslint:disable-line
setPixels(this.nativeView, layout.round(length.value));
} else if (length.unit == "%") { // tslint:disable-line
setPercent(this.nativeView, length.value);
} else {
throw new Error(`Unsupported PercentLength ${length}`);
}
}
}
}
createNativePercentLengthProperty({
setter: marginTopProperty.setNative,
get setPixels() { return org.nativescript.widgets.ViewHelper.setMarginTop },
get setPercent() { return org.nativescript.widgets.ViewHelper.setMarginTopPercent }
});
createNativePercentLengthProperty({
setter: marginRightProperty.setNative,
get setPixels() { return org.nativescript.widgets.ViewHelper.setMarginRight },
get setPercent() { return org.nativescript.widgets.ViewHelper.setMarginRightPercent }
});
createNativePercentLengthProperty({
setter: marginBottomProperty.setNative,
get setPixels() { return org.nativescript.widgets.ViewHelper.setMarginBottom },
get setPercent() { return org.nativescript.widgets.ViewHelper.setMarginBottomPercent }
});
createNativePercentLengthProperty({
setter: marginLeftProperty.setNative,
get setPixels() { return org.nativescript.widgets.ViewHelper.setMarginLeft },
get setPercent() { return org.nativescript.widgets.ViewHelper.setMarginLeftPercent }
});
createNativePercentLengthProperty({
setter: widthProperty.setNative,
auto: -1, //android.view.ViewGroup.LayoutParams.MATCH_PARENT,
get setPixels() { return org.nativescript.widgets.ViewHelper.setWidth },
get setPercent() { return org.nativescript.widgets.ViewHelper.setWidthPercent }
});
createNativePercentLengthProperty({
setter: heightProperty.setNative,
auto: -1, //android.view.ViewGroup.LayoutParams.MATCH_PARENT,
get setPixels() { return org.nativescript.widgets.ViewHelper.setHeight },
get setPercent() { return org.nativescript.widgets.ViewHelper.setHeightPercent }
});
createNativePercentLengthProperty({
setter: "_setMinWidthNative",
get setPixels() { return org.nativescript.widgets.ViewHelper.setMinWidth }
});
createNativePercentLengthProperty({
setter: "_setMinHeightNative",
get setPixels() { return org.nativescript.widgets.ViewHelper.setMinHeight }
});