add profile for functions, profile CSS-es on startup, use __time (#4227)

Call native setters once when a page is instantiated.

Fix background not being reset, Label's background is now set to background, not layer

Make the nativeView field instead of property for performance reasons
This commit is contained in:
Panayot Cankov
2017-06-08 09:51:09 +03:00
committed by GitHub
parent ce01ab597f
commit 014e7a8e0f
30 changed files with 654 additions and 284 deletions

View File

@@ -223,6 +223,9 @@ allTests["SEGMENTED-BAR"] = segmentedBarTests;
import * as animationTests from "./ui/animation/animation-tests";
allTests["ANIMATION"] = animationTests;
import * as lifecycle from "./ui/lifecycle/lifecycle-tests";
allTests["LIFECYCLE"] = lifecycle;
import * as cssAnimationTests from "./ui/animation/css-animation-tests";
allTests["CSS-ANIMATION"] = cssAnimationTests;

View File

@@ -224,6 +224,8 @@ var _testNativeBackgroundColorFromCss = function (views: Array<viewModule.View>)
var page = <pagesModule.Page>views[1];
page.css = "button { background-color: " + actualBackgroundColorHex + "; }";
helper.waitUntilLayoutReady(button);
var actualResult = buttonTestsNative.getNativeBackgroundColor(button).hex;
TKUnit.assert(actualResult === expectedNormalizedBackgroundColorHex, "Actual: " + actualResult + "; Expected: " + expectedNormalizedBackgroundColorHex);
}
@@ -232,6 +234,8 @@ var _testNativeBackgroundColorFromLocal = function (views: Array<viewModule.View
var button = <buttonModule.Button>views[0];
button.style.backgroundColor = new colorModule.Color(actualBackgroundColorHex);
helper.waitUntilLayoutReady(button);
var actualResult = buttonTestsNative.getNativeBackgroundColor(button).hex;
TKUnit.assert(actualResult === expectedNormalizedBackgroundColorHex, "Actual: " + actualResult + "; Expected: " + expectedNormalizedBackgroundColorHex);
}
@@ -267,6 +271,8 @@ export var test_StateHighlighted_also_fires_pressedState = function () {
var expectedNormalizedColor = "#FF0000";
page.css = "button:pressed { background-color: " + expectedColor + "; }";
helper.waitUntilLayoutReady(view);
view._goToVisualState('highlighted');
var actualResult = buttonTestsNative.getNativeBackgroundColor(view);
@@ -282,6 +288,8 @@ export var test_StateHighlighted_also_fires_activeState = function () {
var expectedNormalizedColor = "#FF0000";
page.css = "button:active { background-color: " + expectedColor + "; }";
helper.waitUntilLayoutReady(view);
view._goToVisualState('highlighted');
var actualResult = buttonTestsNative.getNativeBackgroundColor(view);
@@ -297,6 +305,8 @@ export var test_applying_disabled_visual_State_when_button_is_disable = function
var expectedNormalizedColor = "#FF0000";
page.css = "button:disabled { background-color: " + expectedColor + "; }";
helper.waitUntilLayoutReady(view);
view.isEnabled = false;
var actualResult = buttonTestsNative.getNativeBackgroundColor(view);

View File

@@ -153,6 +153,10 @@ export function waitUntilNavigatedFrom(oldPage: page.Page) {
TKUnit.waitUntilReady(() => getCurrentPage() && getCurrentPage() !== oldPage);
}
export function waitUntilLayoutReady(view: view.View): void {
TKUnit.waitUntilReady(() => view.isLayoutValid);
}
export function navigateWithEntry(entry: frame.NavigationEntry): page.Page {
let page = frame.resolvePageFromEntry(entry);
entry.moduleName = null;

View File

@@ -125,6 +125,8 @@ export class LabelTest extends testModule.UITest<LabelModule.Label> {
if (testLabel.android) {
this.waitUntilTestElementIsLoaded();
} else {
helper.waitUntilLayoutReady(testLabel);
}
const actualNative = labelTestsNative.getNativeBackgroundColor(testLabel);
@@ -217,7 +219,8 @@ export class LabelTest extends testModule.UITest<LabelModule.Label> {
let expBackgroundColor;
this.testPage.css = testCss;
this.waitUntilTestElementIsLoaded();
this.waitUntilTestElementLayoutIsValid();
const testLabel = label;
if (testLabel.android) {
@@ -480,6 +483,8 @@ export class LabelTest extends testModule.UITest<LabelModule.Label> {
view.isEnabled = false;
helper.waitUntilLayoutReady(view);
let actualResult = labelTestsNative.getNativeBackgroundColor(view);
TKUnit.assert(actualResult.hex === expectedNormalizedColor, "Actual: " + actualResult.hex + "; Expected: " + expectedNormalizedColor);
};

View File

@@ -0,0 +1,119 @@
import * as helper from "../helper";
import * as btnCounter from "./pages/button-counter";
import * as TKUnit from "../../TKUnit";
import { isIOS } from "tns-core-modules/platform";
// Integration tests that asser sertain runtime behavior, lifecycle events atc.
export function test_builder_sets_native_properties_once() {
const page = helper.navigateToModule("ui/lifecycle/pages/page-one");
const buttons = ["btn1", "btn2", "btn3", "btn4"].map(id => page.getViewById<btnCounter.Button>(id));
buttons.forEach(btn => {
TKUnit.assertEqual(btn.backgroundInternalSetNativeCount, 1, `Expected ${btn.id}'s backgroundInternal.setNative to be exactly once when inflating from xml.`);
TKUnit.assertEqual(btn.fontInternalSetNativeCount, 1, `Expected ${btn.id}'s fontInternal.setNative to be called exactly once when inflating from xml.`);
TKUnit.assertEqual(btn.nativeBackgroundRedraws, 1, `Expected ${btn.id}'s native background to propagated exactly once when inflating from xml.`);
});
}
export function test_setting_properties_does_not_makes_excessive_calls() {
const page = helper.navigateToModule("ui/lifecycle/pages/page-one");
const btn1 = page.getViewById<btnCounter.Button>("btn1");
function assert(count) {
TKUnit.assertEqual(btn1.backgroundInternalSetNativeCount, count, "backgroundInternal.setNative");
TKUnit.assertEqual(btn1.nativeBackgroundRedraws, count, "_redrawNativeBackground");
}
assert(1);
btn1.width = 50;
btn1.height = 50;
btn1.style.borderWidth = "18";
helper.waitUntilLayoutReady(btn1);
assert(2);
btn1.width = 80;
btn1.height = 80;
btn1.style.borderWidth = "22";
helper.waitUntilLayoutReady(btn1);
assert(3);
btn1.style.borderWidth = "26";
helper.waitUntilLayoutReady(btn1);
assert(4);
}
export function test_setting_one_property_while_suspedned_does_not_call_other_properties_native_setter() {
const page = helper.navigateToModule("ui/lifecycle/pages/page-one");
const btn1 = page.getViewById<btnCounter.Button>("btn1");
TKUnit.assertEqual(btn1.backgroundInternalSetNativeCount, 1, "backgroundInternal.setNative at step1");
TKUnit.assertEqual(btn1.fontInternalSetNativeCount, 1, "fontInternal.setNative at step1");
btn1._batchUpdate(() => {
// None
});
TKUnit.assertEqual(btn1.backgroundInternalSetNativeCount, 1, "backgroundInternal.setNative at step2");
TKUnit.assertEqual(btn1.fontInternalSetNativeCount, 1, "fontInternal.setNative at step2");
btn1._batchUpdate(() => {
btn1.style.borderWidth = "22";
});
TKUnit.assertEqual(btn1.backgroundInternalSetNativeCount, 2, "backgroundInternal.setNative at step3");
TKUnit.assertEqual(btn1.fontInternalSetNativeCount, 1, "fontInternal.setNative at step3");
btn1._batchUpdate(() => {
btn1.style.fontSize = 69;
});
TKUnit.assertEqual(btn1.backgroundInternalSetNativeCount, 2, "backgroundInternal.setNative at step4");
TKUnit.assertEqual(btn1.fontInternalSetNativeCount, 2, "fontInternal.setNative at step4");
}
export function test_css_properties_reset_only_once() {
const page = helper.navigateToModule("ui/lifecycle/pages/page-one");
const btn2 = page.getViewById<btnCounter.Button>("btn2");
TKUnit.assertEqual(btn2.backgroundInternalSetNativeCount, 1, `Expected ${btn2.id}'s backgroundInternal.setNative to be exactly once when inflating from xml.`);
TKUnit.assertEqual(btn2.fontInternalSetNativeCount, 1, `Expected ${btn2.id}'s fontInternal.setNative to be called exactly once when inflating from xml.`);
TKUnit.assertEqual(btn2.nativeBackgroundRedraws, 1, `Expected ${btn2.id}'s native background to propagated exactly once when inflating from xml.`);
page.css = "";
TKUnit.assertEqual(btn2.backgroundInternalSetNativeCount, 2, `Expected ${btn2.id}'s backgroundInternal.setNative to be exactly once when inflating from xml.`);
TKUnit.assertEqual(btn2.fontInternalSetNativeCount, 2, `Expected ${btn2.id}'s fontInternal.setNative to be called exactly once when inflating from xml.`);
TKUnit.assertEqual(btn2.nativeBackgroundRedraws, isIOS ? 1 : 2, `Expected ${btn2.id}'s native background to propagated exactly once when inflating from xml.`);
helper.waitUntilLayoutReady(btn2);
TKUnit.assertEqual(btn2.backgroundInternalSetNativeCount, 2, `Expected ${btn2.id}'s backgroundInternal.setNative to be exactly once when inflating from xml.`);
TKUnit.assertEqual(btn2.fontInternalSetNativeCount, 2, `Expected ${btn2.id}'s fontInternal.setNative to be called exactly once when inflating from xml.`);
TKUnit.assertEqual(btn2.nativeBackgroundRedraws, 2, `Expected ${btn2.id}'s native background to propagated exactly once when inflating from xml.`);
}
export function test_navigating_away_does_not_excessively_reset() {
const page = helper.navigateToModule("ui/lifecycle/pages/page-one");
const buttons = ["btn1", "btn2", "btn3", "btn4"].map(id => page.getViewById<btnCounter.Button>(id));
function assert(count) {
buttons.forEach(button => {
TKUnit.assertEqual(button.backgroundInternalSetNativeCount, count, `Expecting ${button.id}'s backgroundInternal.setNative call count`);
TKUnit.assertEqual(button.fontInternalSetNativeCount, count, `Expecting ${button.id}'s fontInternal.setNative call count`);
TKUnit.assertEqual(button.nativeBackgroundRedraws, count, `Expecting ${button.id}'s nativeBackgroundRedraws call count`);
})
}
assert(1);
const page2 = helper.navigateToModule("ui/lifecycle/pages/page-one");
helper.waitUntilLayoutReady(page2);
// NOTE: Recycling may mess this up so feel free to change the test,
// but ensure a reasonable amount of native setters were called when the views navigate away
assert(1);
}

View File

@@ -0,0 +1,3 @@
{
"main": "lifecycle-tests"
}

View File

@@ -0,0 +1,22 @@
import * as button from "tns-core-modules/ui/button";
import * as view from "tns-core-modules/ui/core/view";
export class Button extends button.Button {
nativeBackgroundRedraws = 0;
backgroundInternalSetNativeCount = 0;
fontInternalSetNativeCount = 0;
[view.backgroundInternalProperty.setNative](value) {
this.backgroundInternalSetNativeCount++;
return super[view.backgroundInternalProperty.setNative](value);
}
[view.fontInternalProperty.setNative](value) {
this.fontInternalSetNativeCount++;
return super[view.fontInternalProperty.setNative](value);
}
_redrawNativeBackground(value: any): void {
this.nativeBackgroundRedraws++;
super._redrawNativeBackground(value);
}
}
Button.prototype.recycleNativeView = false;

View File

@@ -0,0 +1,7 @@
#btn2, #btn3, #btn4 {
border-width: 2;
border-color: teal;
border-radius: 20;
font-weight: 400;
font-size: 32;
}

View File

@@ -0,0 +1,12 @@
<Page xmlns:btnCount="ui/lifecycle/pages/button-counter">
<StackLayout>
<!-- This little piggy has just attributes -->
<btnCount:Button id="btn1" borderWidth="1" borderColor="gray" borderRadius="16" fontWeight="bold" fontSize="16" />
<!-- This little piggy has just CSS -->
<btnCount:Button id="btn2" />
<!-- This little piggy has both attributes and CSS -->
<btnCount:Button id="btn3" borderWidth="1" borderColor="gray" borderRadius="16" fontWeight="bold" fontSize="16" />
<!-- This one has it all -->
<btnCount:Button id="btn4" borderRadius="3" style="background-image: url('~/logo.png'); background-position: center; background-repeat: no-repeat; background-size: cover;" />
</StackLayout>
</Page>

View File

@@ -586,6 +586,10 @@ export function test_setting_font_properties_sets_native_font() {
function test_native_font(style: "normal" | "italic", weight: "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900") {
const testView = new Button();
const page = helper.getCurrentPage();
page.content = testView;
const fontName = "Roboto";
let fontNameSuffix = "";

View File

@@ -306,7 +306,7 @@ export class TabViewTest extends testModule.UITest<tabViewModule.TabView> {
//console.log(`>>>>>>>>>>>>> CREATE 3 ITEMS`);
this.testView.items = this._createItems(1);
this.waitUntilTestElementIsLoaded();
let originalFont = tabViewTestsNative.getNativeFont(this.testView);
//console.log(`>>>>>>>>>>>>> originalFont: ${fontToString(originalFont)}`);
let nativeFont: any;

View File

@@ -449,6 +449,8 @@ export var testNativeBackgroundColorFromCss = function () {
var page = <pagesModule.Page>views[1];
page.css = "textfield { background-color: " + expectedBackgroundColorHex + "; }";
helper.waitUntilLayoutReady(textField);
var actualResult = textFieldTestsNative.getNativeBackgroundColor(textField).hex;
TKUnit.assert(actualResult === expectedNormalizedBackgroundColorHex, "Actual: " + actualResult + "; Expected: " + expectedNormalizedBackgroundColorHex);
});
@@ -459,6 +461,8 @@ export var testNativeBackgroundColorFromLocal = function () {
var textField = <textFieldModule.TextField>views[0];
textField.style.backgroundColor = new colorModule.Color(expectedBackgroundColorHex);
helper.waitUntilLayoutReady(textField);
var actualResult = textFieldTestsNative.getNativeBackgroundColor(textField).hex;
TKUnit.assert(actualResult === expectedNormalizedBackgroundColorHex, "Actual: " + actualResult + "; Expected: " + expectedNormalizedBackgroundColorHex);
});

View File

@@ -427,6 +427,8 @@ export var testNativeBackgroundColorFromCss = function () {
var page = <pagesModule.Page>views[1];
page.css = "textview { background-color: " + expectedBackgroundColorHex + "; }";
helper.waitUntilLayoutReady(textView);
var actualResult = textViewTestsNative.getNativeBackgroundColor(textView).hex;
TKUnit.assert(actualResult === expectedNormalizedBackgroundColorHex, "Actual: " + actualResult + "; Expected: " + expectedNormalizedBackgroundColorHex);
});
@@ -437,6 +439,8 @@ export var testNativeBackgroundColorFromLocal = function () {
var textView = <textViewModule.TextView>views[0];
textView.style.backgroundColor = new colorModule.Color(expectedBackgroundColorHex);
helper.waitUntilLayoutReady(textView);
var actualResult = textViewTestsNative.getNativeBackgroundColor(textView).hex;
TKUnit.assert(actualResult === expectedNormalizedBackgroundColorHex, "Actual: " + actualResult + "; Expected: " + expectedNormalizedBackgroundColorHex);
});

View File

@@ -297,7 +297,6 @@ class TestView extends Layout {
public createNativeView() {
if (isIOS) {
this.nativeView = this._nativeView;
return this._nativeView;
}
@@ -888,6 +887,7 @@ export function testSetInlineStyle() {
export function testBorderWidth() {
helper.buildUIAndRunTest(_createLabelWithBorder(), function (views: Array<View>) {
const lbl = views[0];
helper.waitUntilLayoutReady(lbl);
const expectedValue = Math.round(<number>lbl.borderWidth * utils.layout.getDisplayDensity());
const actualValue = definition.getUniformNativeBorderWidth(lbl);
TKUnit.assertAreClose(actualValue, expectedValue, 0.01, "borderWidth");
@@ -897,7 +897,7 @@ export function testBorderWidth() {
export function testCornerRadius() {
helper.buildUIAndRunTest(_createLabelWithBorder(), function (views: Array<View>) {
const lbl = views[0];
TKUnit.waitUntilReady(() => lbl.isLayoutValid);
helper.waitUntilLayoutReady(lbl);
const expectedValue = Math.round(<number>lbl.borderRadius * utils.layout.getDisplayDensity());
const actualValue = definition.getUniformNativeCornerRadius(lbl);
TKUnit.assertAreClose(actualValue, expectedValue, 0.01, "borderRadius");
@@ -907,6 +907,7 @@ export function testCornerRadius() {
export function testBorderColor() {
helper.buildUIAndRunTest(_createLabelWithBorder(), function (views: Array<View>) {
const lbl = views[0];
helper.waitUntilLayoutReady(lbl);
TKUnit.assertEqual(definition.checkUniformNativeBorderColor(lbl), true, "BorderColor not applied correctly!");
});
};
@@ -914,6 +915,7 @@ export function testBorderColor() {
export function testBackgroundColor() {
helper.buildUIAndRunTest(_createLabelWithBorder(), function (views: Array<View>) {
const lbl = views[0];
helper.waitUntilLayoutReady(lbl);
TKUnit.assertEqual(definition.checkNativeBackgroundColor(lbl), true, "BackgroundColor not applied correctly!");
});
};
@@ -970,7 +972,7 @@ export function test_getLocationRelativeToOtherView() {
a1.addChild(a2);
helper.buildUIAndRunTest(a1, function (views: Array<View>) {
TKUnit.waitUntilReady(() => a1.isLayoutValid);
helper.waitUntilLayoutReady(a1);
const labelInA2 = label.getLocationRelativeTo(a2);
const labelInA1 = label.getLocationRelativeTo(a1);
@@ -992,7 +994,7 @@ export function test_getActualSize() {
label.width = 100;
label.height = 200;
helper.buildUIAndRunTest(label, function (views: Array<View>) {
TKUnit.waitUntilReady(() => label.isLayoutValid);
helper.waitUntilLayoutReady(label);
const actualSize = label.getActualSize();
TKUnit.assertAreClose(actualSize.width, 100, delta, "actualSize.width");
TKUnit.assertAreClose(actualSize.height, 200, delta, "actualSize.height");
@@ -1003,6 +1005,6 @@ export function test_background_image_doesnt_throw() {
var btn = new Button();
btn.style.backgroundImage = 'https://www.bodybuilding.com/images/2016/june/8-benefits-to-working-out-in-the-morning-header-v2-830x467.jpg';
helper.buildUIAndRunTest(btn, function (views: Array<View>) {
TKUnit.waitUntilReady(() => btn.isLayoutValid);
helper.waitUntilLayoutReady(btn);
});
}

View File

@@ -10,15 +10,12 @@ import * as utils from "tns-core-modules/utils/utils";
global.moduleMerge(commonTests, exports);
class MyGrid extends grid.GridLayout {
public backgroundSetterCount: number = 0;
public backgroundDrawCount: number = 0;
[view.backgroundInternalProperty.getDefault](): any {
return null;
_redrawNativeBackground(background: any) {
this.backgroundDrawCount++;
super._redrawNativeBackground(background);
}
[view.backgroundInternalProperty.setNative](value: any) {
this.backgroundSetterCount ++;
}
}
export function getUniformNativeBorderWidth(v: view.View): number {
@@ -26,7 +23,7 @@ export function getUniformNativeBorderWidth(v: view.View): number {
}
export function checkUniformNativeBorderColor(v: view.View): boolean {
if (v.borderColor instanceof color.Color){
if (v.borderColor instanceof color.Color) {
return (<UIView>v.ios).layer.borderColor === (<color.Color>v.borderColor).ios.CGColor;
}
@@ -58,12 +55,12 @@ export function testBackgroundInternalChangedOnceOnResize() {
layout.className = "myClass";
layout.backgroundColor = new color.Color(255, 255, 0, 0);
root.css = ".myClass { background-image: url('~/tests/logo.png') }";
root.css = ".myClass { background-image: url('~/logo.png') }";
root.content = layout;
function trackCount() {
let result = layout.backgroundSetterCount;
layout.backgroundSetterCount = 0;
let result = layout.backgroundDrawCount;
layout.backgroundDrawCount = 0;
return result;
}
@@ -87,6 +84,7 @@ export function testBackgroundInternalChangedOnceOnResize() {
export function test_automation_text_set_to_native() {
var newButton = new button.Button();
newButton.automationText = "Button1";
helper.getCurrentPage().content = newButton;
TKUnit.assertEqual((<UIView>newButton.ios).accessibilityIdentifier, "Button1", "accessibilityIdentifier not set to native view.");
TKUnit.assertEqual((<UIView>newButton.ios).accessibilityLabel, "Button1", "accessibilityIdentifier not set to native view.");
}

View File

@@ -96,6 +96,7 @@ export class CssAnimationProperty<T extends Style, U> {
public readonly getDefault: symbol;
public readonly setNative: symbol;
public readonly key: symbol;
public readonly name: string;
public readonly cssName: string;

View File

@@ -54,92 +54,99 @@ export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<
public readonly defaultValue: U;
public readonly nativeValueChange: (owner: T, value: U) => void;
public isStyleProperty: boolean;
public get: () => U;
public set: (value: U) => void;
public enumerable: boolean = true;
public configurable: boolean = true;
constructor(options: definitions.PropertyOptions<T, U>) {
const name = options.name;
this.name = name;
const propertyName = options.name;
this.name = propertyName;
const key = Symbol(name + ":propertyKey");
const key = Symbol(propertyName + ":propertyKey");
this.key = key;
const getDefault: symbol = Symbol(name + ":getDefault");
const getDefault: symbol = Symbol(propertyName + ":getDefault");
this.getDefault = getDefault;
const setNative: symbol = Symbol(name + ":setNative");
const setNative: symbol = Symbol(propertyName + ":setNative");
this.setNative = setNative;
const defaultValueKey = Symbol(name + ":nativeDefaultValue");
const defaultValueKey = Symbol(propertyName + ":nativeDefaultValue");
this.defaultValueKey = defaultValueKey;
const defaultValue: U = options.defaultValue;
this.defaultValue = defaultValue;
const eventName = name + "Change";
const eventName = propertyName + "Change";
const equalityComparer = options.equalityComparer;
const affectsLayout: boolean = options.affectsLayout;
const valueChanged = options.valueChanged;
const valueConverter = options.valueConverter;
this.set = function (this: T, value: U): void {
const reset = value === unsetValue;
let unboxedValue: U;
const property = this;
this.set = function (this: T, boxedValue: U): void {
const reset = boxedValue === unsetValue;
let value: U;
let wrapped: boolean;
if (reset) {
unboxedValue = defaultValue;
value = defaultValue;
} else {
wrapped = value && (<any>value).wrapped;
unboxedValue = wrapped ? WrappedValue.unwrap(value) : value;
wrapped = boxedValue && (<any>boxedValue).wrapped;
value = wrapped ? WrappedValue.unwrap(boxedValue) : boxedValue;
if (valueConverter && typeof unboxedValue === "string") {
unboxedValue = valueConverter(unboxedValue);
if (valueConverter && typeof value === "string") {
value = valueConverter(value);
}
}
const currentValue = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(currentValue, unboxedValue) : currentValue !== unboxedValue;
const oldValue = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (wrapped || changed) {
const setNativeValue = this.nativeView && this[setNative];
if (reset) {
delete this[key];
if (valueChanged) {
valueChanged(this, currentValue, unboxedValue);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (defaultValueKey in this) {
this[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
if (this[setNative]) {
if (this._suspendNativeUpdatesCount) {
if (this._suspendedUpdates) {
this._suspendedUpdates[propertyName] = property;
}
} else {
this[setNative](defaultValue);
if (defaultValueKey in this) {
this[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
} else {
this[setNative](defaultValue);
}
}
}
} else {
this[key] = unboxedValue;
this[key] = value;
if (valueChanged) {
valueChanged(this, currentValue, unboxedValue);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = this[getDefault] ? this[getDefault]() : defaultValue;
if (this[setNative]) {
if (this._suspendNativeUpdatesCount) {
if (this._suspendedUpdates) {
this._suspendedUpdates[propertyName] = property;
}
} else {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = this[getDefault] ? this[getDefault]() : defaultValue;
}
this[setNative](value);
}
this[setNative](unboxedValue);
}
}
if (this.hasListeners(eventName)) {
this.notify<PropertyChangeData>({
eventName: eventName,
propertyName: name,
object: this,
value: unboxedValue,
oldValue: currentValue
});
this.notify<PropertyChangeData>({ object: this, eventName, propertyName, value, oldValue });
}
if (affectsLayout) {
@@ -153,12 +160,12 @@ export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<
};
this.nativeValueChange = function (owner: T, value: U): void {
const currentValue = key in owner ? owner[key] : defaultValue;
const changed = equalityComparer ? !equalityComparer(currentValue, value) : currentValue !== value;
const oldValue = key in owner ? owner[key] : defaultValue;
const changed = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (changed) {
owner[key] = value;
if (valueChanged) {
valueChanged(owner, currentValue, value);
valueChanged(owner, oldValue, value);
}
if (owner.nativeView && !(defaultValueKey in owner)) {
@@ -166,13 +173,7 @@ export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<
}
if (owner.hasListeners(eventName)) {
owner.notify<PropertyChangeData>({
eventName: eventName,
propertyName: name,
object: owner,
value: value,
oldValue: currentValue
});
owner.notify<PropertyChangeData>({ object: owner, eventName, propertyName, value, oldValue });
}
if (affectsLayout) {
@@ -196,6 +197,7 @@ export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<
return this.key in instance;
}
}
Property.prototype.isStyleProperty = false;
export class CoercibleProperty<T extends ViewBase, U> extends Property<T, U> implements definitions.CoercibleProperty<T, U> {
public readonly coerce: (target: T) => void;
@@ -203,89 +205,95 @@ export class CoercibleProperty<T extends ViewBase, U> extends Property<T, U> imp
constructor(options: definitions.CoerciblePropertyOptions<T, U>) {
super(options);
const name = options.name;
const propertyName = options.name;
const key = this.key;
const getDefault: symbol = this.getDefault;
const setNative: symbol = this.setNative;
const defaultValueKey = this.defaultValueKey;
const defaultValue: U = this.defaultValue;
const coerceKey = Symbol(name + ":coerceKey");
const coerceKey = Symbol(propertyName + ":coerceKey");
const eventName = name + "Change";
const eventName = propertyName + "Change";
const affectsLayout: boolean = options.affectsLayout;
const equalityComparer = options.equalityComparer;
const valueChanged = options.valueChanged;
const valueConverter = options.valueConverter;
const coerceCallback = options.coerceValue;
const property = this;
this.coerce = function (target: T): void {
const originalValue: U = coerceKey in target ? target[coerceKey] : defaultValue;
// need that to make coercing but also fire change events
target[name] = originalValue;
target[propertyName] = originalValue;
}
this.set = function (this: T, value: U): void {
const reset = value === unsetValue;
let unboxedValue: U;
this.set = function (this: T, boxedValue: U): void {
const reset = boxedValue === unsetValue;
let value: U;
let wrapped: boolean;
if (reset) {
unboxedValue = defaultValue;
value = defaultValue;
delete this[coerceKey];
} else {
wrapped = value && (<any>value).wrapped;
unboxedValue = wrapped ? WrappedValue.unwrap(value) : value;
wrapped = boxedValue && (<any>boxedValue).wrapped;
value = wrapped ? WrappedValue.unwrap(boxedValue) : boxedValue;
if (valueConverter && typeof unboxedValue === "string") {
unboxedValue = valueConverter(unboxedValue);
if (valueConverter && typeof value === "string") {
value = valueConverter(value);
}
this[coerceKey] = unboxedValue;
unboxedValue = coerceCallback(this, unboxedValue);
this[coerceKey] = value;
value = coerceCallback(this, value);
}
const currentValue = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(currentValue, unboxedValue) : currentValue !== unboxedValue;
const oldValue = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (wrapped || changed) {
const setNativeValue = this.nativeView && this[setNative];
if (reset) {
delete this[key];
if (valueChanged) {
valueChanged(this, currentValue, unboxedValue);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (defaultValueKey in this) {
this[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
if (this[setNative]) {
if (this._suspendNativeUpdatesCount) {
if (this._suspendedUpdates) {
this._suspendedUpdates[propertyName] = property;
}
} else {
this[setNative](defaultValue);
if (defaultValueKey in this) {
this[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
} else {
this[setNative](defaultValue);
}
}
}
} else {
this[key] = unboxedValue;
this[key] = value;
if (valueChanged) {
valueChanged(this, currentValue, unboxedValue);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = this[getDefault] ? this[getDefault]() : defaultValue;
if (this[setNative]) {
if (this._suspendNativeUpdatesCount) {
if (this._suspendedUpdates) {
this._suspendedUpdates[propertyName] = property;
}
} else {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = this[getDefault] ? this[getDefault]() : defaultValue;
}
this[setNative](value);
}
this[setNative](unboxedValue);
}
}
if (this.hasListeners(eventName)) {
this.notify<PropertyChangeData>({
eventName: eventName,
propertyName: name,
object: this,
value: unboxedValue,
oldValue: currentValue
});
this.notify<PropertyChangeData>({ object: this, eventName, propertyName, value, oldValue });
}
if (affectsLayout) {
@@ -378,6 +386,8 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
protected readonly cssValueDescriptor: PropertyDescriptor;
protected readonly localValueDescriptor: PropertyDescriptor;
public isStyleProperty: boolean;
public readonly key: symbol;
public readonly getDefault: symbol;
public readonly setNative: symbol;
@@ -386,36 +396,38 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
public readonly defaultValue: U;
constructor(options: definitions.CssPropertyOptions<T, U>) {
const name = options.name;
this.name = name;
const propertyName = options.name;
this.name = propertyName;
this.cssName = `css:${options.cssName}`;
this.cssLocalName = options.cssName;
const key = Symbol(name + ":propertyKey");
const key = Symbol(propertyName + ":propertyKey");
this.key = key;
const sourceKey = Symbol(name + ":valueSourceKey");
const sourceKey = Symbol(propertyName + ":valueSourceKey");
this.sourceKey = sourceKey;
const getDefault = Symbol(name + ":getDefault");
const getDefault = Symbol(propertyName + ":getDefault");
this.getDefault = getDefault;
const setNative = Symbol(name + ":setNative");
const setNative = Symbol(propertyName + ":setNative");
this.setNative = setNative;
const defaultValueKey = Symbol(name + ":nativeDefaultValue");
const defaultValueKey = Symbol(propertyName + ":nativeDefaultValue");
this.defaultValueKey = defaultValueKey;
const defaultValue: U = options.defaultValue;
this.defaultValue = defaultValue;
const eventName = name + "Change";
const eventName = propertyName + "Change";
const affectsLayout: boolean = options.affectsLayout;
const equalityComparer = options.equalityComparer;
const valueChanged = options.valueChanged;
const valueConverter = options.valueConverter;
const property = this;
function setLocalValue(this: T, value: U): void {
const reset = value === unsetValue;
if (reset) {
@@ -429,49 +441,53 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
}
}
const currentValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(currentValue, value) : currentValue !== value;
const oldValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (changed) {
const view = this.view;
const setNativeValue = view.nativeView && view[setNative];
if (reset) {
delete this[key];
if (valueChanged) {
valueChanged(this, currentValue, value);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (defaultValueKey in this) {
view[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
view[setNative](defaultValue);
if (defaultValueKey in this) {
view[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
} else {
view[setNative](defaultValue);
}
}
}
} else {
this[key] = value;
if (valueChanged) {
valueChanged(this, currentValue, value);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
}
view[setNative](value);
}
view[setNative](value);
}
}
if (this.hasListeners(eventName)) {
this.notify<PropertyChangeData>({
eventName: eventName,
propertyName: name,
object: this,
value: value,
oldValue: currentValue
});
this.notify<PropertyChangeData>({ object: this, eventName, propertyName, value, oldValue });
}
if (affectsLayout) {
@@ -499,49 +515,53 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
this[sourceKey] = ValueSource.Css;
}
const currentValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(currentValue, value) : currentValue !== value;
const oldValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (changed) {
const view = this.view;
const setNativeValue = view.nativeView && view[setNative];
if (reset) {
delete this[key];
if (valueChanged) {
valueChanged(this, currentValue, value);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (defaultValueKey in this) {
view[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
view[setNative](defaultValue);
if (defaultValueKey in this) {
view[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
} else {
view[setNative](defaultValue);
}
}
}
} else {
this[key] = value;
if (valueChanged) {
valueChanged(this, currentValue, value);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
}
view[setNative](value);
}
view[setNative](value);
}
}
if (this.hasListeners(eventName)) {
this.notify<PropertyChangeData>({
eventName: eventName,
propertyName: name,
object: this,
value: value,
oldValue: currentValue
});
this.notify<PropertyChangeData>({ object: this, eventName, propertyName, value, oldValue });
}
if (affectsLayout) {
@@ -587,6 +607,7 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
return this.key in instance;
}
}
CssProperty.prototype.isStyleProperty = true;
export class CssAnimationProperty<T extends Style, U> {
public readonly name: string;
@@ -599,10 +620,12 @@ export class CssAnimationProperty<T extends Style, U> {
public readonly keyframe: string;
public readonly defaultValueKey: symbol;
public readonly computedValueKey: symbol;
public readonly key: symbol;
public readonly defaultValue: U;
public isStyleProperty: boolean;
private static properties: { [cssName: string]: CssAnimationProperty<any, any> } = {};
public _valueConverter?: (value: string) => any;
@@ -635,7 +658,7 @@ export class CssAnimationProperty<T extends Style, U> {
const styleValue = Symbol(propertyName);
const keyframeValue = Symbol(keyframeName);
const computedValue = Symbol("computed-value:" + propertyName);
this.computedValueKey = computedValue;
this.key = computedValue;
const computedSource = Symbol("computed-source:" + propertyName);
this.getDefault = Symbol(propertyName + ":getDefault");
@@ -643,13 +666,15 @@ export class CssAnimationProperty<T extends Style, U> {
const setNative = this.setNative = Symbol(propertyName + ":setNative");
const eventName = propertyName + "Change";
const property = this;
function descriptor(symbol: symbol, propertySource: ValueSource, enumerable: boolean, configurable: boolean, getsComputed: boolean): PropertyDescriptor {
return {
enumerable, configurable,
get: getsComputed ? function (this: T) { return this[computedValue]; } : function (this: T) { return this[symbol]; },
set(this: T, value: U) {
let prev = this[computedValue];
if (value === unsetValue) {
set(this: T, boxedValue: U) {
let oldValue = this[computedValue];
if (boxedValue === unsetValue) {
this[symbol] = unsetValue;
if (this[computedSource] === propertySource) {
// Fallback to lower value source.
@@ -665,30 +690,37 @@ export class CssAnimationProperty<T extends Style, U> {
}
}
} else {
if (valueConverter && typeof value === "string") {
value = valueConverter(value);
if (valueConverter && typeof boxedValue === "string") {
boxedValue = valueConverter(boxedValue);
}
this[symbol] = value;
this[symbol] = boxedValue;
if (this[computedSource] <= propertySource) {
this[computedSource] = propertySource;
this[computedValue] = value;
this[computedValue] = boxedValue;
}
}
let next = this[computedValue];
if (prev !== next && (!equalityComparer || !equalityComparer(prev, next))) {
let value = this[computedValue];
if (oldValue !== value && (!equalityComparer || !equalityComparer(oldValue, value))) {
if (valueChanged) {
valueChanged(this, prev, next);
valueChanged(this, oldValue, value);
}
const view = this.view;
if (view.nativeView && view[setNative]) {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
}
view[setNative](next);
const view = this.view;
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
}
view[setNative](value);
}
}
if (this.hasListeners(eventName)) {
this.notify<PropertyChangeData>({ eventName, object: this, propertyName, value, oldValue: prev });
this.notify<PropertyChangeData>({ object: this, eventName, propertyName, value, oldValue });
}
}
}
@@ -726,16 +758,17 @@ export class CssAnimationProperty<T extends Style, U> {
}
public isSet(instance: T): boolean {
return instance[this.computedValueKey] !== unsetValue;
return instance[this.key] !== unsetValue;
}
}
CssAnimationProperty.prototype.isStyleProperty = true;
export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U> implements definitions.InheritedCssProperty<T, U> {
public setInheritedValue: (value: U) => void;
constructor(options: definitions.CssPropertyOptions<T, U>) {
super(options);
const name = options.name;
const propertyName = options.name;
const key = this.key;
const sourceKey = this.sourceKey;
@@ -743,15 +776,17 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
const setNative = this.setNative;
const defaultValueKey = this.defaultValueKey;
const eventName = name + "Change";
const eventName = propertyName + "Change";
const defaultValue: U = options.defaultValue;
const affectsLayout: boolean = options.affectsLayout;
const equalityComparer = options.equalityComparer;
const valueChanged = options.valueChanged;
const valueConverter = options.valueConverter;
const setFunc = (valueSource: ValueSource) => function (this: T, value: any): void {
const reset = value === unsetValue;
const property = this;
const setFunc = (valueSource: ValueSource) => function (this: T, boxedValue: any): void {
const reset = boxedValue === unsetValue;
const currentValueSource: number = this[sourceKey] || ValueSource.Default;
if (reset) {
// If we want to reset cssValue and we have localValue - return;
@@ -765,71 +800,76 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
}
const view = this.view;
let newValue: U;
let value: U;
if (reset) {
// If unsetValue - we want to reset this property.
let parent = view.parent;
let style = parent ? parent.style : null;
// If we have parent and it has non-default value we use as our inherited value.
if (style && style[sourceKey] > ValueSource.Default) {
newValue = style[name];
value = style[propertyName];
this[sourceKey] = ValueSource.Inherited;
} else {
newValue = defaultValue;
value = defaultValue;
delete this[sourceKey];
}
} else {
this[sourceKey] = valueSource;
if (valueConverter && typeof value === "string") {
newValue = valueConverter(value);
if (valueConverter && typeof boxedValue === "string") {
value = valueConverter(boxedValue);
} else {
newValue = value;
value = boxedValue;
}
}
const currentValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(currentValue, newValue) : currentValue !== newValue;
const oldValue: U = key in this ? this[key] : defaultValue;
const changed: boolean = equalityComparer ? !equalityComparer(oldValue, value) : oldValue !== value;
if (changed) {
const view = this.view;
const setNativeValue = view.nativeView && view[setNative];
if (reset) {
delete this[key];
if (valueChanged) {
valueChanged(this, currentValue, newValue);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (defaultValueKey in this) {
view[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
view[setNative](defaultValue);
if (defaultValueKey in this) {
view[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
} else {
view[setNative](defaultValue);
}
}
}
} else {
this[key] = newValue;
this[key] = value;
if (valueChanged) {
valueChanged(this, currentValue, newValue);
valueChanged(this, oldValue, value);
}
if (setNativeValue) {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
}
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
if (!(defaultValueKey in this)) {
this[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
}
view[setNative](newValue);
view[setNative](value);
}
}
}
if (this.hasListeners(eventName)) {
this.notify<PropertyChangeData>({
eventName: eventName,
propertyName: name,
object: this,
value: newValue,
oldValue: currentValue
});
this.notify<PropertyChangeData>({ object: this, eventName, propertyName, value, oldValue });
}
if (affectsLayout) {
@@ -845,7 +885,7 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
}
} else {
if (childValueSource <= ValueSource.Inherited) {
setInheritedFunc.call(childStyle, newValue);
setInheritedFunc.call(childStyle, value);
}
}
return true;
@@ -891,18 +931,22 @@ export class ShorthandProperty<T extends Style, P> implements definitions.Shorth
function setLocalValue(this: T, value: string | P): void {
if (this[key] !== value) {
this[key] = value;
for (let [p, v] of converter(value)) {
this[p.name] = v;
}
this.view._batchUpdate(() => {
for (let [p, v] of converter(value)) {
this[p.name] = v;
}
});
}
}
function setCssValue(this: T, value: string): void {
if (this[key] !== value) {
this[key] = value;
for (let [p, v] of converter(value)) {
this[p.cssName] = v;
}
this.view._batchUpdate(() => {
for (let [p, v] of converter(value)) {
this[p.cssName] = v;
}
});
}
}
@@ -943,7 +987,7 @@ function inheritablePropertyValuesOn(view: ViewBase): Array<{ property: Inherite
const valueSource: number = view[sourceKey] || ValueSource.Default;
if (valueSource !== ValueSource.Default) {
// use prop.name as it will return value or default value.
// prop.key will return undefined if property is set t the same value as default one.
// prop.key will return undefined if property is set the same value as default one.
array.push({ property: prop, value: view[prop.name] });
}
}
@@ -958,7 +1002,7 @@ function inheritableCssPropertyValuesOn(style: Style): Array<{ property: Inherit
const valueSource: number = style[sourceKey] || ValueSource.Default;
if (valueSource !== ValueSource.Default) {
// use prop.name as it will return value or default value.
// prop.key will return undefined if property is set t the same value as default one.
// prop.key will return undefined if property is set the same value as default one.
array.push({ property: prop, value: style[prop.name] });
}
}
@@ -966,7 +1010,54 @@ function inheritableCssPropertyValuesOn(style: Style): Array<{ property: Inherit
return array;
}
type PropertyInterface = Property<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any>;
export const initNativeView = profile('"properties".initNativeView', function initNativeView(view: ViewBase): void {
if (view._suspendedUpdates) {
applyPendingNativeSetters(view);
} else {
applyAllNativeSetters(view);
}
// Would it be faster to delete all members of the old object?
view._suspendedUpdates = {};
});
export function applyPendingNativeSetters(view: ViewBase): void {
// TODO: Check what happens if a view was suspended and its value was reset, or set back to default!
const suspendedUpdates = view._suspendedUpdates;
for (var propertyName in suspendedUpdates) {
const property = <PropertyInterface>suspendedUpdates[propertyName];
const setNative = property.setNative;
if (view[setNative]) {
const { getDefault, isStyleProperty, defaultValueKey, defaultValue } = property;
let value;
if (isStyleProperty) {
const style = view.style;
if ((<CssProperty<Style, any> | CssAnimationProperty<Style, any>>property).isSet(view.style)) {
if (!(defaultValueKey in style)) {
style[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
}
value = view.style[propertyName];
} else {
value = style[defaultValueKey];
}
} else {
if ((<Property<ViewBase, any>>property).isSet(view)) {
if (!(defaultValueKey in view)) {
view[defaultValueKey] = view[getDefault] ? view[getDefault]() : defaultValue;
}
value = view[propertyName];
} else {
value = view[defaultValueKey];
}
}
// TODO: Only if value is different from the value before the scope was created.
view[setNative](value);
}
}
}
export function applyAllNativeSetters(view: ViewBase): void {
let symbols = Object.getOwnPropertySymbols(view);
for (let symbol of symbols) {
const property: Property<any, any> = symbolPropertyMap[symbol];
@@ -1007,7 +1098,7 @@ export const initNativeView = profile('"properties".initNativeView', function in
view[property.setNative](value);
}
}
});
}
export function resetNativeView(view: ViewBase): void {
let symbols = Object.getOwnPropertySymbols(view);

View File

@@ -2,7 +2,7 @@
* @module "ui/core/view-base"
*/ /** */
import { Property, InheritedProperty, Style } from "../properties";
import { Property, CssProperty, CssAnimationProperty, InheritedProperty, Style } from "../properties";
import { BindingOptions, Observable } from "../bindable";
import { SelectorCore } from "../../styling/css-selector";
@@ -97,6 +97,15 @@ export abstract class ViewBase extends Observable {
* @private
*/
_defaultPaddingLeft: number;
/**
* A property bag holding suspended native updates.
* Native setters that had to execute while there was no native view,
* or the view was detached from the visual tree etc. will accumulate in this object,
* and will be applied when all prerequisites are met.
* @private
*/
_suspendedUpdates: { [propertyName: string]: Property<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any> };
//@endprivate
public effectiveMinWidth: number;
@@ -128,7 +137,12 @@ export abstract class ViewBase extends Observable {
public ios: any;
public android: any;
/**
* read-only. If you want to set out-of-band the nativeView use the setNativeView method.
*/
public nativeView: any;
public bindingContext: any;
public recycleNativeView: boolean;
@@ -181,6 +195,7 @@ export abstract class ViewBase extends Observable {
public onLoaded(): void;
public onUnloaded(): void;
public onResumeNativeUpdates(): void;
public bind(options: BindingOptions, source?: Object): void;
public unbind(property: string): void;
@@ -251,6 +266,13 @@ export abstract class ViewBase extends Observable {
*/
resetNativeView(): void;
/**
* Set the nativeView field performing extra checks and updates to the native properties on the new view.
* Use in cases where the createNativeView is not suitable.
* As an example use in item controls where the native parent view will create the native views for child items.
*/
setNativeView(view: any): void;
_isAddedToNativeVisualTree: boolean;
/**
@@ -278,12 +300,13 @@ export abstract class ViewBase extends Observable {
public _styleScope: any;
/**
* Determines the depth of batchUpdates.
* When the value is 0 the current updates are not batched.
* If the value is 1 or greater, the current updates are batched.
* Do not set this field, the _batchUpdate method is responsible to keep the count up to date.
* Determines the depth of suspended updates.
* When the value is 0 the current property updates are not batched nor scoped and must be immediately applied.
* If the value is 1 or greater, the current updates are batched and does not have to provide immediate update.
* Do not set this field, the _batchUpdate method is responsible to keep the count up to date,
* as well as adding/rmoving the view to/from the visual tree.
*/
public _batchUpdateScope: number;
public _suspendNativeUpdatesCount: number;
/**
* Allow multiple updates to be performed on the instance at once.

View File

@@ -6,7 +6,7 @@ import { Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from "../../la
import { KeyframeAnimation } from "../../animation/keyframe-animation";
// Types.
import { Property, InheritedProperty, Style, clearInheritedProperties, propagateInheritableProperties, propagateInheritableCssProperties, resetCSSProperties, initNativeView, resetNativeView } from "../properties";
import { Property, CssProperty, CssAnimationProperty, InheritedProperty, Style, clearInheritedProperties, propagateInheritableProperties, propagateInheritableCssProperties, resetCSSProperties, initNativeView, resetNativeView } from "../properties";
import { Binding, BindingOptions, Observable, WrappedValue, PropertyChangeData, traceEnabled, traceWrite, traceCategories, traceNotifyEvent } from "../bindable";
import { isIOS, isAndroid } from "../../../platform";
import { layout } from "../../../utils/utils";
@@ -141,6 +141,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
private _registeredAnimations: Array<KeyframeAnimation>;
private _visualState: string;
private _inlineStyleSelector: SelectorCore;
private __nativeView: any;
public bindingContext: any;
public nativeView: any;
@@ -155,6 +156,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public _isAddedToNativeVisualTree: boolean;
public _cssState: ssm.CssState;
public _styleScope: ssm.StyleScope;
public _suspendedUpdates: { [propertyName: string]: Property<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any> };
public _suspendNativeUpdatesCount: number;
// Dynamic properties.
left: Length;
@@ -265,6 +268,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
@profile
public onLoaded() {
this._isLoaded = true;
this._resumeNativeUpdates();
this._loadEachChild();
this._emit("loaded");
}
@@ -278,18 +282,27 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
@profile
public onUnloaded() {
this._suspendNativeUpdates();
this._unloadEachChild();
this._isLoaded = false;
this._emit("unloaded");
}
public _batchUpdateScope: number;
public _suspendNativeUpdates(): void {
this._suspendNativeUpdatesCount++;
}
public _resumeNativeUpdates(): void {
this._suspendNativeUpdatesCount--;
if (!this._suspendNativeUpdatesCount) {
this.onResumeNativeUpdates();
}
}
public _batchUpdate<T>(callback: () => T): T {
try {
++this._batchUpdateScope;
this._suspendNativeUpdates();
return callback();
} finally {
--this._batchUpdateScope;
this._resumeNativeUpdates();
}
}
@@ -638,6 +651,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
@profile
public _setupUI(context: android.content.Context, atIndex?: number, parentIsLoaded?: boolean) {
traceNotifyEvent(this, "_setupUI");
if (traceEnabled()) {
traceWrite(`${this}._setupUI(${context})`, traceCategories.VisualTreeEvents);
@@ -650,9 +664,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this._context = context;
traceNotifyEvent(this, "_onContextChanged");
let currentNativeView = this.nativeView;
let nativeView;
if (isAndroid) {
let nativeView: android.view.View;
if (this.recycleNativeView) {
nativeView = <android.view.View>getNativeView(context, this.typeName);
}
@@ -661,7 +674,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
nativeView = <android.view.View>this.createNativeView();
}
this._androidView = this.nativeView = nativeView;
this._androidView = nativeView;
if (nativeView) {
let result: android.graphics.Rect = (<any>nativeView).defaultPaddings;
if (result === undefined) {
@@ -690,24 +703,22 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
} else {
// TODO: Implement _createNativeView for iOS
const nativeView = this.createNativeView();
if (!currentNativeView && nativeView) {
this.nativeView = this._iosView = nativeView;
nativeView = this.createNativeView();
if (nativeView) {
this._iosView = nativeView;
}
}
this.initNativeView();
// This will account for nativeView that is created in createNativeView, recycled
// or for backward compatability - set before _setupUI in iOS contructor.
this.setNativeView(nativeView || this.nativeView);
if (this.parent) {
let nativeIndex = this.parent._childIndexToNativeChildIndex(atIndex);
this._isAddedToNativeVisualTree = this.parent._addViewToNativeVisualTree(this, nativeIndex);
}
if (this.nativeView) {
if (currentNativeView !== this.nativeView) {
initNativeView(this);
}
}
this._resumeNativeUpdates();
this.eachChild((child) => {
child._setupUI(context);
@@ -715,6 +726,23 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
});
}
setNativeView(value: any): void {
if (this.__nativeView === value) {
return;
}
if (this.__nativeView) {
this._suspendNativeUpdates();
// We may do a `this.resetNativeView()` here?
}
this.__nativeView = this.nativeView = value;
if (this.__nativeView) {
this._suspendedUpdates = undefined;
this.initNativeView();
this._resumeNativeUpdates();
}
}
@profile
public _tearDownUI(force?: boolean) {
// No context means we are already teared down.
@@ -747,8 +775,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.disposeNativeView();
this._suspendNativeUpdates();
if (isAndroid) {
this.nativeView = null;
this.setNativeView(null);
this._androidView = null;
}
@@ -813,6 +843,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
public _parentChanged(oldParent: ViewBase): void {
const newParent = this.parent;
//Overridden
if (oldParent) {
clearInheritedProperties(this);
@@ -820,12 +852,16 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
oldParent.off("bindingContextChange", this.bindingContextChanged, this);
}
} else if (this.shouldAddHandlerToParentBindingContextChanged) {
const parent = this.parent;
parent.on("bindingContextChange", this.bindingContextChanged, this);
this.bindings.get("bindingContext").bind(parent.bindingContext);
newParent.on("bindingContextChange", this.bindingContextChanged, this);
this.bindings.get("bindingContext").bind(newParent.bindingContext);
}
}
public onResumeNativeUpdates(): void {
// Apply native setters...
initNativeView(this);
}
public _registerAnimation(animation: KeyframeAnimation) {
if (this._registeredAnimations === undefined) {
this._registeredAnimations = new Array<KeyframeAnimation>();
@@ -880,7 +916,11 @@ ViewBase.prototype._defaultPaddingBottom = 0;
ViewBase.prototype._defaultPaddingLeft = 0;
ViewBase.prototype._isViewBase = true;
ViewBase.prototype._batchUpdateScope = 0;
// Removing from visual tree does +1, adding to visual tree does -1, see parentChanged
// Removing the nativeView does +1, adding the nativeView does -1, see set nativeView
// Pre _setupUI and post _tearDownUI does +1, in between _setupUI and _tearDownUI is -1
// Initially launch with 2, native updates wont fire unless both added to the JS visual tree and got nativeView.
ViewBase.prototype._suspendNativeUpdatesCount = 3;
export const bindingContextProperty = new InheritedProperty<ViewBase, any>({ name: "bindingContext" });
bindingContextProperty.register(ViewBase);

View File

@@ -770,6 +770,10 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public _setNativeClipToBounds() {
//
}
public _redrawNativeBackground(value: any): void {
//
}
}
export const automationTextProperty = new Property<ViewCommon, string>({ name: "automationText" });

View File

@@ -429,11 +429,7 @@ export class View extends ViewCommon {
return this.nativeView.getBackground();
}
[backgroundInternalProperty.setNative](value: android.graphics.drawable.Drawable | Background) {
if (value instanceof android.graphics.drawable.Drawable) {
this.nativeView.setBackground(value);
} else {
androidBackground.onBackgroundOrBorderPropertyChanged(this);
}
this._redrawNativeBackground(value);
}
[minWidthProperty.setNative](value: Length) {
@@ -451,6 +447,14 @@ export class View extends ViewCommon {
this._setMinHeightNative(this.minHeight);
}
}
_redrawNativeBackground(value: android.graphics.drawable.Drawable | Background): void {
if (value instanceof android.graphics.drawable.Drawable) {
this.nativeView.setBackground(value);
} else {
androidBackground.onBackgroundOrBorderPropertyChanged(this);
}
}
}
export class CustomLayoutView extends View implements CustomLayoutViewDefinition {

View File

@@ -554,6 +554,10 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
* @private
*/
_setMinHeightNative(value: Length): void;
/**
* @private
*/
_redrawNativeBackground(value: any): void;
//@endprivate
/**

View File

@@ -27,6 +27,13 @@ export class View extends ViewCommon {
private _privateFlags: number = PFLAG_LAYOUT_REQUIRED | PFLAG_FORCE_LAYOUT;
private _cachedFrame: CGRect;
private _suspendCATransaction = false;
/**
* Native background states.
* - `unset` - is the default, from this state it transitions to "invalid" in the base backgroundInternalProperty.setNative, overriding it without calling `super` will prevent the background from ever being drawn.
* - `invalid` - the view background must be redrawn on the next layot.
* - `drawn` - the view background has been property drawn, on subsequent layouts it may need to be redrawn if the background depends on the view's size.
*/
_nativeBackgroundState: "unset" | "invalid" | "drawn";
// get nativeView(): UIView {
// return this.ios;
@@ -91,6 +98,9 @@ export class View extends ViewCommon {
if (sizeChanged) {
this._onSizeChanged();
} else if (this._nativeBackgroundState === "invalid") {
let background = this.style.backgroundInternal;
this._redrawNativeBackground(background);
}
this._privateFlags &= ~PFLAG_FORCE_LAYOUT;
@@ -210,7 +220,7 @@ export class View extends ViewCommon {
let myPointInWindow = this.nativeView.convertPointToView(this.nativeView.bounds.origin, null);
let otherPointInWindow = otherView.nativeView.convertPointToView(otherView.nativeView.bounds.origin, null);
return {
return {
x: myPointInWindow.x - otherPointInWindow.x,
y: myPointInWindow.y - otherPointInWindow.y
};
@@ -223,8 +233,10 @@ export class View extends ViewCommon {
}
let background = this.style.backgroundInternal;
if (!background.isEmpty() && this[backgroundInternalProperty.setNative]) {
this[backgroundInternalProperty.setNative](background);
const backgroundDependsOnSize = background.image || !background.hasUniformBorder();
if (this._nativeBackgroundState === "invalid" || (this._nativeBackgroundState === "drawn" && backgroundDependsOnSize)) {
this._redrawNativeBackground(background);
}
let clipPath = this.style.clipPath;
@@ -274,7 +286,7 @@ export class View extends ViewCommon {
}
public _isPresentationLayerUpdateSuspeneded() {
return this._suspendCATransaction || this._batchUpdateScope;
return this._suspendCATransaction || this._suspendNativeUpdatesCount;
}
[isEnabledProperty.getDefault](): boolean {
@@ -395,6 +407,13 @@ export class View extends ViewCommon {
return this.nativeView.backgroundColor;
}
[backgroundInternalProperty.setNative](value: UIColor | Background) {
this._nativeBackgroundState = "invalid";
if (this.isLayoutValid) {
this._redrawNativeBackground(value);
}
}
_redrawNativeBackground(value: UIColor | Background): void {
let updateSuspended = this._isPresentationLayerUpdateSuspeneded();
if (!updateSuspended) {
CATransaction.begin();
@@ -412,6 +431,8 @@ export class View extends ViewCommon {
if (!updateSuspended) {
CATransaction.commit();
}
this._nativeBackgroundState = "drawn";
}
_setNativeClipToBounds() {
@@ -419,6 +440,7 @@ export class View extends ViewCommon {
this.nativeView.clipsToBounds = backgroundInternal.hasBorderWidth() || backgroundInternal.hasBorderRadius();
}
}
View.prototype._nativeBackgroundState = "unset";
export class CustomLayoutView extends View {

View File

@@ -1,7 +1,7 @@
import { Label as LabelDefinition } from ".";
import { Background } from "../styling/background";
import {
TextBase, View, layout, backgroundInternalProperty,
TextBase, View, layout,
borderTopWidthProperty, borderRightWidthProperty, borderBottomWidthProperty, borderLeftWidthProperty,
paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty, whiteSpaceProperty,
Length, WhiteSpace
@@ -95,10 +95,7 @@ export class Label extends TextBase implements LabelDefinition {
}
}
[backgroundInternalProperty.getDefault](): any /* CGColor */ {
return this.nativeView.layer.backgroundColor;
}
[backgroundInternalProperty.setNative](value: Background) {
_redrawNativeBackground(value: UIColor | Background): void {
if (value instanceof Background) {
ios.createBackgroundUIColor(this, (color: UIColor) => {
const cgColor = color ? color.CGColor : null;

View File

@@ -15,8 +15,6 @@ export class ListPicker extends ListPickerBase {
this.nativeView = this._ios = UIPickerView.new();
this._ios.dataSource = this._dataSource = ListPickerDataSource.initWithOwner(new WeakRef(this));
this._delegate = ListPickerDelegateImpl.initWithOwner(new WeakRef(this));
this.nativeView = this._ios;
}
@profile

View File

@@ -285,8 +285,10 @@ export class PageBase extends ContentView implements PageDefinition {
private _resetCssValues() {
const resetCssValuesFunc = (view: View): boolean => {
view._cancelAllAnimations();
resetCSSProperties(view.style);
view._batchUpdate(() => {
view._cancelAllAnimations();
resetCSSProperties(view.style);
});
return true;
};

View File

@@ -13,7 +13,7 @@ export class ProxyViewContainer extends LayoutBase implements ProxyViewContainer
constructor() {
super();
this.nativeView = undefined;
this.nativeView = undefined;
}
// No native view for proxy container.

View File

@@ -1,7 +1,7 @@
import { Font } from "../styling/font";
import {
SegmentedBarItemBase, SegmentedBarBase, selectedIndexProperty, itemsProperty, selectedBackgroundColorProperty,
colorProperty, fontInternalProperty, fontSizeProperty, Color, initNativeView, layout
colorProperty, fontInternalProperty, fontSizeProperty, Color, layout
} from "./segmented-bar-common";
export * from "./segmented-bar-common";
@@ -92,18 +92,13 @@ function initializeNativeClasses(): void {
export class SegmentedBarItem extends SegmentedBarItemBase {
nativeView: android.widget.TextView;
public createNativeView(): android.widget.TextView {
return this.nativeView;
}
public setupNativeView(tabIndex: number): void {
// TabHost.TabSpec.setIndicator DOES NOT WORK once the title has been set.
// http://stackoverflow.com/questions/2935781/modify-tab-indicator-dynamically-in-android
const titleTextView = <android.widget.TextView>this.parent.nativeView.getTabWidget().getChildAt(tabIndex).findViewById(TITLE_TEXT_VIEW_ID);
this.nativeView = titleTextView;
this.setNativeView(titleTextView);
if (titleTextView) {
initNativeView(this);
if (this.titleDirty) {
this._update();
}

View File

@@ -5,7 +5,7 @@ import {
tabTextColorProperty, tabBackgroundColorProperty, selectedTabTextColorProperty,
androidSelectedTabHighlightColorProperty, androidOffscreenTabLimitProperty,
fontSizeProperty, fontInternalProperty, View, layout,
traceCategory, traceEnabled, traceWrite, initNativeView, Color
traceCategory, traceEnabled, traceWrite, Color
} from "./tab-view-common"
import { textTransformProperty, TextTransform, getTransformedText } from "../text-base";
import { fromFileOrResource } from "../../image-source";
@@ -212,13 +212,6 @@ export class TabViewItem extends TabViewItemBase {
return this.nativeView;
}
public setNativeView(textView: android.widget.TextView): void {
this.nativeView = textView;
if (textView) {
initNativeView(this);
}
}
public _update(): void {
const tv = this.nativeView;
if (tv) {

View File

@@ -3,7 +3,7 @@
import {
TabViewBase, TabViewItemBase, itemsProperty, selectedIndexProperty,
tabTextColorProperty, tabBackgroundColorProperty, selectedTabTextColorProperty, iosIconRenderingModeProperty,
View, fontInternalProperty, layout, traceEnabled, traceWrite, traceCategories, Color, initNativeView
View, fontInternalProperty, layout, traceEnabled, traceWrite, traceCategories, Color
} from "./tab-view-common"
import { textTransformProperty, TextTransform, getTransformedText } from "../text-base";
import { fromFileOrResource } from "../../image-source";
@@ -128,13 +128,12 @@ export class TabViewItem extends TabViewItemBase {
public setViewController(controller: UIViewController) {
this._iosViewController = controller;
(<any>this)._nativeView = this.nativeView = controller.view;
initNativeView(this);
this.setNativeView((<any>this)._nativeView = controller.view);
}
public disposeNativeView() {
this._iosViewController = undefined;
this.nativeView = undefined;
this.setNativeView(undefined);
}
public _update() {