Avoid applying CSS multiple times (#4784)

* Move the applyStyleFromScope to onLoaded, when the views are created and id or className properties are set the CSS selectors are queried and applied multiple times

* Condense the changes when applying properties
This commit is contained in:
Panayot Cankov
2017-09-25 18:32:00 +03:00
committed by SvetoslavTsenov
parent b0577728be
commit 6d7c1ff295
25 changed files with 536 additions and 524 deletions

View File

@@ -77,6 +77,7 @@ export class CssProperty<T extends Style, U> {
public readonly setNative: symbol;
public readonly name: string;
public readonly cssName: string;
public readonly cssLocalName: string;
public readonly defaultValue: U;
public register(cls: { prototype: T }): void;
public isSet(instance: T): boolean;
@@ -92,7 +93,7 @@ export class ShorthandProperty<T extends Style, P> {
public readonly name: string;
public readonly cssName: string;
public register(cls: { prototype: T }): void;
public register(cls: typeof Style): void;
}
export class CssAnimationProperty<T extends Style, U> {
@@ -100,11 +101,10 @@ 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;
public readonly native: symbol;
public readonly cssLocalName: string;
readonly keyframe: string;

View File

@@ -646,9 +646,10 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
}
CssProperty.prototype.isStyleProperty = true;
export class CssAnimationProperty<T extends Style, U> {
export class CssAnimationProperty<T extends Style, U> implements definitions.CssAnimationProperty<T, U> {
public readonly name: string;
public readonly cssName: string;
public readonly cssLocalName: string;
public readonly getDefault: symbol;
public readonly setNative: symbol;
@@ -682,7 +683,9 @@ export class CssAnimationProperty<T extends Style, U> {
this._valueConverter = options.valueConverter;
const cssName = "css:" + (options.cssName || propertyName);
const cssLocalName = (options.cssName || propertyName);
this.cssLocalName = cssLocalName;
const cssName = "css:" + cssLocalName;
this.cssName = cssName;
const keyframeName = "keyframe:" + propertyName;
@@ -866,8 +869,10 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
}
}
const oldValue: U = key in this ? this[key] : defaultValue;
const view = this.view;
let value: U;
let unsetNativeValue = false;
if (reset) {
// If unsetValue - we want to reset this property.
let parent = view.parent;
@@ -876,9 +881,12 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
if (style && style[sourceKey] > ValueSource.Default) {
value = style[propertyName];
this[sourceKey] = ValueSource.Inherited;
this[key] = value;
} else {
value = defaultValue;
delete this[sourceKey];
delete this[key];
unsetNativeValue = true;
}
} else {
this[sourceKey] = valueSource;
@@ -887,44 +895,30 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
} else {
value = boxedValue;
}
this[key] = 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;
if (reset) {
delete this[key];
if (valueChanged) {
valueChanged(this, oldValue, value);
}
if (valueChanged) {
valueChanged(this, oldValue, value);
}
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
if (view[setNative]) {
if (view._suspendNativeUpdatesCount) {
if (view._suspendedUpdates) {
view._suspendedUpdates[propertyName] = property;
}
} else {
if (unsetNativeValue) {
if (defaultValueKey in this) {
view[setNative](this[defaultValueKey]);
delete this[defaultValueKey];
} else {
view[setNative](defaultValue);
}
}
}
} else {
this[key] = value;
if (valueChanged) {
valueChanged(this, oldValue, value);
}
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;
@@ -981,6 +975,7 @@ export class ShorthandProperty<T extends Style, P> implements definitions.Shorth
protected readonly cssValueDescriptor: PropertyDescriptor;
protected readonly localValueDescriptor: PropertyDescriptor;
protected readonly propertyBagDescriptor: PropertyDescriptor;
public readonly sourceKey: symbol;
@@ -1025,19 +1020,32 @@ export class ShorthandProperty<T extends Style, P> implements definitions.Shorth
set: setLocalValue
};
this.propertyBagDescriptor = {
enumerable: false,
configurable: true,
set(value: string) {
converter(value).forEach(([property, value]) => {
this[property.cssLocalName] = value;
});
}
}
cssSymbolPropertyMap[key] = this;
}
public register(cls: { prototype: T }): void {
public register(cls: typeof Style): void {
if (this.registered) {
throw new Error(`Property ${this.name} already registered.`);
}
this.registered = true;
Object.defineProperty(cls.prototype, this.name, this.localValueDescriptor);
Object.defineProperty(cls.prototype, this.cssName, this.cssValueDescriptor);
if (this.cssLocalName !== this.cssName) {
Object.defineProperty(cls.prototype, this.cssLocalName, this.localValueDescriptor);
}
Object.defineProperty(cls.prototype.PropertyBag, this.cssLocalName, this.propertyBagDescriptor);
}
}

View File

@@ -167,11 +167,6 @@ export abstract class ViewBase extends Observable {
*/
public className: string;
/**
* Gets or sets inline style selectors for this view.
*/
public inlineStyleSelector: SelectorCore;
/**
* Gets owner page. This is a read-only property.
*/
@@ -220,15 +215,22 @@ export abstract class ViewBase extends Observable {
_domId: number;
_cssState: any /* "ui/styling/style-scope" */;
_setCssState(next: any /* "ui/styling/style-scope" */);
_registerAnimation(animation: KeyframeAnimation);
_unregisterAnimation(animation: KeyframeAnimation);
_cancelAllAnimations();
/**
* @private
* Notifies each child's css state for change, recursively.
* Either the style scope, className or id properties were changed.
*/
_onCssStateChange(): void;
public cssClasses: Set<string>;
public cssPseudoClasses: Set<string>;
public _goToVisualState(state: string): void;
/**
* This used to be the way to set attribute values in early {N} versions.
* Now attributes are expected to be set as plain properties on the view instances.
* @deprecated
*/
public _applyXmlAttribute(attribute, value): boolean;
public setInlineStyle(style: string): void;
@@ -293,7 +295,7 @@ export abstract class ViewBase extends Observable {
/**
* @protected
* @unstable
* A widget can call this method to discard mathing css pseudo class.
* A widget can call this method to discard matching css pseudo class.
*/
public deletePseudoClass(name: string): void;
@@ -331,6 +333,11 @@ export abstract class ViewBase extends Observable {
* @private
*/
_setupAsRootView(context: any): void;
/**
* @private
*/
_inheritStyleScope(styleScope: any /* StyleScope */): void;
//@endprivate
}
@@ -348,4 +355,4 @@ export const bindingContextProperty: InheritedProperty<ViewBase, any>;
* Converts string into boolean value.
* Throws error if value is not 'true' or 'false'.
*/
export function booleanConverter(v: string): boolean;
export function booleanConverter(v: string): boolean;

View File

@@ -138,9 +138,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
private _androidView: Object;
private _style: Style;
private _isLoaded: boolean;
private _registeredAnimations: Array<KeyframeAnimation>;
private _visualState: string;
private _inlineStyleSelector: SelectorCore;
private __nativeView: any;
// private _disableNativeViewRecycling: boolean;
public domNode: DOMNode;
@@ -157,7 +155,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public _domId: number;
public _context: any;
public _isAddedToNativeVisualTree: boolean;
public _cssState: ssm.CssState;
public _cssState: ssm.CssState = new ssm.CssState(this);
public _styleScope: ssm.StyleScope;
public _suspendedUpdates: { [propertyName: string]: Property<ViewBase, any> | CssProperty<Style, any> | CssAnimationProperty<Style, any> };
public _suspendNativeUpdatesCount: number;
@@ -229,8 +227,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
get style(): Style {
return this._style;
}
set style(value) {
throw new Error("View.style property is read-only.");
set style(inlineStyle: Style /* | string */) {
if (typeof inlineStyle === "string") {
this.setInlineStyle(inlineStyle);
} else {
throw new Error("View.style property is read-only.");
}
}
get android(): any {
@@ -254,13 +256,6 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.className = v;
}
get inlineStyleSelector(): SelectorCore {
return this._inlineStyleSelector;
}
set inlineStyleSelector(value: SelectorCore) {
this._inlineStyleSelector = value;
}
getViewById<T extends ViewBaseDefinition>(id: string): T {
return <T>getViewById(this, id);
}
@@ -288,6 +283,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
@profile
public onLoaded() {
this._isLoaded = true;
this._cssState.onLoaded();
this._resumeNativeUpdates();
this._loadEachChild();
this._emit("loaded");
@@ -305,6 +301,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this._suspendNativeUpdates();
this._unloadEachChild();
this._isLoaded = false;
this._cssState.onUnloaded();
this._emit("unloaded");
}
@@ -336,101 +333,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
});
}
@profile
public _applyStyleFromScope() {
const scope = this._styleScope;
if (scope) {
scope.applySelectors(this);
} else {
this._setCssState(null);
}
}
// TODO: Make sure the state is set to null and this is called on unloaded to clean up change listeners...
@profile
_setCssState(next: ssm.CssState): void {
const previous = this._cssState;
this._cssState = next;
if (!this._invalidateCssHandler) {
this._invalidateCssHandler = () => {
if (this._invalidateCssHandlerSuspended) {
return;
}
this.applyCssState();
};
}
try {
this._invalidateCssHandlerSuspended = true;
if (next) {
next.changeMap.forEach((changes, view) => {
if (changes.attributes) {
changes.attributes.forEach(attribute => {
view.addEventListener(attribute + "Change", this._invalidateCssHandler);
});
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach(pseudoClass => {
let eventName = ":" + pseudoClass;
view.addEventListener(":" + pseudoClass, this._invalidateCssHandler);
if (view[eventName]) {
view[eventName](+1);
}
});
}
});
}
if (previous) {
previous.changeMap.forEach((changes, view) => {
if (changes.attributes) {
changes.attributes.forEach(attribute => {
view.removeEventListener("onPropertyChanged:" + attribute, this._invalidateCssHandler);
});
}
if (changes.pseudoClasses) {
changes.pseudoClasses.forEach(pseudoClass => {
let eventName = ":" + pseudoClass;
view.removeEventListener(eventName, this._invalidateCssHandler);
if (view[eventName]) {
view[eventName](-1);
}
});
}
});
}
} finally {
this._invalidateCssHandlerSuspended = false;
}
this.applyCssState();
}
private notifyPseudoClassChanged(pseudoClass: string): void {
this.notify({ eventName: ":" + pseudoClass, object: this });
}
/**
* Notify that some attributes or pseudo classes that may affect the current CssState had changed.
*/
private _invalidateCssHandler;
private _invalidateCssHandlerSuspended: boolean;
@profile
private applyCssState(): void {
this._batchUpdate(() => {
if (!this._cssState) {
this._cancelAllAnimations();
resetCSSProperties(this.style);
return;
}
this._cssState.apply();
});
}
private pseudoClassAliases = {
'highlighted': [
'active',
@@ -474,19 +380,6 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
@profile
private _applyInlineStyle(inlineStyle) {
if (typeof inlineStyle === "string") {
try {
// this.style._beginUpdate();
ensureStyleScopeModule();
styleScopeModule.applyInlineStyle(this, inlineStyle);
} finally {
// this.style._endUpdate();
}
}
}
private bindingContextChanged(data: PropertyChangeData): void {
this.bindings.get("bindingContext").bind(data.value);
}
@@ -584,24 +477,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
}
@profile
private _setStyleScope(scope: ssm.StyleScope): void {
this._styleScope = scope;
this._applyStyleFromScope();
this.eachChild((v) => {
v._setStyleScope(scope);
return true;
});
}
public _addViewCore(view: ViewBase, atIndex?: number) {
propagateInheritableProperties(this, view);
const styleScope = this._styleScope;
if (styleScope) {
view._setStyleScope(styleScope);
}
view._inheritStyleScope(this._styleScope);
propagateInheritableCssProperties(this.style, view.style);
if (this._context) {
@@ -614,8 +492,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
/**
* Core logic for removing a child view from this instance. Used by the framework to handle lifecycle events more centralized. Do not use outside the UI Stack implementation.
*/
* Core logic for removing a child view from this instance. Used by the framework to handle lifecycle events more centralized. Do not use outside the UI Stack implementation.
*/
public _removeView(view: ViewBase) {
if (traceEnabled()) {
traceWrite(`${this}._removeView(${view})`, traceCategories.ViewHierarchy);
@@ -638,17 +516,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
* Method is intended to be overridden by inheritors and used as "protected"
*/
public _removeViewCore(view: ViewBase) {
// TODO: Discuss this.
if (this._styleScope === view._styleScope) {
view._setStyleScope(null);
}
if (view.isLoaded) {
view.onUnloaded();
}
// view.unsetInheritedProperties();
if (view._context) {
view._tearDownUI();
}
@@ -663,10 +534,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
public initNativeView(): void {
// No initNativeView(this)?
if (this._cssState) {
this._cssState.playPendingKeyframeAnimations();
}
//
}
public resetNativeView(): void {
@@ -688,9 +556,9 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
// }
// }
if (this._cssState) {
this._cancelAllAnimations();
}
// if (this._cssState) {
// this._cancelAllAnimations();
// }
}
_setupAsRootView(context: any): void {
@@ -895,12 +763,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.addPseudoClass(state);
}
public _applyXmlAttribute(attribute, value): boolean {
if (attribute === "style") {
this._applyInlineStyle(value);
return true;
}
/**
* This used to be the way to set attribute values in early {N} versions.
* Now attributes are expected to be set as plain properties on the view instances.
* @deprecated
*/
public _applyXmlAttribute(): boolean {
return false;
}
@@ -909,7 +777,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
throw new Error("Parameter should be valid CSS string!");
}
this._applyInlineStyle(style);
ensureStyleScopeModule();
styleScopeModule.applyInlineStyle(this, style);
}
public _parentChanged(oldParent: ViewBase): void {
@@ -932,30 +801,6 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
initNativeView(this);
}
public _registerAnimation(animation: KeyframeAnimation) {
if (this._registeredAnimations === undefined) {
this._registeredAnimations = new Array<KeyframeAnimation>();
}
this._registeredAnimations.push(animation);
}
public _unregisterAnimation(animation: KeyframeAnimation) {
if (this._registeredAnimations) {
let index = this._registeredAnimations.indexOf(animation);
if (index >= 0) {
this._registeredAnimations.splice(index, 1);
}
}
}
public _cancelAllAnimations() {
if (this._registeredAnimations) {
for (let animation of this._registeredAnimations) {
animation.cancel();
}
}
}
public toString(): string {
let str = this.typeName;
if (this.id) {
@@ -970,6 +815,25 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
return str;
}
_onCssStateChange(): void {
this._cssState.onChange();
eachDescendant(this, (child: ViewBase) => {
child._cssState.onChange();
return true;
});
}
_inheritStyleScope(styleScope: ssm.StyleScope): void {
if (this._styleScope !== styleScope) {
this._styleScope = styleScope;
this._onCssStateChange();
this.eachChild(child => {
child._inheritStyleScope(styleScope);
return true
});
}
}
}
ViewBase.prototype.isCollapsed = false;
@@ -1019,20 +883,12 @@ export const classNameProperty = new Property<ViewBase, string>({
if (typeof newValue === "string") {
newValue.split(" ").forEach(c => classes.add(c));
}
resetStyles(view);
view._onCssStateChange();
}
});
classNameProperty.register(ViewBase);
function resetStyles(view: ViewBase): void {
view._applyStyleFromScope();
view.eachChild((child) => {
resetStyles(child);
return true;
});
}
export const idProperty = new Property<ViewBase, string>({ name: "id", valueChanged: (view, oldValue, newValue) => resetStyles(view) });
export const idProperty = new Property<ViewBase, string>({ name: "id", valueChanged: (view, oldValue, newValue) => view._onCssStateChange() });
idProperty.register(ViewBase);
export function booleanConverter(v: string): boolean {

View File

@@ -100,7 +100,6 @@ export class View extends ViewCommon {
this.nativeViewProtected.setClickable(this._isClickable);
}
this._cancelAllAnimations();
super.onUnloaded();
}

View File

@@ -67,7 +67,7 @@ export interface Size {
* This class is the base class for all UI components.
* A View occupies a rectangular area on the screen and is responsible for drawing and layouting of all UI components within.
*/
export abstract class View extends ViewBase implements ApplyXmlAttributes {
export abstract class View extends ViewBase {
/**
* Gets the android-specific native instance that lies behind this proxy. Will be available if running on an Android platform.
*/
@@ -83,8 +83,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
*/
bindingContext: any;
//----------Style property shortcuts----------
/**
* Gets or sets the border color of the view.
*/
@@ -413,12 +411,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
*/
public focus(): boolean;
/**
* Sets in-line CSS string as style.
* @param style - In-line CSS string.
*/
public setInlineStyle(style: string): void;
public getGestureObservers(type: GestureTypes): Array<GesturesObserver>;
/**
@@ -490,7 +482,6 @@ export abstract class View extends ViewBase implements ApplyXmlAttributes {
_eachLayoutView(callback: (View) => void): void;
public _applyXmlAttribute(attribute: string, value: any): boolean;
public eachChildView(callback: (view: View) => boolean): void;
//@private
@@ -647,19 +638,6 @@ export interface AddChildFromBuilder {
_addChildFromBuilder(name: string, value: any): void;
}
/**
* Defines an interface used to create a member of a class from string representation (used in xml declaration).
*/
export interface ApplyXmlAttributes {
/**
* Called for every attribute in xml declaration. <... fontWeight="bold" ../>
* @param attributeName - the name of the attribute (fontAttributes)
* @param attrValue - the value of the attribute (bold)
* Should return true if this attribute is handled and there is no need default handler to process it.
*/
_applyXmlAttribute(attributeName: string, attrValue: any): boolean;
}
export const automationTextProperty: Property<View, string>;
export const originXProperty: Property<View, number>;
export const originYProperty: Property<View, number>;