feat(view): introduce LayoutChanged event on every View component (#5825)

* feat(view): introduce LayoutChanged event

* test(view): add LayoutChanged event tests

* chore(view-android): attach to onLayoutChange only if listener attached

* feat(view-android): override on/off in order to attach and detach from OnLayoutChangeListener
This commit is contained in:
Alexander Djenkov
2018-05-21 17:22:40 +03:00
committed by GitHub
parent f671f778f3
commit 0fc1547a19
8 changed files with 305 additions and 18 deletions

View File

@ -141,6 +141,9 @@ allTests["FRAME"] = frameTests;
import * as viewTests from "./ui/view/view-tests";
allTests["VIEW"] = viewTests;
import * as viewLayoutChangedEventTests from "./ui/view/view-tests-layout-event";
allTests["VIEW-LAYOUT-EVENT"] = viewLayoutChangedEventTests;
import * as styleTests from "./ui/styling/style-tests";
allTests["STYLE"] = styleTests;

View File

@ -37,7 +37,7 @@ export function getColor(uiColor: UIColor): Color {
return new Color(alpha, red, green, blue);
}
function clearPage(): void {
export function clearPage(): void {
let newPage = getCurrentPage();
if (!newPage) {
throw new Error("NO CURRENT PAGE!!!!");

View File

@ -0,0 +1,210 @@
import * as commonTests from "./view-tests-common";
import { View } from "tns-core-modules/ui/core/view";
import { Button } from "tns-core-modules/ui/button";
import { StackLayout } from "tns-core-modules/ui/layouts/stack-layout/stack-layout";
import * as helper from "../helper";
import * as TKUnit from "../../TKUnit";
import * as utils from "tns-core-modules/utils/utils";
export function test_event_LayoutChanged_GetActualSize() {
const test = function (views: Array<View>) {
let buttonLayoutChanged = false;
views[1].on(View.layoutChangedEvent, (data) => {
buttonLayoutChanged = true;
});
TKUnit.waitUntilReady(() => buttonLayoutChanged);
TKUnit.assert(views[1].getActualSize().height > 0);
TKUnit.assert(views[1].getActualSize().width > 0);
};
helper.do_PageTest_WithStackLayout_AndButton(test);
};
export function test_event_LayoutChanged_Listeners() {
const test = function (views: Array<View>) {
let stackLayoutChanged = false;
let buttonLayoutChanged = false;
views[1].on(View.layoutChangedEvent, (data) => {
buttonLayoutChanged = true;
});
TKUnit.waitUntilReady(() => buttonLayoutChanged);
TKUnit.assertFalse(views[0].hasListeners(View.layoutChangedEvent));
TKUnit.assert(views[1].hasListeners(View.layoutChangedEvent));
};
helper.do_PageTest_WithStackLayout_AndButton(test);
};
export function test_event_LayoutChanged_IsRaised() {
helper.clearPage();
let newPage = helper.getCurrentPage();
let stackLayoutChanged = false;
let buttonLayoutChanged = false;
let stackLayout = new StackLayout();
let button = new Button();
stackLayout.on(View.layoutChangedEvent, (data) => {
stackLayoutChanged = true;
});
button.on(View.layoutChangedEvent, (data) => {
buttonLayoutChanged = true;
});
stackLayout.addChild(button);
newPage.content = stackLayout;
TKUnit.waitUntilReady(() => stackLayoutChanged && buttonLayoutChanged);
TKUnit.assert(stackLayoutChanged);
TKUnit.assert(buttonLayoutChanged);
newPage.content = null;
};
export function test_event_LayoutChanged_IsRaised_StackLayout_ChildAdded() {
helper.clearPage();
let newPage = helper.getCurrentPage();
let stackLayoutChangedCount = 0;
let button1LayoutChangedCount = 0;
let button2LayoutChanged = false;
let stackLayout = new StackLayout();
// StackLayout should not be stretched in order to layout again when new button added.
stackLayout.verticalAlignment = "top";
let button1 = new Button();
let button2 = new Button();
stackLayout.on(View.layoutChangedEvent, (data) => {
stackLayoutChangedCount++;
});
button1.on(View.layoutChangedEvent, (data) => {
button1LayoutChangedCount++;
});
button2.on(View.layoutChangedEvent, (data) => {
button2LayoutChanged = true;
});
stackLayout.addChild(button1);
newPage.content = stackLayout;
TKUnit.waitUntilReady(() => stackLayout.isLoaded);
stackLayout.addChild(button2);
TKUnit.waitUntilReady(() => button2LayoutChanged);
TKUnit.assertEqual(stackLayoutChangedCount, 2);
TKUnit.assertEqual(button1LayoutChangedCount, 1);
TKUnit.assert(button2LayoutChanged);
newPage.content = null;
};
export function test_event_LayoutChanged_IsRaised_ChildMarginChanged() {
const test = function (views: Array<View>) {
let stackLayoutChanged = false;
let buttonLayoutChanged = false;
views[1].on(View.layoutChangedEvent, (data) => {
stackLayoutChanged = true;
});
views[2].on(View.layoutChangedEvent, (data) => {
buttonLayoutChanged = true;
});
(<Button>views[2]).marginTop = 50;
TKUnit.waitUntilReady(() => buttonLayoutChanged);
TKUnit.assert(stackLayoutChanged);
TKUnit.assert(buttonLayoutChanged);
};
helper.do_PageTest_WithStackLayout_AndButton(test);
};
export function test_event_LayoutChanged_IsRaised_ParentMarginChanged() {
const test = function (views: Array<View>) {
let stackLayoutChanged = false;
let buttonLayoutChanged = false;
views[1].on(View.layoutChangedEvent, (data) => {
stackLayoutChanged = true;
});
views[2].on(View.layoutChangedEvent, (data) => {
buttonLayoutChanged = true;
});
(<Button>views[2]).marginTop = 50;
TKUnit.waitUntilReady(() => buttonLayoutChanged);
TKUnit.assert(stackLayoutChanged);
TKUnit.assert(buttonLayoutChanged);
};
helper.do_PageTest_WithStackLayout_AndButton(test);
};
export function test_event_LayoutChanged_IsNotRaised_TransformChanged() {
const test = function (views: Array<View>) {
let stackLayoutChangedCount = 0;
let buttonLayoutChangedCount = 0;
const button = <Button>views[2];
views[1].on(View.layoutChangedEvent, (data) => {
stackLayoutChangedCount++;
});
button.on(View.layoutChangedEvent, (data) => {
buttonLayoutChangedCount++;
});
button.translateX += 50;
button.translateY += 50;
button.rotate += 50;
button.height = 200;
TKUnit.waitUntilReady(() => button.height === 200);
TKUnit.assertEqual(stackLayoutChangedCount, 1);
TKUnit.assertEqual(buttonLayoutChangedCount, 1);
};
helper.do_PageTest_WithStackLayout_AndButton(test);
};
export function test_event_LayoutChanged_IsRaised_StackLayout_SizeChanged() {
const test = function (views: Array<View>) {
let stackLayoutChanged = false;
let buttonLayoutChanged = false;
views[1].on(View.layoutChangedEvent, (data) => {
stackLayoutChanged = true;
});
views[2].on(View.layoutChangedEvent, (data) => {
buttonLayoutChanged = true;
});
(<StackLayout>views[1]).height = 100;
TKUnit.waitUntilReady(() => buttonLayoutChanged);
TKUnit.assert(stackLayoutChanged);
TKUnit.assert(buttonLayoutChanged);
};
helper.do_PageTest_WithStackLayout_AndButton(test);
};

View File

@ -1,15 +1,16 @@
import * as commonTests from "./view-tests-common";
import * as view from "tns-core-modules/ui/core/view";
import * as grid from "tns-core-modules/ui/layouts/grid-layout";
import * as color from "tns-core-modules/color";
import { View } from "tns-core-modules/ui/core/view";
import { Button } from "tns-core-modules/ui/button";
import { GridLayout } from "tns-core-modules/ui/layouts/grid-layout";
import { Color } from "tns-core-modules/color";
import * as helper from "../helper";
import * as TKUnit from "../../TKUnit";
import * as button from "tns-core-modules/ui/button";
import * as utils from "tns-core-modules/utils/utils";
global.moduleMerge(commonTests, exports);
class MyGrid extends grid.GridLayout {
class MyGrid extends GridLayout {
public backgroundDrawCount: number = 0;
_redrawNativeBackground(background: any) {
@ -18,33 +19,33 @@ class MyGrid extends grid.GridLayout {
}
}
export function getUniformNativeBorderWidth(v: view.View): number {
export function getUniformNativeBorderWidth(v: View): number {
return utils.layout.toDevicePixels((<UIView>v.ios).layer.borderWidth);
}
export function checkUniformNativeBorderColor(v: view.View): boolean {
if (v.borderColor instanceof color.Color) {
return (<UIView>v.ios).layer.borderColor === (<color.Color>v.borderColor).ios.CGColor;
export function checkUniformNativeBorderColor(v: View): boolean {
if (v.borderColor instanceof Color) {
return (<UIView>v.ios).layer.borderColor === (<Color>v.borderColor).ios.CGColor;
}
return undefined;
}
export function getUniformNativeCornerRadius(v: view.View): number {
export function getUniformNativeCornerRadius(v: View): number {
return utils.layout.toDevicePixels((<UIView>v.ios).layer.cornerRadius);
}
export function checkNativeBackgroundColor(v: view.View): boolean {
export function checkNativeBackgroundColor(v: View): boolean {
if (v.ios instanceof UILabel) {
var cgColor1 = (<UILabel>v.ios).layer.backgroundColor;
var cgColor2 = (<UIColor>(<color.Color>v.backgroundColor).ios).CGColor;
var cgColor2 = (<UIColor>(<Color>v.backgroundColor).ios).CGColor;
return v.backgroundColor && !!CGColorEqualToColor(cgColor1, cgColor2);
}
return v.backgroundColor && (<UIView>v.ios).backgroundColor.isEqual((<color.Color>v.backgroundColor).ios);
return v.backgroundColor && (<UIView>v.ios).backgroundColor.isEqual((<Color>v.backgroundColor).ios);
}
export function checkNativeBackgroundImage(v: view.View): boolean {
export function checkNativeBackgroundImage(v: View): boolean {
return (<UIView>v.ios).backgroundColor !== undefined;
}
@ -53,7 +54,7 @@ export function testBackgroundInternalChangedOnceOnResize() {
let root = helper.getCurrentPage();
let layout = new MyGrid();
layout.className = "myClass";
layout.backgroundColor = new color.Color(255, 255, 0, 0);
layout.backgroundColor = new Color(255, 255, 0, 0);
root.css = ".myClass { background-image: url('~/logo.png') }";
root.content = layout;
@ -82,7 +83,7 @@ export function testBackgroundInternalChangedOnceOnResize() {
}
export function test_automation_text_set_to_native() {
var newButton = new button.Button();
var newButton = new Button();
newButton.automationText = "Button1";
helper.getCurrentPage().content = newButton;
TKUnit.assertEqual((<UIView>newButton.ios).accessibilityIdentifier, "Button1", "accessibilityIdentifier not set to native view.");

View File

@ -61,6 +61,7 @@ export function PseudoClassHandler(...pseudoClasses: string[]): MethodDecorator
export const _rootModalViews = new Array<ViewBase>();
export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public static layoutChangedEvent = "layoutChanged";
public static shownModallyEvent = "shownModally";
public static showingModallyEvent = "showingModally";
@ -277,6 +278,14 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
//
}
protected _raiseLayoutChangedEvent() {
const args: EventData = {
eventName: ViewCommon.layoutChangedEvent,
object: this
};
this.notify(args);
}
protected _raiseShownModallyEvent() {
const args: ShownModallyData = {
eventName: ViewCommon.shownModallyEvent,

View File

@ -7,7 +7,7 @@ import {
ViewCommon, layout, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty,
traceEnabled, traceWrite, traceCategories, traceNotifyEvent,
paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty,
Color
Color, EventData
} from "./view-common";
import {
@ -229,6 +229,8 @@ export class View extends ViewCommon {
private _isClickable: boolean;
private touchListenerIsSet: boolean;
private touchListener: android.view.View.OnTouchListener;
private layoutChangeListenerIsSet: boolean;
private layoutChangeListener: android.view.View.OnLayoutChangeListener;
private _manager: android.app.FragmentManager;
nativeViewProtected: android.view.View;
@ -241,6 +243,26 @@ export class View extends ViewCommon {
}
}
on(eventNames: string, callback: (data: EventData) => void, thisArg?: any) {
super.on(eventNames, callback, thisArg);
const isLayoutEvent = typeof eventNames === "string" ? eventNames.indexOf(ViewCommon.layoutChangedEvent) !== -1 : false;
if (this.isLoaded && !this.layoutChangeListenerIsSet && isLayoutEvent) {
this.setOnLayoutChangeListener();
}
}
off(eventNames: string, callback?: any, thisArg?: any) {
super.off(eventNames, callback, thisArg);
const isLayoutEvent = typeof eventNames === "string" ? eventNames.indexOf(ViewCommon.layoutChangedEvent) !== -1 : false;
// Remove native listener only if there are no more user listeners for LayoutChanged event
if (this.isLoaded && this.layoutChangeListenerIsSet && isLayoutEvent && !this.hasListeners(ViewCommon.layoutChangedEvent)) {
this.nativeViewProtected.removeOnLayoutChangeListener(this.layoutChangeListener);
this.layoutChangeListenerIsSet = false;
}
}
public _getFragmentManager(): android.app.FragmentManager {
let manager = this._manager;
if (!manager) {
@ -305,6 +327,19 @@ export class View extends ViewCommon {
public initNativeView(): void {
super.initNativeView();
this._isClickable = this.nativeViewProtected.isClickable();
if (this.hasListeners(ViewCommon.layoutChangedEvent)) {
this.setOnLayoutChangeListener();
}
}
public disposeNativeView(): void {
super.disposeNativeView();
if (this.layoutChangeListenerIsSet) {
this.layoutChangeListenerIsSet = false;
this.nativeViewProtected.removeOnLayoutChangeListener(this.layoutChangeListener);
}
}
private setOnTouchListener() {
@ -320,6 +355,25 @@ export class View extends ViewCommon {
}
}
private setOnLayoutChangeListener() {
if (this.nativeViewProtected) {
const owner = this;
this.layoutChangeListenerIsSet = true;
this.layoutChangeListener = this.layoutChangeListener || new android.view.View.OnLayoutChangeListener({
onLayoutChange(
v: android.view.View,
left: number, top: number, right: number, bottom: number,
oldLeft: number, oldTop: number, oldRight: number, oldBottom: number): void {
if (left !== oldLeft || top !== oldTop || right !== oldRight || bottom !== oldBottom) {
owner._raiseLayoutChangedEvent();
}
}
});
this.nativeViewProtected.addOnLayoutChangeListener(this.layoutChangeListener);
}
}
get isLayoutRequired(): boolean {
return !this.isLayoutValid;
}

View File

@ -103,6 +103,10 @@ export interface ShownModallyData extends EventData {
* 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 {
/**
* String value used when hooking to layoutChanged event.
*/
public static layoutChangedEvent: string;
/**
* String value used when hooking to showingModally event.
*/

View File

@ -28,6 +28,7 @@ export class View extends ViewCommon {
nativeViewProtected: UIView;
viewController: UIViewController;
private _isLaidOut = false;
private _hasTransfrom = false;
private _privateFlags: number = PFLAG_LAYOUT_REQUIRED | PFLAG_FORCE_LAYOUT;
private _cachedFrame: CGRect;
@ -160,6 +161,11 @@ export class View extends ViewCommon {
const boundsOrigin = nativeView.bounds.origin;
nativeView.bounds = CGRectMake(boundsOrigin.x, boundsOrigin.y, frame.size.width, frame.size.height);
this._raiseLayoutChangedEvent();
this._isLaidOut = true;
} else if (!this._isLaidOut) {
// Rects could be equal on the first layout and an event should be raised.
this._raiseLayoutChangedEvent();
}
}