From 791aab04e5bbcdecb59288bf942cded352f31b56 Mon Sep 17 00:00:00 2001 From: Panayot Cankov Date: Wed, 5 Oct 2016 13:58:33 +0300 Subject: [PATCH] Flexbox ios initial commit Items now appear, but they should initially shrink and they don't Add 15-ish tests for flexbox layout Port reasonable flexbox test set Fixing issues, adding unit tests Moved from .tsx to .ts and used our ui/builder.parse --- tests/app/testRunner.ts | 1 + tests/app/ui/layouts/flexbox-layout-tests.ts | 1901 +++++++++++++++++ tests/tsconfig.json | 4 +- tns-core-modules/ui/core/view-common.ts | 10 + tns-core-modules/ui/core/view.android.ts | 9 + tns-core-modules/ui/core/view.d.ts | 4 + .../flexbox-layout/flexbox-layout-common.ts | 48 +- .../flexbox-layout/flexbox-layout.android.ts | 35 +- .../flexbox-layout/flexbox-layout.d.ts | 18 +- .../flexbox-layout.internal.d.ts | 6 + .../flexbox-layout/flexbox-layout.ios.ts | 1448 +++++++++++++ tns-core-modules/ui/layouts/layout.ios.ts | 2 +- tns-core-modules/utils/utils-common.ts | 1 + tns-core-modules/utils/utils.d.ts | 1 + .../android/org.nativescript.widgets.d.ts | 3 + tsconfig.json | 2 + 16 files changed, 3457 insertions(+), 36 deletions(-) create mode 100644 tests/app/ui/layouts/flexbox-layout-tests.ts create mode 100644 tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.internal.d.ts create mode 100644 tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.ios.ts diff --git a/tests/app/testRunner.ts b/tests/app/testRunner.ts index ca2b102e1..9a608ab66 100644 --- a/tests/app/testRunner.ts +++ b/tests/app/testRunner.ts @@ -62,6 +62,7 @@ allTests["WRAPLAYOUT"] = require("./ui/layouts/wrap-layout-tests"); allTests["ABSOLUTELAYOUT"] = require("./ui/layouts/absolute-layout-tests"); allTests["GRIDLAYOUT"] = require("./ui/layouts/grid-layout-tests"); allTests["STACKLAYOUT"] = require("./ui/layouts/stack-layout-tests"); +allTests["FLEXBOXLAYOUT"] = require("./ui/layouts/flexbox-layout-tests"); allTests["STYLE-PROPERTIES"] = require("./ui/styling/style-properties-tests"); allTests["FRAME"] = require("./ui/frame/frame-tests"); allTests["VIEW"] = require("./ui/view/view-tests"); diff --git a/tests/app/ui/layouts/flexbox-layout-tests.ts b/tests/app/ui/layouts/flexbox-layout-tests.ts new file mode 100644 index 000000000..c5dde4db6 --- /dev/null +++ b/tests/app/ui/layouts/flexbox-layout-tests.ts @@ -0,0 +1,1901 @@ +import { + FlexboxLayout, + FlexWrap, + AlignItems, + AlignContent, + AlignSelf, + FlexDirection, + JustifyContent +} from "ui/layouts/flexbox-layout"; +import {View} from "ui/core/view"; +import {Label} from "ui/label"; +import TKUnit = require("../../TKUnit"); +import helper = require("../helper"); +import {layout} from "utils/utils"; +import {parse} from "ui/builder"; + +import dipToDp = layout.toDevicePixels; + +const EPS = 1; + +function waitUntilTestElementLayoutIsValid(view: View, timeoutSec?: number): void { + TKUnit.waitUntilReady(() => { + return view.isLayoutValid; + }, timeoutSec || 1); +} + +function commonAncestor(view1: View, view2: View): View { + let set = new Set(); + do { + if (view1) { + if (set.has(view1)) { + return view1; + } + set.add(view1); + view1 = view1.parent; + } + if (view2) { + if (set.has(view2)) { + return view2; + } + set.add(view2); + view2 = view2.parent; + } + } while(view1 || view2); + return null; +} + +interface Bounds { + left: number; + top: number; + right: number; + bottom: number; +} + +function bounds(view: View): Bounds { + return view._getCurrentLayoutBounds(); +} + +/** + * Get the bounds of the child as if it was placed in the same view as its ancestor, + * so the bounds can be compared in the same coordinate system. + */ +function boundsToAncestor(child: View, ancestor: View = null) { + let currentBounds = bounds(child); + while(child && child !== ancestor) { + child = child.parent; + let childBounds = bounds(child); + currentBounds.left += childBounds.left; + currentBounds.right += childBounds.left; + currentBounds.top += childBounds.top; + currentBounds.bottom += childBounds.top; + } + return currentBounds; +} + +function baseline(view: View): number { + // TODO: Return View's baseline! + return 0; +} + +function width(child: View): number { + let b = bounds(child); + return b.right - b.left; +} + +function height(child: View): number { + let b = bounds(child); + return b.bottom - b.top; +} + +function top(child: View): number { return bounds(child).top; } +function right(child: View): number { return bounds(child).right; } +function bottom(child: View): number { return bounds(child).bottom; } +function left(child: View): number { return bounds(child).left; } + +function equal(a: T, b: T, message?: string) { + message ? TKUnit.assertEqual(a, b, message) : TKUnit.assertEqual(a, b); +} + +function closeEnough(a: number, b: number, message?: string) { + message ? TKUnit.assertTrue(Math.abs(a - b) <= EPS, message) : TKUnit.assertTrue(Math.abs(a - b) <= EPS); +} + +function notEqual(a: T, b: T, message?: string) { + message ? TKUnit.assertNotEqual(a, b, message) : TKUnit.assertNotEqual(a, b); +} + +function check(exp: boolean, message?: string) { + message ? TKUnit.assert(exp, message) : TKUnit.assert(exp); +} + +function widthEqual(view1: View, view2: View) { + equal(width(view1), width(view2), `Expected width of ${view1} to equal width of ${view2}.`); +} + +function heightEqual(view1: View, view2: View) { + equal(height(view1), height(view2), `Expected height of ${view1} to equal height of ${view2}.`); +} + +function comparableBounds(view1: View, view2: View): { bounds1: Bounds, bounds2: Bounds } { + let ancestor = commonAncestor(view1, view2); + let bounds1 = boundsToAncestor(view1, ancestor); + let bounds2 = boundsToAncestor(view2, ancestor); + return { bounds1, bounds2 }; +} + +function isLeftAlignedWith(view1: View, view2: View) { + let { bounds1, bounds2 } = comparableBounds(view1, view2); + TKUnit.assertEqual(bounds1.left, bounds2.left, `${view1} is not left-aligned with ${view2}`); +} + +function isRightAlignedWith(view1: View, view2: View) { + let { bounds1, bounds2 } = comparableBounds(view1, view2); + TKUnit.assertEqual(bounds1.right, bounds2.right, `${view1} is not right-aligned with ${view2}`); +} + +function isTopAlignedWith(view1: View, view2: View) { + let { bounds1, bounds2 } = comparableBounds(view1, view2); + TKUnit.assertEqual(bounds1.top, bounds2.top, `${view1} is not top-aligned with ${view2}`); +} + +function isBottomAlignedWith(view1: View, view2: View) { + let { bounds1, bounds2 } = comparableBounds(view1, view2); + TKUnit.assertEqual(bounds1.bottom, bounds2.bottom, `${view1} is not bottom-aligned with ${view2}`); +} + +function isRightOf(view1: View, view2: View) { + let { bounds1, bounds2 } = comparableBounds(view1, view2); + TKUnit.assert(bounds1.left >= bounds2.right, `${view1}.left: ${bounds1.left} is not right of ${view2}.right: ${bounds2.right}`); +} + +function isLeftOf(view1: View, view2: View) { + let { bounds1, bounds2 } = comparableBounds(view1, view2); + TKUnit.assert(bounds1.right <= bounds2.left, `${view1}.right: ${bounds1.right} is not left of ${view2}.left: ${bounds2.left}`); +} + +function isBelow(view1: View, view2: View) { + let { bounds1, bounds2 } = comparableBounds(view1, view2); + TKUnit.assert(bounds1.top >= bounds2.bottom, `${view1}.top: ${bounds1.top} is not below ${view2}.bottom: ${bounds2.bottom}`); +} + +function isAbove(view1: View, view2: View) { + let { bounds1, bounds2 } = comparableBounds(view1, view2); + TKUnit.assert(bounds1.bottom <= bounds2.top, `${view1}.bottom: ${bounds1.bottom} is not above ${view2}.top: ${bounds2.top}`); +} + +const noop = () => { + // no operation +}; + +// TODO: Order tests! + +function test(ui: () => U, setup: (ui: U) => void, test: (ui: U) => void): () => void { + return () => { + let i = ui(); + setup(i); + helper.buildUIAndRunTest(i.root, () => { + waitUntilTestElementLayoutIsValid(i.root); + test(i); + }); + } +}; + +let getViews = (template: string) => { + let root = parse(template); + return { root, + flexbox: root.getViewById("flexbox") as FlexboxLayout, + text1: root.getViewById("text1") as Label, + text2: root.getViewById("text2") as Label, + text3: root.getViewById("text3") as Label, + text4: root.getViewById("text4") as Label, + text5: root.getViewById("text5") as Label + }; +}; + +let activity_flex_wrap = () => getViews( + ` + ` +); + +export const testFlexWrap_wrap = test( + activity_flex_wrap, + ({flexbox}) => flexbox.flexWrap = FlexWrap.WRAP, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + isLeftAlignedWith(text3, flexbox); + // equal(flexbox.getFlexLines().size(), is(1)); + } +); + +export const testFlexWrap_nowrap = test( + activity_flex_wrap, + ({flexbox}) => flexbox.flexWrap = FlexWrap.NOWRAP, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + isRightOf(text3, text2); + isTopAlignedWith(text3, flexbox); + // equal(flexbox.getFlexLines().size(), is(1)); + } +); + +export const testFlexWrap_wrap_reverse = test( + activity_flex_wrap, + ({flexbox}) => flexbox.flexWrap = FlexWrap.WRAP_REVERSE, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isAbove(text3, text1); + isAbove(text3, text2); + isLeftAlignedWith(text3, flexbox); + // equal(flexbox.getFlexLines().size(), is(2)); + } +); + +export const testFlexWrap_wrap_flexDirection_column = test( + activity_flex_wrap, + ({flexbox}) => flexbox.flexDirection = FlexDirection.COLUMN, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isLeftAlignedWith(text2, flexbox); + isRightOf(text3, text1); + isRightOf(text3, text2); + isTopAlignedWith(text3, flexbox); + // equal(flexbox.getFlexLines().size(), is(2)); + } +); + +export const testFlexWrap_nowrap_flexDirection_column = test( + activity_flex_wrap, + ({flexbox}) => { + flexbox.flexDirection = FlexDirection.COLUMN; + flexbox.flexWrap = FlexWrap.NOWRAP; + }, ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isLeftAlignedWith(text2, flexbox); + isBelow(text3, text2); + isLeftAlignedWith(text3, flexbox); + // equal(flexbox.getFlexLines().size(), is(1)); + } +); + +export const testFlexWrap_wrap_reverse_flexDirection_column = test( + activity_flex_wrap, + ({flexbox}) => { + flexbox.flexDirection = FlexDirection.COLUMN; + flexbox.flexWrap = FlexWrap.WRAP_REVERSE; + }, ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isRightAlignedWith(text1, flexbox); + isBelow(text2, text1); + isLeftOf(text3, text1); + isLeftOf(text3, text2); + isTopAlignedWith(text3, flexbox); + // equal(flexbox.getFlexLines().size(), is(2)); + } +); + +let activity_flex_item_match_parent = () => getViews( + ` + ` +); + +export const testFlexItem_match_parent = test( + activity_flex_item_match_parent, + noop, + ({root, flexbox, text1, text2, text3}) => { + widthEqual(text1, flexbox); + widthEqual(text2, flexbox); + widthEqual(text3, flexbox); + equal(height(flexbox), height(text1) + height(text2) + height(text3), `Expected height of ${flexbox} to equal sum of widths for ${text1}, ${text2}, ${text3}`); + } +); + +let activity_flex_item_match_parent_direction_column = () => getViews( + ` + ` +); + +export const testFlexItem_match_parent_flexDirection_column = test( + activity_flex_item_match_parent_direction_column, + noop, + ({root, flexbox, text1, text2, text3}) => { + heightEqual(text1, flexbox); + heightEqual(text2, flexbox); + heightEqual(text3, flexbox); + equal(width(flexbox), width(text1) + width(text2) + width(text3), `Expected width of ${flexbox} to equal sum of widths for ${text1}, ${text2}, ${text3}`); + } +); + +let activity_flexbox_wrap_content = () => getViews( + ` + ` +); + +export const testFlexboxLayout_wrapContent = test( + activity_flexbox_wrap_content, + noop, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + isBottomAlignedWith(text1, flexbox); + + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + isBottomAlignedWith(text2, flexbox); + + isRightOf(text3, text2); + isTopAlignedWith(text3, flexbox); + isBottomAlignedWith(text3, flexbox); + isRightAlignedWith(text3, flexbox); + } +); + +let activity_flexbox_wrapped_with_scrollview = () => getViews( + ` + + + ` +); + +export const testFlexboxLayout_wrapped_with_ScrollView = test( + activity_flexbox_wrapped_with_scrollview, + noop, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + + isBelow(text3, text1); + isBelow(text3, text2); + isLeftAlignedWith(text3, flexbox); + + equal(height(flexbox), height(text1) + height(text3)); + } +); + +let activity_flexbox_wrapped_with_horizontalscrollview = () => getViews( + ` + + + ` +); + +export const testFlexboxLayout_wrapped_with_HorizontalScrollView = test( + activity_flexbox_wrapped_with_horizontalscrollview, + noop, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + + isRightOf(text3, text1); + isRightOf(text3, text2); + isTopAlignedWith(text3, flexbox); + + equal(width(flexbox), width(text1) + width(text2) + width(text3)); + } +); + +let activity_justify_content_test = () => getViews( + ` + ` +); + +export const testJustifyContent_flexStart = test( + activity_justify_content_test, + noop, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isRightOf(text3, text2); + } +); + +let activity_justify_content_with_parent_padding = () => getViews( + ` + + + ` +); + +export const testJustifyContent_flexStart_withParentPadding = test( + activity_justify_content_with_parent_padding, + noop, + ({root, flexbox, text1, text2, text3}) => { + isRightOf(text2, text1); + isRightOf(text3, text2); + equal(left(text1), dipToDp(flexbox.paddingLeft), `Expected ${text1}.left to equal ${flexbox}.paddingLeft`); + equal(top(text1), dipToDp(flexbox.paddingTop), `Expected ${text1}.top to equal ${flexbox}.paddingTop`); + } +); + +export const testJustifyContent_flexEnd = test( + activity_justify_content_test, + ({flexbox}) => flexbox.justifyContent = JustifyContent.FLEX_END, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text3, flexbox); + isRightAlignedWith(text3, flexbox); + isLeftOf(text2, text3); + isLeftOf(text1, text2); + } +); + +export const testJustifyContent_flexEnd_withParentPadding = test( + activity_justify_content_with_parent_padding, + ({flexbox}) => flexbox.justifyContent = JustifyContent.FLEX_END, + ({root, flexbox, text1, text2, text3}) => { + isLeftOf(text2, text3); + isLeftOf(text1, text2); + equal(width(flexbox) - right(text3), dipToDp(flexbox.paddingRight)); + equal(top(text3), dipToDp(flexbox.paddingTop)); + } +); + +export const testJustifyContent_center = test( + activity_justify_content_test, + ({flexbox}) => flexbox.justifyContent = JustifyContent.CENTER, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + isRightOf(text3, text2); + isTopAlignedWith(text3, flexbox); + + let space = width(flexbox) - width(text1) - width(text2) - width(text3); + space = space / 2; + check(space - 1 <= left(text1) && left(text1) <= space + 1); + check(space - 1 <= width(flexbox) - right(text3) && width(flexbox) - right(text3) <= space + 1); + } +); + +export const testJustifyContent_center_withParentPadding = test( + activity_justify_content_with_parent_padding, + ({flexbox}) => flexbox.justifyContent = JustifyContent.CENTER, + ({root, flexbox, text1, text2, text3}) => { + isRightOf(text2, text1); + isRightOf(text3, text2); + let space = width(flexbox) - width(text1) - width(text2) - width(text3) - dipToDp(flexbox.paddingLeft) - dipToDp(flexbox.paddingRight); + space = space / 2; + check(space - 1 <= left(text1) - dipToDp(flexbox.paddingLeft) && left(text1) - dipToDp(flexbox.paddingLeft) <= space + 1); + check(space - 1 <= width(flexbox) - right(text3) - dipToDp(flexbox.paddingRight) && width(flexbox) - right(text3) - dipToDp(flexbox.paddingRight) <= space + 1); + } +); + +export const testJustifyContent_spaceBetween = test( + activity_justify_content_test, + ({flexbox}) => flexbox.justifyContent = JustifyContent.SPACE_BETWEEN, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isTopAlignedWith(text3, flexbox); + isRightAlignedWith(text3, flexbox); + let space = width(flexbox) - width(text1) - width(text2) - width(text3); + space = space / 2; + check(space - 1 <= left(text2) - right(text1) && left(text2) - right(text1) <= space + 1); + check(space - 1 <= left(text3) - right(text2) && left(text3) - right(text2) <= space + 1); + } +); + +export const testJustifyContent_spaceBetween_withPadding = test( + activity_justify_content_test, + ({flexbox}) => { + flexbox.justifyContent = JustifyContent.SPACE_BETWEEN; + flexbox.padding = padding; + }, + ({root, flexbox, text1, text2, text3}) => { + let space = width(flexbox) - width(text1) - width(text2) - width(text3) - dipToDp(padding) * 2; + space = space / 2; + equal(left(text1), dipToDp(padding)); + equal(width(flexbox) - right(text3), dipToDp(padding)); + check(space - 1 <= left(text2) - right(text1) && left(text2) - right(text1) <= space + 1); + check(space - 1 <= left(text3) - right(text2) && left(text3) - right(text2) <= space + 1); + } +); + +export const testJustifyContent_spaceAround = test( + activity_justify_content_test, + ({flexbox}) => flexbox.justifyContent = JustifyContent.SPACE_AROUND, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isTopAlignedWith(text3, flexbox); + + let space = width(flexbox) - width(text1) - width(text2) - width(text3); + space = space / 6; // Divide by the number of children * 2 + check(space - 1 <= left(text1) && left(text1) <= space + 1); + let spaceLowerBound = space * 2 - 1; + let spaceUpperBound = space * 2 + 1; + check(spaceLowerBound <= left(text2) - right(text1) && left(text2) - right(text1) <= spaceUpperBound); + check(spaceLowerBound <= left(text3) - right(text2) && left(text3) - right(text2) <= spaceUpperBound); + check(space - 1 <= width(flexbox) - right(text3) && width(flexbox) - right(text3) <= space + 1); + } +); + +const padding: any = 40; + +export const testJustifyContent_spaceAround_withPadding = test( + activity_justify_content_test, + ({flexbox}) => { + flexbox.justifyContent = JustifyContent.SPACE_AROUND; + flexbox.padding = padding; + }, + ({root, flexbox, text1, text2, text3}) => { + let space = width(flexbox) - width(text1) - width(text2) - width(text3) - dipToDp(padding) * 2; + space = space / 6; // Divide by the number of children * 2 + check(space - 1 <= left(text1) - dipToDp(padding) && left(text1) - dipToDp(padding) <= space + 1); + let spaceLowerBound = space * 2 - 1; + let spaceUpperBound = space * 2 + 1; + check(spaceLowerBound <= left(text2) - right(text1) && left(text2) - right(text1) <= spaceUpperBound); + check(spaceLowerBound <= left(text3) - right(text2) && left(text3) - right(text2) <= spaceUpperBound); + check(space - 1 <= width(flexbox) - right(text3) - dipToDp(padding) && width(flexbox) - right(text3) - dipToDp(padding) <= space + 1); + } +); + +export const testJustifyContent_flexStart_flexDirection_column = test( + activity_justify_content_test, + ({flexbox}) => flexbox.flexDirection = FlexDirection.COLUMN, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isBelow(text2, text1); + isLeftAlignedWith(text2, flexbox); + isBelow(text3, text2); + isLeftAlignedWith(text3, flexbox); + } +); + +export const testJustifyContent_flexEnd_flexDirection_column = test( + activity_justify_content_test, + ({flexbox}) => { + flexbox.justifyContent = JustifyContent.FLEX_END; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isBottomAlignedWith(text3, flexbox); + isRightAlignedWith(text3, flexbox); + isAbove(text2, text3); + isAbove(text1, text2); + } +); + +export const testJustifyContent_center_flexDirection_column = test( + activity_justify_content_test, + ({flexbox}) => { + flexbox.justifyContent = JustifyContent.CENTER; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isBelow(text2, text1); + isLeftAlignedWith(text2, flexbox); + isBelow(text3, text2); + isLeftAlignedWith(text3, flexbox); + + let space = height(flexbox) - height(text1) - height(text2) - height(text3); + space = space / 2; + check(space - 1 <= top(text1) && top(text1) <= space + 1); + check(space - 1 <= height(flexbox) - bottom(text3) && height(flexbox) - bottom(text3) <= space + 1); + } +); + +export const testJustifyContent_spaceBetween_flexDirection_column = test( + activity_justify_content_test, + ({flexbox}) => { + flexbox.justifyContent = JustifyContent.SPACE_BETWEEN; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isLeftAlignedWith(text3, flexbox); + isBottomAlignedWith(text3, flexbox); + + let space = height(flexbox) - height(text1) - height(text2) - height(text3); + space = space / 2; + check(space - 1 <= top(text2) - bottom(text1) && top(text2) - bottom(text1) <= space + 1); + check(space - 1 <= top(text3) - bottom(text2) && top(text3) - bottom(text2) <= space + 1); + } +); + +export const testJustifyContent_spaceBetween_flexDirection_column_withPadding = test( + activity_justify_content_test, + ({flexbox}) => { + flexbox.justifyContent = JustifyContent.SPACE_BETWEEN; + flexbox.flexDirection = FlexDirection.COLUMN; + flexbox.padding = padding; + }, + ({root, flexbox, text1, text2, text3}) => { + let space = height(flexbox) - height(text1) - height(text2) - height(text3) - dipToDp(padding) * 2; + space = space / 2; + equal(top(text1), dipToDp(padding)); + equal(height(flexbox) - bottom(text3), dipToDp(padding)); + check(space - 1 <= top(text2) - bottom(text1) && top(text2) - bottom(text1) <= space + 1); + check(space - 1 <= top(text3) - bottom(text2) && top(text3) - bottom(text2) <= space + 1); + } +); + +export const testJustifyContent_spaceAround_flexDirection_column = test( + activity_justify_content_test, + ({flexbox}) => { + flexbox.justifyContent = JustifyContent.SPACE_AROUND + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isLeftAlignedWith(text3, flexbox); + + let space = height(flexbox) - height(text1) - height(text2) - height(text3); + space = space / 6; // Divide by the number of children * 2 + check(space - 1 <= top(text1) && top(text1) <= space + 1); + let spaceLowerBound = space * 2 - 1; + let spaceUpperBound = space * 2 + 1; + check(spaceLowerBound <= top(text2) - bottom(text1) && top(text2) - bottom(text1) <= spaceUpperBound); + check(spaceLowerBound <= top(text3) - bottom(text2) && top(text3) - bottom(text2) <= spaceUpperBound); + check(space - 1 <= height(flexbox) - bottom(text3) && height(flexbox) - bottom(text3) <= space + 1); + } +); + +export const testJustifyContent_spaceAround_flexDirection_column_withPadding = test( + activity_justify_content_test, + ({flexbox}) => { + flexbox.justifyContent = JustifyContent.SPACE_AROUND; + flexbox.flexDirection = FlexDirection.COLUMN; + flexbox.padding = padding; + }, + ({root, flexbox, text1, text2, text3}) => { + let space = height(flexbox) - height(text1) - height(text2) - height(text3) - dipToDp(padding) * 2; + space = space / 6; // Divide by the number of children * 2 + check(space - 1 <= top(text1) - dipToDp(padding) && top(text1) - dipToDp(padding) <= space + 1); + let spaceLowerBound = space * 2 - 1; + let spaceUpperBound = space * 2 + 1; + check(spaceLowerBound <= top(text2) - bottom(text1) && top(text2) - bottom(text1) <= spaceUpperBound); + check(spaceLowerBound <= top(text3) - bottom(text2) && top(text3) - bottom(text2) <= spaceUpperBound); + check(space - 1 <= height(flexbox) - bottom(text3) - dipToDp(padding) && height(flexbox) - bottom(text3) - dipToDp(padding) <= space + 1); + } +); + +let activity_flex_grow_test = () => getViews( + ` + ` +); + +export const testFlexGrow_withExactParentLength = test( + activity_flex_grow_test, + noop, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + isTopAlignedWith(text3, flexbox); + isRightAlignedWith(text3, flexbox); + isRightOf(text3, text2); + + equal(width(text3), width(flexbox) - width(text1) - width(text2)); + } +); + +export const testFlexGrow_withExactParentLength_flexDirection_column = test( + activity_flex_grow_test, + ({flexbox}) => flexbox.flexDirection = FlexDirection.COLUMN, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + + isLeftAlignedWith(text3, flexbox); + isBottomAlignedWith(text3, flexbox); + isBelow(text3, text2); + + equal(height(text3), height(flexbox) - height(text1) - height(text2)); + } +); + +export const testFlexGrow_including_view_gone = test( + activity_flex_grow_test, + ({flexbox, text2}) => text2.visibility = "collapse", + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + + isTopAlignedWith(text3, flexbox); + isRightAlignedWith(text3, flexbox); + isRightOf(text3, text1); + + equal(width(text3), width(flexbox) - width(text1)); + } +); + +let activity_align_content_test = () => getViews( + ` + ` +); + +export const testAlignContent_stretch = test( + activity_align_content_test, + noop, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2,text1); + + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + + equal(top(text3), height(flexbox) / 2); + } +); + +export const testAlignContent_flexStart = test( + activity_align_content_test, + ({flexbox}) => flexbox.alignContent = AlignContent.FLEX_START, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + + equal(top(text3), height(text1)); + } +); + +export const testAlignContent_flexEnd = test( + activity_align_content_test, + ({flexbox}) => flexbox.alignContent = AlignContent.FLEX_END, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text3, flexbox); + isBottomAlignedWith(text3, flexbox); + isAbove(text1, text3); + isLeftAlignedWith(text1, flexbox); + isAbove(text2, text3); + + equal(bottom(text1), height(flexbox) - height(text3)); + } +); + +export const testAlignContent_flexEnd_parentPadding = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.alignContent = AlignContent.FLEX_END; + flexbox.padding = "32"; + }, + ({root, flexbox, text1, text2, text3}) => { + isAbove(text1, text3); + isAbove(text1, text3); + isAbove(text2, text3); + + equal(bottom(text3), height(flexbox) - dipToDp(flexbox.paddingBottom)); + } +); + +export const testAlignContent_flexEnd_parentPadding_column = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.alignContent = AlignContent.FLEX_END; + flexbox.flexDirection = FlexDirection.COLUMN; + flexbox.padding = "32"; + }, + ({root, flexbox, text1, text2, text3}) => { + isLeftOf(text1, text3); + isLeftOf(text2, text3); + + let { bounds1, bounds2 } = comparableBounds(text3, flexbox); + equal(bounds1.right, bounds2.right - dipToDp(flexbox.paddingRight)); + } +); + +export const testAlignContent_center = test( + activity_align_content_test, + ({flexbox}) => flexbox.alignContent = AlignContent.CENTER, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isBelow(text3, text1); + + let spaceAboveAndBottom = height(flexbox) - height(text1) - height(text3); + spaceAboveAndBottom /= 2; + + check(spaceAboveAndBottom - 1 <= top(text1) && top(text1) <= spaceAboveAndBottom + 1); + check(height(flexbox) - spaceAboveAndBottom - 1 <= bottom(text3) && bottom(text3) <= height(flexbox) - spaceAboveAndBottom + 1); + + // TODO: equal(flexbox.getFlexLines().size(), is(2)); + } +); + +export const testAlignContent_spaceBetween = test( + activity_align_content_test, + ({flexbox}) => flexbox.alignContent = AlignContent.SPACE_BETWEEN, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + isBottomAlignedWith(text3, flexbox); + isLeftAlignedWith(text3, flexbox); + // TODO: equal(flexbox.getFlexLines().size(), is(2)); + } +); + +export const testAlignContent_spaceBetween_withPadding = test( + activity_align_content_test, + ({flexbox}) => flexbox.alignContent = AlignContent.SPACE_BETWEEN, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isTopAlignedWith(text2, flexbox); + isBottomAlignedWith(text3, flexbox); + isLeftAlignedWith(text3, flexbox); + } +); + +export const testAlignContent_spaceAround = test( + activity_align_content_test, + ({flexbox}) => flexbox.alignContent = AlignContent.SPACE_AROUND, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + + let spaceAround = height(flexbox) - height(text1) - height(text3); + spaceAround /= 4; // Divide by the number of flex lines * 2 + + check(spaceAround - 1 <= top(text1) && top(text1) <= spaceAround + 1); + let spaceLowerBound = bottom(text1) + spaceAround * 2 - 1; + let spaceUpperBound = bottom(text1) + spaceAround * 2 + 1; + check(spaceLowerBound <= top(text3) && top(text3) <= spaceUpperBound); + // TODO: equal(flexbox.getFlexLines().size(), is(2)); + } +); + +export const testAlignContent_stretch_parentWrapContent = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.height = Number.NaN; // TODO: Check that "NaN" is auto-ish + flexbox.verticalAlignment = "top"; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + + equal(top(text3), height(text1)); + // TODO: equal(flexbox.getFlexLines().size(), is(2)); + } +); + +export const testAlignContent_stretch_flexDirection_column = test( + activity_align_content_test, + ({flexbox}) => flexbox.flexDirection = FlexDirection.COLUMN, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + // the third TextView is wrapped to the next flex line + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text1); + isRightOf(text3, text2); + + let flexLineCrossSize = width(flexbox) / 2; + equal(left(text3), flexLineCrossSize); + } +); + +export const testAlignContent_flexStart_flexDirection_column = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.alignContent = AlignContent.FLEX_START; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text1); + isRightOf(text3, text2); + + equal(left(text3), width(text1)); + } +); + +export const testAlignContent_flexEnd_flexDirection_column = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.alignContent = AlignContent.FLEX_END; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isRightAlignedWith(text3, flexbox); + isTopAlignedWith(text3, flexbox); + isLeftOf(text1, text3); + isTopAlignedWith(text1, flexbox); + isBelow(text2, text3); + + equal(right(text1), width(flexbox) - width(text3)); + } +); + +export const testAlignContent_center_flexDirection_column = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.alignContent = AlignContent.CENTER; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isRightOf(text3, text1); + + let spaceLeftAndRight = width(flexbox) - width(text1) - width(text3); + spaceLeftAndRight /= 2; + + check(spaceLeftAndRight - 1 <= left(text1) && left(text1) <= spaceLeftAndRight + 1); + let spaceLowerBound = width(flexbox) - spaceLeftAndRight - 1; + let spaceUpperBound = width(flexbox) - spaceLeftAndRight + 1; + check(spaceLowerBound <= right(text3) && right(text3) <= spaceUpperBound); + } +); + +export const testAlignContent_spaceBetween_flexDirection_column = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.alignContent = AlignContent.SPACE_BETWEEN; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isLeftAlignedWith(text2, flexbox); + isRightAlignedWith(text3, flexbox); + isTopAlignedWith(text3, flexbox); + } +); + +export const testAlignContent_spaceAround_flexDirection_column = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.alignContent = AlignContent.SPACE_AROUND; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isTopAlignedWith(text3, flexbox); + + let spaceAround = width(flexbox) - width(text1) - width(text3); + spaceAround /= 4; // Divide by the number of flex lines * 2 + + check(spaceAround - 1 <= left(text1) && left(text1) <= spaceAround + 1); + let spaceLowerBound = right(text1) + spaceAround * 2 - 1; + let spaceUpperBound = right(text1) + spaceAround * 2 + 1; + check(spaceLowerBound <= left(text3) && left(text3) <= spaceUpperBound); + } +); + +export const testAlignContent_stretch_parentWrapContent_flexDirection_column = test( + activity_align_content_test, + ({flexbox}) => { + flexbox.width = Number.NaN; // TODO: Check default is Number.NaN + flexbox.horizontalAlignment = "left"; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text1); + isRightOf(text3, text2); + + equal(left(text3), width(text1)); + } +); + +let activity_stretch_test = () => getViews( + ` + ` +); + +export const testAlignItems_stretch = test( + activity_stretch_test, + noop, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + + let flexLineSize = height(flexbox) / 2; + check(flexLineSize - 1 <= height(text1) && height(text1) <= flexLineSize + 1); + check(flexLineSize - 1 <= height(text2) && flexLineSize <= flexLineSize + 1); + check(flexLineSize - 1 <= height(text3) && height(text3) <= flexLineSize + 1); + } +); + +let activity_align_self_stretch_test = () => getViews( + ` + ` +); + +export const testAlignSelf_stretch = test( + activity_align_self_stretch_test, + noop, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + + let flexLineSize = height(flexbox) / 2; + + check(flexLineSize - 1 <= height(text1) && height(text1) <= flexLineSize + 1); + + // use eps. + notEqual(height(text2), flexLineSize); + notEqual(height(text3), flexLineSize); + } +); + +export const testAlignSelf_stretch_flexDirection_column = test( + activity_align_self_stretch_test, + ({flexbox}) => flexbox.flexDirection = FlexDirection.COLUMN, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text1); + isRightOf(text3, text2); + + let flexLineSize = width(flexbox) / 2; + check(flexLineSize - 1 <= width(text1) && width(text1) <= flexLineSize + 1); + // use eps. + notEqual(width(text2), flexLineSize); + notEqual(width(text3), flexLineSize); + } +); + +let activity_align_items_test = () => getViews( + ` + ` +); + +export const testAlignItems_flexStart = test( + activity_align_items_test, + ({flexbox}) => flexbox.alignItems = AlignItems.FLEX_START, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + + let flexLineSize = height(flexbox) / 2; + + notEqual(height(text1), flexLineSize); + notEqual(height(text2), flexLineSize); + notEqual(height(text3), flexLineSize); + check(flexLineSize - 1 <= top(text3) && top(text3) <= flexLineSize + 1); + } +); + +export const testAlignItems_flexEnd = test( + activity_align_items_test, + ({flexbox}) => flexbox.alignItems = AlignItems.FLEX_END, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + isBottomAlignedWith(text3, flexbox); + + let flexLineSize = height(flexbox) / 2; + + notEqual(height(text1), flexLineSize); + notEqual(height(text2), flexLineSize); + notEqual(height(text3), flexLineSize); + check(flexLineSize - 1 <= bottom(text1) && bottom(text1) <= flexLineSize + 1); + check(flexLineSize - 1 <= bottom(text2) && bottom(text2) <= flexLineSize + 1); + equal(bottom(text3), height(flexbox)); + } +); + +let activity_align_items_parent_padding_test = () => getViews( + ` + ` +); + +export const testAlignItems_flexEnd_parentPadding = test( + activity_align_items_parent_padding_test, + ({flexbox}) => flexbox.alignItems = AlignItems.FLEX_END, + ({root, flexbox, text1, text2, text3}) => { + isRightOf(text2, text1); + equal(bottom(text1), height(flexbox) - dipToDp(flexbox.paddingBottom)); + equal(bottom(text2), height(flexbox) - dipToDp(flexbox.paddingBottom)); + } +); + +export const testAlignItems_flexEnd_parentPadding_column = test( + activity_align_items_parent_padding_test, + ({flexbox}) => { + flexbox.alignItems = AlignItems.FLEX_END; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isBelow(text2, text1); + equal(right(text1), width(flexbox) - dipToDp(flexbox.paddingRight)); + equal(right(text2), width(flexbox) - dipToDp(flexbox.paddingRight)); + } +); + +export const testAlignItems_center = test( + activity_align_items_test, + ({flexbox}) => flexbox.alignItems = AlignItems.CENTER, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + + let flexLineSize = height(flexbox) / 2; + + let spaceAboveAndBelow = (flexLineSize - height(text1)) / 2; + notEqual(height(text1), flexLineSize); + notEqual(height(text2), flexLineSize); + notEqual(height(text3), flexLineSize); + check(spaceAboveAndBelow - 1 <= top(text1) && top(text1) <= spaceAboveAndBelow + 1); + check(spaceAboveAndBelow - 1 <= top(text2) && top(text2) <= spaceAboveAndBelow + 1); + check(flexLineSize + spaceAboveAndBelow - 1 <= top(text3) && top(text3) <= flexLineSize + spaceAboveAndBelow + 1); + } +); + +export const testAlignItems_flexEnd_wrapReverse = test( + activity_align_items_test, + ({flexbox}) => { + flexbox.flexWrap = FlexWrap.WRAP_REVERSE; + flexbox.alignItems = AlignItems.FLEX_END; + }, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + isTopAlignedWith(text3, flexbox); + + let flexLineSize = height(flexbox) / 2; + + notEqual(height(text1), flexLineSize); + notEqual(height(text2), flexLineSize); + notEqual(height(text3), flexLineSize); + let lowerBound = height(flexbox) - flexLineSize - 1; + let upperBound = height(flexbox) - flexLineSize + 1; + check(lowerBound <= top(text1) && top(text1) <= upperBound); + check(lowerBound <= top(text2) && top(text2) <= upperBound); + equal(top(text3), 0); + } +); + +export const testAlignItems_center_wrapReverse = test( + activity_align_items_test, + ({flexbox}) => { + flexbox.flexWrap = FlexWrap.WRAP_REVERSE; + flexbox.alignItems = AlignItems.CENTER; + }, + ({root, flexbox, text1, text2, text3}) => { + isLeftAlignedWith(text1, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + + let flexLineSize = height(flexbox) / 2; + + let spaceAboveAndBelow = (flexLineSize - height(text1)) / 2; + notEqual(height(text1), flexLineSize); + notEqual(height(text2), flexLineSize); + notEqual(height(text3), flexLineSize); + let lowerBound = height(flexbox) - spaceAboveAndBelow - 1; + let upperBound = height(flexbox) - spaceAboveAndBelow + 1; + check(lowerBound <= bottom(text1) && bottom(text1) <= upperBound); + check(lowerBound <= bottom(text2) && bottom(text2) <= upperBound); + check(height(flexbox) - flexLineSize - spaceAboveAndBelow - 1 <= bottom(text3) + && bottom(text3) <= height(flexbox) - flexLineSize - spaceAboveAndBelow + 1); + } +); + +export const testAlignItems_flexStart_flexDirection_column = test( + activity_align_items_test, + ({flexbox}) => flexbox.flexDirection = FlexDirection.COLUMN, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text1); + isRightOf(text3, text2); + + let flexLineSize = width(flexbox) / 2; + notEqual(width(text1), flexLineSize); + notEqual(width(text2), flexLineSize); + notEqual(width(text3), flexLineSize); + check(flexLineSize - 1 <= left(text3) && left(text3) <= flexLineSize + 1); + } +); + +export const testAlignItems_flexEnd_flexDirection_column = test( + activity_align_items_test, + ({flexbox}) => { + flexbox.alignItems = AlignItems.FLEX_END; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text1); + isRightOf(text3, text2); + isRightAlignedWith(text3, flexbox); + + let flexLineSize = height(flexbox) / 2; + + notEqual(width(text1), flexLineSize); + notEqual(width(text2), flexLineSize); + notEqual(width(text3), flexLineSize); + check(flexLineSize - 1 <= right(text1) && right(text1) <= flexLineSize + 1); + check(flexLineSize - 1 <= right(text2) && right(text2) <= flexLineSize + 1); + equal(right(text3), width(flexbox)); + } +) + +export const testAlignItems_center_flexDirection_column = test( + activity_align_items_test, + ({flexbox}) => { + flexbox.alignItems = AlignItems.CENTER; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text1); + isRightOf(text3, text2); + + let flexLineSize = width(flexbox) / 2; + + let spaceLeftAndRight = (flexLineSize - width(text1)) / 2; + notEqual(width(text1), flexLineSize); + notEqual(width(text2), flexLineSize); + notEqual(width(text3), flexLineSize); + check(spaceLeftAndRight - 1 <= left(text1) && left(text1) <= spaceLeftAndRight + 1); + check(spaceLeftAndRight - 1 <= left(text2) && left(text2) <= spaceLeftAndRight + 1); + check(flexLineSize + spaceLeftAndRight - 1 <= left(text3) && left(text2) <= flexLineSize + spaceLeftAndRight + 1); + } +); + +export const testAlignItems_flexEnd_wrapReverse_flexDirection_column = test( + activity_align_items_test, + ({flexbox}) => { + flexbox.flexWrap = FlexWrap.WRAP_REVERSE; + flexbox.alignItems = AlignItems.FLEX_END; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isLeftAlignedWith(text3, flexbox); + isTopAlignedWith(text3, flexbox); + + let flexLineSize = width(flexbox) / 2; + + notEqual(width(text1), flexLineSize); + notEqual(width(text2), flexLineSize); + notEqual(width(text3), flexLineSize); + let lowerBound = width(flexbox) - flexLineSize - 1; + let upperBound = width(flexbox) - flexLineSize + 1; + check(lowerBound <= left(text1) && left(text1) <= upperBound); + check(lowerBound <= left(text2) && left(text2) <= upperBound); + equal(left(text3), 0); + } +); + +export const testAlignItems_center_wrapReverse_flexDirection_column = test( + activity_align_items_test, + ({flexbox}) => { + flexbox.flexWrap = FlexWrap.WRAP_REVERSE; + flexbox.alignItems = AlignItems.CENTER; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isBelow(text2, text1); + isTopAlignedWith(text3, flexbox); + + let flexLineSize = width(flexbox) / 2; + + let spaceLeftAndRight = (flexLineSize - width(text1)) / 2; + notEqual(width(text1), flexLineSize); + notEqual(width(text2), flexLineSize); + notEqual(width(text3), flexLineSize); + let lowerBound = width(flexbox) - spaceLeftAndRight - 1; + let upperBound = width(flexbox) - spaceLeftAndRight + 1; + check(lowerBound <= right(text1) && right(text1) <= upperBound); + check(lowerBound <= right(text2) && right(text2) <= upperBound); + check(width(flexbox) - flexLineSize - spaceLeftAndRight - 1 <= right(text3) && right(text3) <= width(flexbox) - flexLineSize - spaceLeftAndRight + 1); + } +); + +let activity_align_items_baseline_test = () => getViews( + ` + ` +); + +export const testAlignItems_baseline = test( + activity_align_items_baseline_test, + noop, + ({root, flexbox, text1, text2, text3}) => { + let topPluBaseline1 = top(text1) + baseline(text1); + let topPluBaseline2 = top(text2) + baseline(text2); + let topPluBaseline3 = top(text3) + baseline(text3); + + equal(topPluBaseline1, topPluBaseline2); + equal(topPluBaseline2, topPluBaseline3); + } +); + +export const testAlignItems_baseline_wrapReverse = test( + activity_align_items_baseline_test, + ({flexbox}) => flexbox.flexWrap = FlexWrap.WRAP_REVERSE, + ({root, flexbox, text1, text2, text3}) => { + let bottomPluBaseline1 = bottom(text1) + baseline(text1); + let bottomPluBaseline2 = bottom(text2) + baseline(text2); + let bottomPluBaseline3 = bottom(text3) + baseline(text3); + + equal(bottomPluBaseline1, bottomPluBaseline2); + equal(bottomPluBaseline2, bottomPluBaseline3); + } +); + +let activity_flex_wrap_test = () => getViews( + ` + ` +) + +export const testFlexDirection_row_reverse = test( + activity_flex_wrap_test, + ({flexbox}) => flexbox.flexDirection = FlexDirection.ROW_REVERSE, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isRightAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isLeftOf(text2, text1); + isBelow(text3, text1); + isBelow(text3, text2); + isRightAlignedWith(text3, flexbox); + } +); + +export const testFlexDirection_column_reverse = test( + activity_flex_wrap_test, + ({flexbox}) => flexbox.flexDirection = FlexDirection.COLUMN_REVERSE, + ({root, flexbox, text1, text2, text3}) => { + isBottomAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isAbove(text2, text1); + isRightOf(text3, text1); + isRightOf(text3, text2); + isBottomAlignedWith(text3, flexbox); + } +); + +let activity_flex_basis_percent_test = () => getViews( + ` + ` +); + +export const testFlexBasisPercent_wrap = test( + activity_flex_basis_percent_test, + noop, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + isRightOf(text3, text2); + + // use eps. + closeEnough(width(text1), Math.round(width(flexbox) * 0.5)); + closeEnough(width(text2), Math.round(width(flexbox) * 0.6)); + } +); + +export const testFlexBasisPercent_nowrap = test( + activity_flex_basis_percent_test, + ({flexbox}) => flexbox.flexWrap = FlexWrap.NOWRAP, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text2); + + let totalWidth = width(text1) + width(text2) + width(text3); + check(totalWidth >= width(flexbox) - 3 || totalWidth <= width(flexbox) + 3); + } +); + +export const testFlexBasisPercent_wrap_flexDirection_column = test( + activity_flex_basis_percent_test, + ({flexbox}) => flexbox.flexDirection = FlexDirection.COLUMN, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + isBelow(text3, text2); + + closeEnough(height(text1), Math.round(height(flexbox) * 0.5)); + closeEnough(height(text2), Math.round(height(flexbox) * 0.6)); + } +); + +export const testFlexBasisPercent_nowrap_flexDirection_column = test( + activity_flex_basis_percent_test, + ({flexbox}) => { + flexbox.flexWrap = FlexWrap.NOWRAP; + flexbox.flexDirection = FlexDirection.COLUMN; + }, + ({root, flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text2); + + let totalHeight = height(text1) + height(text2) + height(text3); + + check(totalHeight >= height(flexbox) - 3 || totalHeight <= height(flexbox) + 3); + } +); + +let activity_minwidth_test = () => getViews( + ` + ` + ); + +export const testMinWidth_initial_width_less_than_minWidth = test( + activity_minwidth_test, + noop, + ({root, flexbox, text1, text2, text3}) => { + let minWidth = 100; + equal(width(text1), dipToDp(100)); + equal(width(text2), width(flexbox) - dipToDp(100)); + } +); + +let activity_minwidth_lower_bound_test = () => getViews( + ` + ` +); + +export const testMinWidth_works_as_lower_bound_shrink_to = test( + activity_minwidth_lower_bound_test, + noop, + ({root, flexbox, text1, text2, text3, text4}) => { + equal(width(text1), dipToDp(150)); + equal(width(flexbox), width(text1) + width(text2) + width(text3) + width(text4)); + } +); + +let activity_minheight_test = () => getViews( + ` + ` +); + +export const testMinHeight_initial_height_less_than_minHeight = test( + activity_minheight_test, + noop, + ({root, flexbox, text1, text2}) => { + equal(height(text1), dipToDp(100)); + equal(height(text2), height(flexbox) - dipToDp(100)); + } +); + +let activity_minheight_lower_bound_test = () => getViews( + ` + ` +); + +export const testMinHeight_works_as_lower_bound_shrink_to = test( + activity_minheight_lower_bound_test, + noop, + ({root, flexbox, text1, text2, text3, text4}) => { + equal(height(text1), dipToDp(150)); + equal(height(flexbox), height(text1) + height(text2) + height(text3) + height(text4)); + } +); + +// We do not support maxWidth/maxHeight +// omit: testMaxWidth_initial_width_more_than_maxWidth +// omit: testMaxWidth_works_as_upper_bound_expand_to +// omit: testMaxHeight_initial_height_more_than_maxHeight +// omit: testMaxHeight_works_as_lower_bound_expand_to + +let activity_views_visibility_gone = () => getViews( + ` + ` +); + +export const testView_visibility_gone = test( + activity_views_visibility_gone, + noop, + ({root, flexbox, text1, text2, text3, text4, text5}) => { + isTopAlignedWith(text3, flexbox); + isLeftAlignedWith(text3, flexbox); + isTopAlignedWith(text4, flexbox); + isRightOf(text4, text3); + isLeftAlignedWith(text5, flexbox); + isBelow(text5, text3); + + equal(left(text4), right(text3)); + equal(top(text5), bottom(text3)); + } +); + +let activity_visibility_gone_first_item_in_flex_line_row = () => getViews( + ` + ` +); + +export const testView_visibility_gone_first_item_in_flex_line_horizontal = test( + activity_visibility_gone_first_item_in_flex_line_row, + noop, + ({root, flexbox, text1, text2, text3}) => { + check(height(flexbox) > 0); + equal(height(flexbox), height(text1) + height(text3)); + } +); + +let activity_visibility_gone_first_item_in_flex_line_column = () => getViews( + ` + ` +); + +export const testView_visibility_gone_first_item_in_flex_line_vertical = test( + activity_visibility_gone_first_item_in_flex_line_column, + noop, + ({flexbox, text1, text3}) => { + check(width(flexbox) > 0); + equal(width(flexbox), width(text1) + width(text3)); + } +); + +let activity_wrap_before_test = () => getViews( + ` + ` +); + +export const testWrapBefore = test( + activity_wrap_before_test, + noop, + ({flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isBelow(text2, text1); + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text2); + } +); + +export const testWrapBefore2 = test( + activity_wrap_before_test, + ({text2}) => FlexboxLayout.setFlexWrapBefore(text2, false), + ({flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + isLeftAlignedWith(text3, flexbox); + isBelow(text3, text1); + isBelow(text3, text2); + equal(height(flexbox), height(text1) + height(text3)); + } +) + +export const testWrapBefore_nowrap = test( + activity_wrap_before_test, + ({flexbox}) => flexbox.flexWrap = FlexWrap.NOWRAP, + ({flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isBottomAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isRightOf(text2, text1); + isBottomAlignedWith(text2, flexbox); + isTopAlignedWith(text3, flexbox); + isRightOf(text3, text2); + isBottomAlignedWith(text3, flexbox); + } +); + +let activity_wrap_parent_padding_horizontal_test = () => getViews( + ` + ` +); + +export const testWrap_parentPadding_horizontal = test( + activity_wrap_parent_padding_horizontal_test, + noop, + ({flexbox, text1, text2, text3}) => { + isBelow(text2, text1); + isRightOf(text3, text2); + equal(height(flexbox), dipToDp(flexbox.paddingTop) + dipToDp(flexbox.paddingBottom) + height(text1) + height(text2)); + } +); + +let activity_wrap_parent_padding_vertical_test = () => getViews( + ` + ` +); + +export const testWrap_parentPadding_vertical = test( + activity_wrap_parent_padding_vertical_test, + noop, + ({flexbox, text1, text2, text3}) => { + isRightOf(text2, text1); + isBelow(text3, text2); + equal(width(flexbox), dipToDp(flexbox.paddingLeft) + dipToDp(flexbox.paddingRight) + width(text1) + width(text2)); + } +); + +let activity_wrap_child_margin_horizontal_test = () => getViews( + ` + ` +) + +export const testWrap_childMargin_horizontal = test( + activity_wrap_child_margin_horizontal_test, + noop, + ({flexbox, text1, text2, text3}) => { + isBelow(text2, text1); + isRightOf(text3, text2); + equal(height(flexbox), height(text1) + height(text2) + dipToDp(text2.marginTop) + dipToDp(text2.marginBottom)); + } +); + +let activity_first_item_large_horizontal_test = () => getViews( + ` + ` +); + +export const testFirstItemLarge_horizontal = test( + activity_first_item_large_horizontal_test, + noop, + ({flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isLeftAlignedWith(text2, flexbox); + isLeftAlignedWith(text3, flexbox); + isBottomAlignedWith(text3, flexbox); + + equal(height(flexbox), height(text1) + height(text2) + height(text3)); + } +); + +let activity_first_item_large_vertical_test = () => getViews( + ` + ` +) + +export const testFirstItemLarge_vertical = test( + activity_first_item_large_vertical_test, + noop, + ({flexbox, text1, text2, text3}) => { + isTopAlignedWith(text1, flexbox); + isLeftAlignedWith(text1, flexbox); + isTopAlignedWith(text2, flexbox); + isTopAlignedWith(text3, flexbox); + isRightAlignedWith(text3, flexbox); + + equal(width(flexbox), width(text1) + width(text2) + width(text3)); + } +); + +let activity_wrap_child_margin_vertical_test = () => getViews( + ` + ` +); + +export const testWrap_childMargin_vertical = test( + activity_wrap_child_margin_vertical_test, + noop, + ({flexbox, text1, text2, text3}) => { + isRightOf(text2, text1); + isBelow(text3, text2); + // dips anyone? + equal(width(flexbox), width(text1) + width(text2) + dipToDp(text2.marginLeft) + dipToDp(text2.marginRight)); + } +); + +// Omit testEmptyChildren +// Omit testDivider_directionRow_verticalBeginning + +// Omit divider test family, we don't draw dividers diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 3651d8054..39310fe14 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -10,7 +10,9 @@ "experimentalDecorators": true, "diagnostics": true, "sourceMap": true, - "noLib": true + "noLib": true, + "jsx": "react", + "reactNamespace": "UIBuilder" }, "exclude": [ "node_modules", diff --git a/tns-core-modules/ui/core/view-common.ts b/tns-core-modules/ui/core/view-common.ts index 6e89a472f..1548c548e 100644 --- a/tns-core-modules/ui/core/view-common.ts +++ b/tns-core-modules/ui/core/view-common.ts @@ -727,6 +727,12 @@ export class View extends ProxyObject implements definition.View { return this._measuredHeight & utils.layout.MEASURED_SIZE_MASK; } + public getMeasuredState(): number { + return (this._measuredWidth & utils.layout.MEASURED_STATE_MASK) + | ((this._measuredHeight >> utils.layout.MEASURED_HEIGHT_STATE_SHIFT) + & (utils.layout.MEASURED_STATE_MASK >> utils.layout.MEASURED_HEIGHT_STATE_SHIFT)); + } + public setMeasuredDimension(measuredWidth: number, measuredHeight: number): void { this._measuredWidth = measuredWidth; this._measuredHeight = measuredHeight; @@ -786,6 +792,10 @@ export class View extends ProxyObject implements definition.View { return Math.round(result + 0.499) | (childMeasuredState & utils.layout.MEASURED_STATE_MASK); } + public static combineMeasuredStates(curState: number, newState): number { + return curState | newState; + } + public static layoutChild(parent: View, child: View, left: number, top: number, right: number, bottom: number): void { if (!child || !child._isVisible) { return; diff --git a/tns-core-modules/ui/core/view.android.ts b/tns-core-modules/ui/core/view.android.ts index 7ee3cf10e..b7732c04c 100644 --- a/tns-core-modules/ui/core/view.android.ts +++ b/tns-core-modules/ui/core/view.android.ts @@ -12,6 +12,8 @@ import background = require("ui/styling/background"); import {CommonLayoutParams} from "ui/styling/style"; import {device} from "platform"; +var flexbox; + global.moduleMerge(viewCommon, exports); var ANDROID = "_android"; @@ -607,6 +609,13 @@ export class ViewStyler implements style.Styler { lp.rightMargin = Math.round(params.rightMargin * utils.layout.getDisplayDensity()); lp.bottomMargin = Math.round(params.bottomMargin * utils.layout.getDisplayDensity()); lp.gravity = gravity; + + if (lp instanceof org.nativescript.widgets.FlexboxLayout.LayoutParams) { + if (!flexbox) { + flexbox = require("ui/layouts/flexbox-layout"); + } + flexbox._setAndroidLayoutParams(lp, view); + } } else { let layoutParams: any = lp; diff --git a/tns-core-modules/ui/core/view.d.ts b/tns-core-modules/ui/core/view.d.ts index a7049e041..f2170e778 100644 --- a/tns-core-modules/ui/core/view.d.ts +++ b/tns-core-modules/ui/core/view.d.ts @@ -394,6 +394,8 @@ declare module "ui/core/view" { */ public getMeasuredHeight(): number; + public getMeasuredState(): number; + /** * Call this when something has changed which has invalidated the layout of this view. This will schedule a layout pass of the view tree. */ @@ -462,6 +464,8 @@ declare module "ui/core/view" { */ public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number; + public static combineMeasuredStates(curState: number, newState): number; + /** * Returns the child view with the specified id. */ diff --git a/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout-common.ts b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout-common.ts index 1d82b43db..ce2af2a29 100644 --- a/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout-common.ts +++ b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout-common.ts @@ -7,6 +7,10 @@ import * as platform from "platform"; export type Basis = "auto" | number; +const ORDER_DEFAULT = 1; +const FLEX_GROW_DEFAULT = 0.0; +const FLEX_SHRINK_DEFAULT = 1.0; + // on Android we explicitly set propertySettings to None because android will invalidate its layout (skip unnecessary native call). var AffectsLayout = platform.device.os === platform.platformNames.android ? PropertyMetadataSettings.None : PropertyMetadataSettings.AffectsLayout; @@ -63,6 +67,11 @@ let validJustifyContent = { "space-around": true } +export type FlexBasisPercent = number; +export namespace FlexBasisPercent { + export const DEFAULT: number = -1; +} + function validateJustifyContent(value: any): boolean { return value in validJustifyContent; } @@ -140,10 +149,11 @@ export abstract class FlexboxLayoutBase extends LayoutBase { public static alignContentProperty = new Property("alignContent", "FlexboxLayout", new PropertyMetadata("stretch", AffectsLayout, undefined, validateAlignContent, (args: any) => args.object.setNativeAlignContent(args.newValue))); // TODO: Validation: - public static orderProperty = new Property("order", "FlexboxLayout", new PropertyMetadata(1, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onOrderPropertyChanged(element, oldValue, newValue)))); - public static flexGrowProperty = new Property("flexGrow", "FlexboxLayout", new PropertyMetadata(0, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onFlexGrowPropertyChanged(element, oldValue, newValue)))); - public static flexShrinkProperty = new Property("flexShrink", "FlexboxLayout", new PropertyMetadata(1, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onFlexShrinkPropertyChanged(element, oldValue, newValue)))); - public static alignSelfProperty = new Property("alignSelf", "FlexboxLayout", new PropertyMetadata(-1, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onAlignSelfPropertyChanged(element, oldValue, newValue)))); + public static orderProperty = new Property("order", "FlexboxLayout", new PropertyMetadata(ORDER_DEFAULT, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onOrderPropertyChanged(element, oldValue, newValue)))); + public static flexGrowProperty = new Property("flexGrow", "FlexboxLayout", new PropertyMetadata(FLEX_GROW_DEFAULT, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onFlexGrowPropertyChanged(element, oldValue, newValue)))); + public static flexShrinkProperty = new Property("flexShrink", "FlexboxLayout", new PropertyMetadata(FLEX_SHRINK_DEFAULT, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onFlexShrinkPropertyChanged(element, oldValue, newValue)))); + public static flexWrapBeforeProperty = new Property("flexWrapBefore", "FlexboxLayout", new PropertyMetadata(false, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onFlexWrapBeforePropertyChanged(element, oldValue, newValue)))) + public static alignSelfProperty = new Property("alignSelf", "FlexboxLayout", new PropertyMetadata(AlignSelf.AUTO, PropertyMetadataSettings.None, FlexboxLayoutBase.childHandler((flexbox, element, oldValue, newValue) => flexbox.onAlignSelfPropertyChanged(element, oldValue, newValue)))); constructor() { super(); @@ -177,10 +187,10 @@ export abstract class FlexboxLayoutBase extends LayoutBase { this._setValue(FlexboxLayoutBase.alignItemsProperty, value); } - get alignContent(): AlignItems { + get alignContent(): AlignContent { return this._getValue(FlexboxLayoutBase.alignContentProperty); } - set alignContent(value: AlignItems) { + set alignContent(value: AlignContent) { this._setValue(FlexboxLayoutBase.alignContentProperty, value); } @@ -212,8 +222,11 @@ export abstract class FlexboxLayoutBase extends LayoutBase { return validateArgs(view)._getValue(FlexboxLayoutBase.alignSelfProperty); } - protected onOrderPropertyChanged(element: View, oldValue: number, newValue: number): void { - console.log("order changed: " + newValue + " " + element); + public static setFlexWrapBefore(view: View, wrap: boolean) { + view._setValue(FlexboxLayoutBase.flexWrapBeforeProperty, wrap); + } + public static getFlexWrapBefore(view: View): boolean { + return view._getValue(FlexboxLayoutBase.flexWrapBeforeProperty); } protected abstract setNativeFlexDirection(flexDirection: FlexDirection); @@ -222,17 +235,11 @@ export abstract class FlexboxLayoutBase extends LayoutBase { protected abstract setNativeAlignItems(alignItems: AlignItems); protected abstract setNativeAlignContent(alignContent: AlignContent); - protected onFlexGrowPropertyChanged(element: View, oldValue: number, newValue: number): void { - console.log("flex-grow changed: " + newValue + " " + element); - } - - protected onFlexShrinkPropertyChanged(element: View, oldValue: number, newValue: number): void { - console.log("flex-shrink changed: " + newValue + " " + element); - } - - protected onAlignSelfPropertyChanged(element: View, oldValue: AlignSelf, newValue: AlignSelf): void { - console.log("align-self changed: " + newValue + " " + element); - } + protected abstract onOrderPropertyChanged(element: View, oldValue: number, newValue: number): void; + protected abstract onFlexGrowPropertyChanged(element: View, oldValue: number, newValue: number): void; + protected abstract onFlexShrinkPropertyChanged(element: View, oldValue: number, newValue: number): void; + protected abstract onAlignSelfPropertyChanged(element: View, oldValue: AlignSelf, newValue: AlignSelf): void; + protected abstract onFlexWrapBeforePropertyChanged(element: View, oldValue: boolean, newValue: boolean): void; private static childHandler(handler: (flexbox: FlexboxLayoutBase, element: View, oldValue: V, newValue: V) => void) { return (data: PropertyChangeData) => { @@ -260,4 +267,7 @@ registerSpecialProperty("flexShrink", (instance, propertyValue) => { registerSpecialProperty("alignSelf", (instance, propertyValue) => { FlexboxLayoutBase.setAlignSelf(instance, propertyValue); }); +registerSpecialProperty("flexWrapBefore", (instance, propertyValue) => { + FlexboxLayoutBase.setFlexWrapBefore(instance, propertyValue); +}); // No flex-basis in our implementation. \ No newline at end of file diff --git a/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.android.ts b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.android.ts index 90b98a276..9a9c9cbb1 100644 --- a/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.android.ts +++ b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.android.ts @@ -8,6 +8,7 @@ import { AlignSelf, FlexboxLayoutBase } from "./flexbox-layout-common"; +import {layout} from "utils/utils"; export * from "./flexbox-layout-common"; @@ -65,7 +66,6 @@ export class FlexboxLayout extends FlexboxLayoutBase { constructor() { super(); - console.log("New FlexBoxLayout!"); } get android(): FlexboxLayoutWidget { return this._layout; } @@ -77,56 +77,63 @@ export class FlexboxLayout extends FlexboxLayoutBase { protected setNativeFlexDirection(flexDirection: FlexDirection) { let value = flexDirectionMap[flexDirection]; - console.log("setNativeFlexDirection: " + flexDirection + " -> " + value); this.android.setFlexDirection(value); } protected setNativeFlexWrap(flexWrap: FlexWrap) { - console.log("flexWrap: " + flexWrap); this.android.setFlexWrap(flexWrapMap[flexWrap]); } protected setNativeJustifyContent(justifyContent: JustifyContent) { - console.log("setNativeJustifyContent: " + justifyContent); this.android.setJustifyContent(justifyContentMap[justifyContent]); } protected setNativeAlignItems(alignItems: AlignItems) { - console.log("setNativeAlignItems: " + alignItems); this.android.setAlignItems(alignItemsMap[alignItems]); } protected setNativeAlignContent(alignContent: AlignContent) { - console.log("setNativeAlignContent: " + alignContent); this.android.setAlignContent(alignContentMap[alignContent]); } protected onOrderPropertyChanged(view: View, oldValue: number, newValue: number): void { - console.log("order changed: " + newValue + " " + view); this.setLayoutParamsProperty(view, lp => lp.order = newValue); } protected onFlexGrowPropertyChanged(view: View, oldValue: number, newValue: number): void { - console.log("flex-grow changed: " + newValue + " " + view); this.setLayoutParamsProperty(view, lp => lp.flexGrow = newValue); } protected onFlexShrinkPropertyChanged(view: View, oldValue: number, newValue: number): void { - console.log("flex-shrink changed: " + newValue + " " + view); this.setLayoutParamsProperty(view, lp => lp.flexShrink = newValue); } protected onAlignSelfPropertyChanged(view: View, oldValue: AlignSelf, newValue: AlignSelf): void { - console.log("align-self changed: " + newValue + " " + view); this.setLayoutParamsProperty(view, lp => lp.alignSelf = alignSelfMap[newValue]); } + protected onFlexWrapBeforePropertyChanged(view: View, oldValue: boolean, newValue: boolean): void { + this.setLayoutParamsProperty(view, lp => lp.wrapBefore = newValue); + } + private setLayoutParamsProperty(view: View, setter: (lp: org.nativescript.widgets.FlexboxLayout.LayoutParams) => void) { let nativeView: android.view.View = view._nativeView; - var lp = nativeView.getLayoutParams() || new org.nativescript.widgets.FlexboxLayout.LayoutParams(); - if (lp instanceof org.nativescript.widgets.FlexboxLayout.LayoutParams) { - setter(lp); - nativeView.setLayoutParams(lp); + if (nativeView) { + var lp = nativeView.getLayoutParams() || new org.nativescript.widgets.FlexboxLayout.LayoutParams(); + if (lp instanceof org.nativescript.widgets.FlexboxLayout.LayoutParams) { + setter(lp); + nativeView.setLayoutParams(lp); + } } } } + +export function _setAndroidLayoutParams(lp: org.nativescript.widgets.FlexboxLayout.LayoutParams, view: View) { + lp.order = FlexboxLayout.getOrder(view); + lp.flexGrow = FlexboxLayout.getFlexGrow(view); + lp.flexShrink = FlexboxLayout.getFlexShrink(view); + lp.alignSelf = alignSelfMap[FlexboxLayout.getAlignSelf(view)]; + lp.wrapBefore = FlexboxLayout.getFlexWrapBefore(view); + lp.minWidth = layout.toDevicePixels(view.minWidth); + lp.minHeight = layout.toDevicePixels(view.minHeight); +} diff --git a/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.d.ts b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.d.ts index 7dea7b4f8..47692dae2 100644 --- a/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.d.ts +++ b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.d.ts @@ -1,6 +1,8 @@ declare module "ui/layouts/flexbox-layout" { import {View} from "ui/core/view"; + import {LayoutBase} from "ui/layouts/layout-base"; + import {PropertyMetadata} from "ui/core/proxy"; export type FlexDirection = "row" | "row-reverse" | "column" | "column-reverse"; export namespace FlexDirection { @@ -55,7 +57,18 @@ declare module "ui/layouts/flexbox-layout" { export const STRETCH: "stretch"; } - export class FlexboxLayout { + export class FlexboxLayout extends LayoutBase { + public static flexDirectionProperty: PropertyMetadata; + public static flexWrapProperty: PropertyMetadata; + public static justifyContentProperty: PropertyMetadata; + public static alignItemsProperty: PropertyMetadata; + + public flexDirection: FlexDirection; + public flexWrap: FlexWrap; + public justifyContent: JustifyContent; + public alignItems: AlignItems; + public alignContent: AlignContent; + public static setOrder(view: View, order: number); public static getOrder(view: View): number; @@ -67,5 +80,8 @@ declare module "ui/layouts/flexbox-layout" { public static setAlignSelf(view: View, align: AlignSelf); public static getAlignSelf(view: View): AlignSelf; + + public static setFlexWrapBefore(view: View, wrap: boolean); + public static getFlexWrapBefore(view: View): boolean; } } \ No newline at end of file diff --git a/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.internal.d.ts b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.internal.d.ts new file mode 100644 index 000000000..6b3a03ed9 --- /dev/null +++ b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.internal.d.ts @@ -0,0 +1,6 @@ +//@private +declare module "ui/layouts/flex-box" { + import {View} from "ui/core/view"; + export function _setAndroidLayoutParams(lp: any /* org.nativescript.widgets.FlexboxLayout.LayoutParams */, view: View); +} +//@endprivate diff --git a/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.ios.ts b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.ios.ts new file mode 100644 index 000000000..3f6df6b57 --- /dev/null +++ b/tns-core-modules/ui/layouts/flexbox-layout/flexbox-layout.ios.ts @@ -0,0 +1,1448 @@ +import { + FlexDirection, + FlexWrap, + JustifyContent, + AlignItems, + AlignContent, + AlignSelf, + FlexboxLayoutBase, + FlexBasisPercent +} from "./flexbox-layout-common"; + +export * from "./flexbox-layout-common"; + +import {CommonLayoutParams, nativeLayoutParamsProperty} from "ui/styling/style"; +import {LayoutBase} from "ui/layouts/layout-base"; +import {View} from "ui/core/view"; +import * as utils from "utils/utils"; +import * as enums from "ui/enums"; + +import EXACTLY = utils.layout.EXACTLY; +import AT_MOST = utils.layout.AT_MOST; +import UNSPECIFIED = utils.layout.UNSPECIFIED; + +import MEASURED_SIZE_MASK = utils.layout.MEASURED_SIZE_MASK; +import MEASURED_STATE_TOO_SMALL = utils.layout.MEASURED_STATE_TOO_SMALL; + +const MATCH_PARENT = -1; +const WRAP_CONTENT = -2; + +const View_sUseZeroUnspecifiedMeasureSpec = true; // NOTE: android version < M + +// Long ints may not be safe in JavaScript +const MAX_SIZE = 0x00FFFFFF & MEASURED_SIZE_MASK; + +import makeMeasureSpec = utils.layout.makeMeasureSpec; +import getMeasureSpecMode = utils.layout.getMeasureSpecMode; +import getMeasureSpecSize = utils.layout.getMeasureSpecSize; + +class FlexLine { + + _left: number = Number.MAX_VALUE; + _top: number = Number.MAX_VALUE; + _right: number = Number.MAX_VALUE; + _bottom: number = Number.MAX_VALUE; + + _mainSize: number = 0; + _dividerLengthInMainSize = 0; + _crossSize: number = 0; + _itemCount: number = 0; + _totalFlexGrow: number = 0; + _totalFlexShrink: number = 0; + _maxBaseline: number = 0; + + _indicesAlignSelfStretch: number[] = []; + + get left(): number { return this._left; } + get top(): number { return this._top; } + get right(): number { return this._right; } + get bottom(): number { return this._bottom; } + + get mainSize(): number { return this._mainSize; } + get crossSize(): number { return this._crossSize; } + get itemCount(): number { return this._itemCount; } + get totalFlexGrow(): number { return this._totalFlexGrow; } + get totalFlexShrink(): number { return this._totalFlexShrink; } +} + +class Order { + index: number; + order: number; + + public compareTo(another: Order): number { + if (this.order !== another.order) { + return this.order - another.order; + } + return this.index - another.index; + } +} + +export class FlexboxLayout extends FlexboxLayoutBase { + + // Omit divider + + private _reorderedIndices: number[]; + + private _orderCache: number[]; + private _flexLines: FlexLine[] = []; + private _childrenFrozen: boolean[]; + + public FlexboxLayout() { + // + } + + protected setNativeFlexDirection(flexDirection: FlexDirection) { + // lint happy no-op + } + protected setNativeFlexWrap(flexWrap: FlexWrap) { + // lint happy no-op + } + protected setNativeJustifyContent(justifyContent: JustifyContent) { + // lint happy no-op + } + protected setNativeAlignItems(alignItems: AlignItems) { + // lint happy no-op + } + protected setNativeAlignContent(alignContent: AlignContent) { + // lint happy no-op + } + + protected onOrderPropertyChanged(element: View, oldValue: number, newValue: number): void { + // lint happy no-op + } + protected onFlexGrowPropertyChanged(element: View, oldValue: number, newValue: number): void { + // lint happy no-op + } + protected onFlexShrinkPropertyChanged(element: View, oldValue: number, newValue: number): void { + // lint happy no-op + } + protected onAlignSelfPropertyChanged(element: View, oldValue: AlignSelf, newValue: AlignSelf): void { + // lint happy no-op + } + protected onFlexWrapBeforePropertyChanged(element: View, oldValue: boolean, newValue: boolean): void { + // lint happy no-op + } + + public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void { + LayoutBase.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + // Omit: super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (this._isOrderChangedFromLastMeasurement) { + this._reorderedIndices = this._createReorderedIndices(); + } + if (!this._childrenFrozen || this._childrenFrozen.length < this.getChildrenCount()) { + this._childrenFrozen = new Array(this.getChildrenCount()); + } + + switch(this.flexDirection) { + case FlexDirection.ROW: + case FlexDirection.ROW_REVERSE: + this._measureHorizontal(widthMeasureSpec, heightMeasureSpec); + break; + case FlexDirection.COLUMN: + case FlexDirection.COLUMN_REVERSE: + this._measureVertical(widthMeasureSpec, heightMeasureSpec); + break; + default: + throw new Error("Invalid value for the flex direction is set: " + this.flexDirection); + } + } + + private _getReorderedChildAt(index: number): View { + if (index < 0 || index >= this._reorderedIndices.length) { + return null; + } + return this.getChildAt(this._reorderedIndices[index]); + } + + public addChild(child: View) { + let index = this.getChildrenCount(); // Goes last + this._reorderedIndices = this._createReorderedIndices(child, index, FlexboxLayout.getLayoutParams(child)); + super.addChild(child); + } + + public insertChild(child: View, index: number): void { + this._reorderedIndices = this._createReorderedIndices(child, index, FlexboxLayout.getLayoutParams(child)); + super.addChild(child); + } + + private _createReorderedIndices(viewBeforeAdded: View, indexForViewBeforeAdded: number, paramsForViewBeforeAdded: FlexboxLayout.LayoutParams): number[]; + private _createReorderedIndices(): number[]; + private _createReorderedIndices(viewBeforeAdded?: View, indexForViewBeforeAdded?: number, paramsForViewBeforeAdded?: FlexboxLayout.LayoutParams) + { + if (arguments.length === 0) { + let childCount = this.getChildrenCount(); + let orders = this._createOrders(childCount); + return this._sortOrdersIntoReorderedIndices(childCount, orders); + } else { + let childCount: number = this.getChildrenCount(); + let orders = this._createOrders(childCount); + let orderForViewToBeAdded = new Order(); + if (viewBeforeAdded !== null) { + orderForViewToBeAdded.order = paramsForViewBeforeAdded.order; + } else { + orderForViewToBeAdded.order = 1 /* Default order */; + } + + if (indexForViewBeforeAdded === -1 || indexForViewBeforeAdded === childCount) { + orderForViewToBeAdded.index = childCount; + } else if (indexForViewBeforeAdded) { + orderForViewToBeAdded.index = indexForViewBeforeAdded; + for (let i = indexForViewBeforeAdded; i < childCount; i++) { + orders[i].index++; + } + } else { + orderForViewToBeAdded.index = childCount; + } + orders.push(orderForViewToBeAdded); + + return this._sortOrdersIntoReorderedIndices(childCount + 1, orders); + } + } + + private _sortOrdersIntoReorderedIndices(childCount: number, orders: Order[]): number[] { + orders.sort(/* TODO: Orders... use the comparer? */); + if (!this._orderCache) { + this._orderCache = []; + } + + this._orderCache.length = 0; + let reorderedIndices: number[] = []; + orders.forEach((order, i) => { + reorderedIndices[i] = order.index; + this._orderCache[i] = order.order; + }); + return reorderedIndices; + } + + private _createOrders(childCount: number): Order[] { + let orders: Order[] = []; + for (let i = 0; i < childCount; i++) { + let child = this.getChildAt(i); + let params = FlexboxLayout.getLayoutParams(child); + let order = new Order(); + order.order = params.order; + order.index = i; + orders.push(order); + } + return orders; + } + + private get _isOrderChangedFromLastMeasurement(): boolean { + let childCount = this.getChildrenCount(); + if (this._orderCache === null) { + this._orderCache = []; + } + if (this._orderCache.length !== childCount) { + return true; + } + for (let i = 0; i < childCount; i++) { + let view = this.getChildAt(i); + if (view === null) { + continue; + } + let lp = FlexboxLayout.getLayoutParams(view); + if (lp.order !== this._orderCache[i]) { + return true; + } + } + return false; + } + + private _measureHorizontal(widthMeasureSpec: number, heightMeasureSpec: number): void { + let widthMode = getMeasureSpecMode(widthMeasureSpec); + let widthSize = getMeasureSpecSize(widthMeasureSpec); + let childState = 0; + + this._flexLines.length = 0; + + (() => { + let childCount = this.getChildrenCount(); + let paddingStart = FlexboxLayout.getPaddingStart(this); + let paddingEnd = FlexboxLayout.getPaddingEnd(this); + let largestHeightInRow = Number.MIN_VALUE; + let flexLine = new FlexLine(); + + let indexInFlexLine = 0; + flexLine._mainSize = paddingStart + paddingEnd; + for (let i = 0; i < childCount; i++) { + let child = this._getReorderedChildAt(i); + if (child === null) { + this._addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } else if (FlexboxLayout.isGone(child)) { + flexLine._itemCount++; + this._addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } + + let lp: FlexboxLayout.LayoutParams = FlexboxLayout.getLayoutParams(child); + if (lp.alignSelf === AlignSelf.STRETCH) { + flexLine._indicesAlignSelfStretch.push(i); + } + + let childWidth = lp.width; + if (lp.flexBasisPercent !== FlexBasisPercent.DEFAULT && widthMode === EXACTLY) { + childWidth = Math.round(widthSize * lp.flexBasisPercent); + } + + let childWidthMeasureSpec = FlexboxLayout.getChildMeasureSpec(widthMeasureSpec, + this.paddingLeft + this.paddingRight + lp.leftMargin + + lp.rightMargin, childWidth < 0 ? WRAP_CONTENT : childWidth); + + let childHeightMeasureSpec = FlexboxLayout.getChildMeasureSpec(heightMeasureSpec, + this.paddingTop + this.paddingBottom + lp.topMargin + + lp.bottomMargin, lp.height < 0 ? WRAP_CONTENT : lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + this._checkSizeConstraints(child); + + childState = View.combineMeasuredStates(childState, child.getMeasuredState()); + largestHeightInRow = Math.max(largestHeightInRow, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + + if (this._isWrapRequired(widthMode, widthSize, flexLine._mainSize, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin, lp, + i, indexInFlexLine)) { + if (flexLine.itemCount > 0) { + this._addFlexLine(flexLine); + } + + flexLine = new FlexLine(); + flexLine._itemCount = 1; + flexLine._mainSize = paddingStart + paddingEnd; + largestHeightInRow = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + indexInFlexLine = 0; + } else { + flexLine._itemCount++; + indexInFlexLine++; + } + flexLine._mainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + flexLine._totalFlexGrow += lp.flexGrow; + flexLine._totalFlexShrink += lp.flexShrink; + + flexLine._crossSize = Math.max(flexLine._crossSize, largestHeightInRow); + + // Omit divider + + if (this.flexWrap !== FlexWrap.WRAP_REVERSE) { + flexLine._maxBaseline = Math.max(flexLine._maxBaseline, FlexboxLayout.getBaseline(child) + lp.topMargin) + } else { + flexLine._maxBaseline = Math.max(flexLine._maxBaseline, child.getMeasuredHeight() - FlexboxLayout.getBaseline(child) + lp.bottomMargin); + } + this._addFlexLineIfLastFlexItem(i, childCount, flexLine); + } + })(); + + this._determineMainSize(this.flexDirection, widthMeasureSpec, heightMeasureSpec); + + if (this.alignItems === AlignItems.BASELINE) { + let viewIndex = 0; + this._flexLines.forEach(flexLine => { + let largestHeightInLine = Number.MIN_VALUE; + for (let i = viewIndex; i < viewIndex + flexLine._itemCount; i++) { + let child = this._getReorderedChildAt(i); + let lp = FlexboxLayout.getLayoutParams(child); + if (this.flexWrap !== FlexWrap.WRAP_REVERSE) { + let marginTop = flexLine._maxBaseline - FlexboxLayout.getBaseline(child); + marginTop = Math.max(marginTop, lp.topMargin); + largestHeightInLine = Math.max(largestHeightInLine, child.height + marginTop + lp.bottomMargin); + } else { + let marginBottom = flexLine._maxBaseline - child.getMeasuredHeight() + FlexboxLayout.getBaseline(child); + marginBottom = Math.max(marginBottom, lp.bottomMargin); + largestHeightInLine = Math.max(largestHeightInLine, child.height + lp.topMargin + marginBottom); + } + } + flexLine._crossSize = largestHeightInLine; + viewIndex += flexLine.itemCount; + }); + } + + this._determineCrossSize(this.flexDirection, widthMeasureSpec, heightMeasureSpec, this.paddingTop + this.paddingBottom); + this._stretchViews(this.flexDirection, this.alignItems); + this._setMeasuredDimensionForFlex(this.flexDirection, widthMeasureSpec, heightMeasureSpec, childState); + } + + private _measureVertical(widthMeasureSpec, heightMeasureSpec): void { + let heightMode = getMeasureSpecMode(heightMeasureSpec); + let heightSize = getMeasureSpecSize(heightMeasureSpec); + let childState = 0; + + this._flexLines.length = 0; + + let childCount = this.getChildrenCount(); + let paddingTop = this.paddingTop; + let paddingBottom = this.paddingBottom; + let largestWidthInColumn = Number.MIN_VALUE; + let flexLine = new FlexLine(); + flexLine._mainSize = paddingTop + paddingBottom; + + let indexInFlexLine = 0; + for(let i = 0; i < childCount; i++) { + let child = this._getReorderedChildAt(i); + if (child === null) { + this._addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } else if (FlexboxLayout.isGone(child)) { + flexLine._itemCount++; + this._addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } + + let lp = FlexboxLayout.getLayoutParams(child); + if (lp.alignSelf === AlignSelf.STRETCH) { + flexLine._indicesAlignSelfStretch.push(i); + } + + let childHeight = lp.height; + if (lp.flexBasisPercent !== FlexBasisPercent.DEFAULT && heightMode === EXACTLY) { + childHeight = Math.round(heightSize * lp.flexBasisPercent); + } + + let childWidthMeasureSpec = FlexboxLayout.getChildMeasureSpec(widthMeasureSpec, + this.paddingLeft + this.paddingRight + lp.leftMargin + + lp.rightMargin, lp.width < 0 ? WRAP_CONTENT : lp.width); + let childHeightMeasureSpec = FlexboxLayout.getChildMeasureSpec(heightMeasureSpec, + this.paddingTop + this.paddingBottom + lp.topMargin + + lp.bottomMargin, childHeight < 0 ? WRAP_CONTENT : childHeight); + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + this._checkSizeConstraints(child); + + childState = View.combineMeasuredStates(childState, child.getMeasuredState()); + largestWidthInColumn = Math.max(largestWidthInColumn, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); + + if (this._isWrapRequired(heightMode, heightSize, flexLine.mainSize, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin, lp, + i, indexInFlexLine)) { + + if (flexLine._itemCount > 0) { + this._addFlexLine(flexLine); + } + + flexLine = new FlexLine(); + flexLine._itemCount = 1; + flexLine._mainSize = paddingTop + paddingBottom; + largestWidthInColumn = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + indexInFlexLine = 0; + } else { + flexLine._itemCount++; + indexInFlexLine++; + } + + flexLine._mainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + flexLine._totalFlexGrow += lp.flexGrow; + flexLine._totalFlexShrink += lp.flexShrink; + + flexLine._crossSize = Math.max(flexLine._crossSize, largestWidthInColumn); + + // Omit divider + this._addFlexLineIfLastFlexItem(i, childCount, flexLine); + } + + this._determineMainSize(this.flexDirection, widthMeasureSpec, heightMeasureSpec); + this._determineCrossSize(this.flexDirection, widthMeasureSpec, heightMeasureSpec, this.paddingLeft + this.paddingRight); + this._stretchViews(this.flexDirection, this.alignItems); + this._setMeasuredDimensionForFlex(this.flexDirection, widthMeasureSpec, heightMeasureSpec, childState); + } + + private _checkSizeConstraints(view: View) { + let needsMeasure = false; + let lp = FlexboxLayout.getLayoutParams(view); + let childWidth = view.getMeasuredWidth(); + let childHeight = view.getMeasuredHeight(); + + if (view.getMeasuredWidth() < lp.minWidth) { + needsMeasure = true; + childWidth = lp.minWidth; + } else if (view.getMeasuredWidth() > lp.maxWidth) { + needsMeasure = true; + childWidth = lp.maxWidth; + } + + if (childHeight < lp.minHeight) { + needsMeasure = true; + childHeight = lp.minHeight; + } else if (childHeight > lp.maxHeight) { + needsMeasure = true; + childHeight = lp.maxHeight; + } + if (needsMeasure) { + view.measure(makeMeasureSpec(childWidth, EXACTLY), makeMeasureSpec(childHeight, EXACTLY)); + } + } + + private _addFlexLineIfLastFlexItem(childIndex: number, childCount: number, flexLine: FlexLine) { + if (childIndex === childCount - 1 && flexLine.itemCount !== 0) { + this._addFlexLine(flexLine); + } + } + + private _addFlexLine(flexLine: FlexLine) { + // Omit divider + this._flexLines.push(flexLine); + } + + private _determineMainSize(flexDirection: FlexDirection, widthMeasureSpec: number, heightMeasureSpec: number) { + let mainSize: number; + let paddingAlongMainAxis: number; + + switch(flexDirection) { + case FlexDirection.ROW: + case FlexDirection.ROW_REVERSE: + let widthMode = getMeasureSpecMode(widthMeasureSpec); + let widthSize = getMeasureSpecSize(widthMeasureSpec); + if (widthMode === EXACTLY) { + mainSize = widthSize; + } else { + mainSize = this._getLargestMainSize(); + } + paddingAlongMainAxis = this.paddingLeft + this.paddingRight; + break; + case FlexDirection.COLUMN: + case FlexDirection.COLUMN_REVERSE: + let heightMode = getMeasureSpecMode(heightMeasureSpec); + let heightSize = getMeasureSpecSize(heightMeasureSpec); + if (heightMode === EXACTLY) { + mainSize = heightSize; + } else { + mainSize = this._getLargestMainSize(); + } + paddingAlongMainAxis = this.paddingTop + this.paddingBottom; + break; + default: + throw new Error("Invalid flex direction: " + flexDirection); + } + + let childIndex = 0; + this._flexLines.forEach(flexLine => { + if (flexLine.mainSize < mainSize) { + childIndex = this._expandFlexItems(flexLine, flexDirection, mainSize, paddingAlongMainAxis, childIndex); + } else { + childIndex = this._shrinkFlexItems(flexLine, flexDirection, mainSize, paddingAlongMainAxis, childIndex); + } + }); + } + + private _expandFlexItems(flexLine: FlexLine, flexDirection: FlexDirection, maxMainSize: number, paddingAlongMainAxis: number, startIndex: number) { + let childIndex = startIndex; + if (flexLine._totalFlexGrow <= 0 || maxMainSize < flexLine._mainSize) { + childIndex += flexLine._itemCount; + return childIndex; + } + let sizeBeforeExpand = flexLine._mainSize; + let needsReexpand = false; + let unitSpace = (maxMainSize - flexLine._mainSize) / flexLine._totalFlexGrow; + flexLine._mainSize = paddingAlongMainAxis + flexLine._dividerLengthInMainSize; + let accumulatedRoundError = 0; + for (let i = 0; i < flexLine.itemCount; i++) { + let child = this._getReorderedChildAt(childIndex); + if (child === null) { + continue; + } else if (FlexboxLayout.isGone(child)) { + childIndex++; + continue; + } + let lp = FlexboxLayout.getLayoutParams(child); + if (this._isMainAxisDirectionHorizontal(flexDirection)) { + if (!this._childrenFrozen[childIndex]) { + let rawCalculatedWidth = child.getMeasuredWidth() + unitSpace * lp.flexGrow; + if (i === flexLine.itemCount - 1) { + rawCalculatedWidth += accumulatedRoundError; + accumulatedRoundError = 0; + } + let newWidth = Math.round(rawCalculatedWidth); + if (newWidth > lp.maxWidth) { + needsReexpand = true; + newWidth = lp.maxWidth; + this._childrenFrozen[childIndex] = true; + flexLine._totalFlexGrow -= lp.flexGrow; + } else { + accumulatedRoundError += (rawCalculatedWidth - newWidth); + if (accumulatedRoundError > 1.0) { + newWidth += 1; + accumulatedRoundError -= 1.0; + } else if (accumulatedRoundError < -1.0) { + newWidth -= 1; + accumulatedRoundError += 1.0; + } + } + child.measure(makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(child.getMeasuredHeight(), EXACTLY)); + } + flexLine._mainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } else { + if (!this._childrenFrozen[childIndex]) { + let rawCalculatedHeight = child.getMeasuredHeight() + unitSpace * lp.flexGrow; + if (i === flexLine._itemCount - 1) { + rawCalculatedHeight += accumulatedRoundError; + accumulatedRoundError = 0; + } + let newHeight = Math.round(rawCalculatedHeight); + if (newHeight > lp.maxHeight) { + needsReexpand = true; + newHeight = lp.maxHeight; + this._childrenFrozen[childIndex] = true; + flexLine._totalFlexGrow -= lp.flexGrow; + } else { + accumulatedRoundError += (rawCalculatedHeight - newHeight); + if (accumulatedRoundError > 1.0) { + newHeight += 1; + accumulatedRoundError -= 1.0; + } else if (accumulatedRoundError < -1.0) { + newHeight -= 1; + accumulatedRoundError += 1.0; + } + } + child.measure(makeMeasureSpec(child.getMeasuredWidth(), EXACTLY), makeMeasureSpec(newHeight, EXACTLY)); + } + flexLine._mainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + childIndex++; + } + + if (needsReexpand && sizeBeforeExpand !== flexLine._mainSize) { + this._expandFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex); + } + return childIndex; + } + + private _shrinkFlexItems(flexLine: FlexLine, flexDirection: FlexDirection, maxMainSize: number, paddingAlongMainAxis: number, startIndex: number): number { + let childIndex = startIndex; + let sizeBeforeShrink = flexLine._mainSize; + if (flexLine._totalFlexShrink <= 0 || maxMainSize > flexLine._mainSize) { + childIndex += flexLine.itemCount; + return childIndex; + } + let needsReshrink = false; + let unitShrink = (flexLine._mainSize - maxMainSize) / flexLine._totalFlexShrink; + let accumulatedRoundError = 0; + flexLine._mainSize = paddingAlongMainAxis + flexLine._dividerLengthInMainSize; + for (let i = 0; i < flexLine.itemCount; i++) { + let child = this._getReorderedChildAt(childIndex); + if (child === null) { + continue; + } else if (FlexboxLayout.isGone(child)) { + childIndex++; + continue; + } + let lp = FlexboxLayout.getLayoutParams(child); + if (this._isMainAxisDirectionHorizontal(flexDirection)) { + // The direction of main axis is horizontal + if (!this._childrenFrozen[childIndex]) { + let rawCalculatedWidth = child.getMeasuredWidth() - unitShrink * lp.flexShrink; + if (i === flexLine.itemCount - 1) { + rawCalculatedWidth += accumulatedRoundError; + accumulatedRoundError = 0; + } + let newWidth = Math.round(rawCalculatedWidth); + if (newWidth < lp.minWidth) { + needsReshrink = true; + newWidth = lp.minWidth; + this._childrenFrozen[childIndex] = true; + flexLine._totalFlexShrink -= lp.flexShrink; + } else { + accumulatedRoundError += (rawCalculatedWidth - newWidth); + if (accumulatedRoundError > 1.0) { + newWidth += 1; + accumulatedRoundError -= 1; + } else if (accumulatedRoundError < -1.0) { + newWidth -= 1; + accumulatedRoundError += 1; + } + } + child.measure(makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(child.getMeasuredHeight(), EXACTLY)); + } + flexLine._mainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } else { + if (!this._childrenFrozen[childIndex]) { + let rawCalculatedHeight = child.getMeasuredHeight() - unitShrink * lp.flexShrink; + if (i === flexLine.itemCount - 1) { + rawCalculatedHeight += accumulatedRoundError; + accumulatedRoundError = 0; + } + let newHeight = Math.round(rawCalculatedHeight); + if (newHeight < lp.minHeight) { + needsReshrink = true; + newHeight = lp.minHeight; + this._childrenFrozen[childIndex] = true; + flexLine._totalFlexShrink -= lp.flexShrink; + } else { + accumulatedRoundError += (rawCalculatedHeight - newHeight); + if (accumulatedRoundError > 1.0) { + newHeight += 1; + accumulatedRoundError -= 1; + } else if (accumulatedRoundError < -1.0) { + newHeight -= 1; + accumulatedRoundError += 1; + } + } + child.measure(makeMeasureSpec(child.getMeasuredWidth(), EXACTLY), makeMeasureSpec(newHeight, EXACTLY)); + } + flexLine._mainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + childIndex++; + } + + if (needsReshrink && sizeBeforeShrink !== flexLine._mainSize) { + this._shrinkFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex); + } + return childIndex; + } + + private _determineCrossSize(flexDirection: FlexDirection, widthMeasureSpec: number, heightMeasureSpec: number, paddingAlongCrossAxis: number) { + let mode; + let size; + switch (flexDirection) { + case FlexDirection.ROW: + case FlexDirection.ROW_REVERSE: + mode = getMeasureSpecMode(heightMeasureSpec); + size = getMeasureSpecSize(heightMeasureSpec); + break; + case FlexDirection.COLUMN: + case FlexDirection.COLUMN_REVERSE: + mode = getMeasureSpecMode(widthMeasureSpec); + size = getMeasureSpecSize(widthMeasureSpec); + break; + default: + throw new Error("Invalid flex direction: " + flexDirection); + } + if (mode === EXACTLY) { + let totalCrossSize = this._getSumOfCrossSize() + paddingAlongCrossAxis; + if (this._flexLines.length === 1) { + this._flexLines[0]._crossSize = size - paddingAlongCrossAxis; + } else if (this._flexLines.length >= 2 && totalCrossSize < size) { + switch (this.alignContent) { + case AlignContent.STRETCH: + (() => { + let freeSpaceUnit = (size - totalCrossSize) / this._flexLines.length; + let accumulatedError = 0; + for (let i = 0, flexLinesSize = this._flexLines.length; i < flexLinesSize; i++) { + let flexLine = this._flexLines[i]; + let newCrossSizeAsFloat = flexLine._crossSize + freeSpaceUnit; + if (i === this._flexLines.length - 1) { + newCrossSizeAsFloat += accumulatedError; + accumulatedError = 0; + } + let newCrossSize = Math.round(newCrossSizeAsFloat); + accumulatedError += (newCrossSizeAsFloat - newCrossSize); + if (accumulatedError > 1) { + newCrossSize += 1; + accumulatedError -= 1; + } else if (accumulatedError < -1) { + newCrossSize -= 1; + accumulatedError += 1; + } + flexLine._crossSize = newCrossSize; + } + })(); + break; + case AlignContent.SPACE_AROUND: + (() => { + let spaceTopAndBottom = size - totalCrossSize; + let numberOfSpaces = this._flexLines.length * 2; + spaceTopAndBottom = spaceTopAndBottom / numberOfSpaces; + let newFlexLines: FlexLine[] = []; + let dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine._crossSize = spaceTopAndBottom; + this._flexLines.forEach(flexLine => { + newFlexLines.push(dummySpaceFlexLine); + newFlexLines.push(flexLine); + newFlexLines.push(dummySpaceFlexLine); + }); + this._flexLines = newFlexLines; + })(); + break; + case AlignContent.SPACE_BETWEEN: + (() => { + let spaceBetweenFlexLine = size - totalCrossSize; + let numberOfSpaces = this._flexLines.length - 1; + spaceBetweenFlexLine = spaceBetweenFlexLine / numberOfSpaces; + let accumulatedError = 0; + let newFlexLines: FlexLine[] = []; + for (let i = 0, flexLineSize = this._flexLines.length; i < flexLineSize; i++) { + let flexLine = this._flexLines[i]; + newFlexLines.push(flexLine); + + if (i !== this._flexLines.length - 1) { + let dummySpaceFlexLine = new FlexLine(); + if (i === this._flexLines.length - 2) { + dummySpaceFlexLine._crossSize = Math.round(spaceBetweenFlexLine + accumulatedError); + accumulatedError = 0; + } else { + dummySpaceFlexLine._crossSize = Math.round(spaceBetweenFlexLine); + } + accumulatedError += (spaceBetweenFlexLine - dummySpaceFlexLine._crossSize); + if (accumulatedError > 1) { + dummySpaceFlexLine._crossSize += 1; + accumulatedError -= 1; + } else if (accumulatedError < -1) { + dummySpaceFlexLine._crossSize -= 1; + accumulatedError += 1; + } + newFlexLines.push(dummySpaceFlexLine); + } + } + this._flexLines = newFlexLines; + })(); + break; + case AlignContent.CENTER: { + let spaceAboveAndBottom = size - totalCrossSize; + spaceAboveAndBottom = spaceAboveAndBottom / 2; + let newFlexLines: FlexLine[] = []; + let dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine._crossSize = spaceAboveAndBottom; + for (let i = 0, flexLineSize = this._flexLines.length; i < flexLineSize; i++) { + if (i === 0) { + newFlexLines.push(dummySpaceFlexLine); + } + let flexLine = this._flexLines[i]; + newFlexLines.push(flexLine); + if (i === this._flexLines.length - 1) { + newFlexLines.push(dummySpaceFlexLine); + } + } + this._flexLines = newFlexLines; + break; + } + case AlignContent.FLEX_END: { + let spaceTop = size - totalCrossSize; + let dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine._crossSize = spaceTop; + this._flexLines.unshift(dummySpaceFlexLine); + break; + } + } + } + } + } + + private _stretchViews(flexDirection: FlexDirection, alignItems: AlignItems) { + if (alignItems === AlignItems.STRETCH) { + let viewIndex = 0; + this._flexLines.forEach(flexLine => { + for (let i = 0; i < flexLine.itemCount; i++, viewIndex++) { + let view = this._getReorderedChildAt(viewIndex); + let lp = FlexboxLayout.getLayoutParams(view); + if (lp.alignSelf !== AlignSelf.AUTO && lp.alignSelf !== AlignSelf.STRETCH) { + continue; + } + switch (flexDirection) { + case FlexDirection.ROW: + case FlexDirection.ROW_REVERSE: + this._stretchViewVertically(view, flexLine._crossSize); + break; + case FlexDirection.COLUMN: + case FlexDirection.COLUMN_REVERSE: + this._stretchViewHorizontally(view, flexLine._crossSize); + break; + default: + throw new Error("Invalid flex direction: " + flexDirection); + } + } + }); + } else { + this._flexLines.forEach(flexLine => { + flexLine._indicesAlignSelfStretch.forEach(index => { + let view = this._getReorderedChildAt(index); + switch (flexDirection) { + case FlexDirection.ROW: + case FlexDirection.ROW_REVERSE: + this._stretchViewVertically(view, flexLine._crossSize); + break; + case FlexDirection.COLUMN: + case FlexDirection.COLUMN_REVERSE: + this._stretchViewHorizontally(view, flexLine._crossSize); + break; + default: + throw new Error("Invalid flex direction: " + flexDirection); + } + }); + }); + } + } + + private _stretchViewVertically(view: View, crossSize: number) { + let lp = FlexboxLayout.getLayoutParams(view); + let newHeight = crossSize - lp.topMargin - lp.bottomMargin; + newHeight = Math.max(newHeight, 0); + view.measure(makeMeasureSpec(view.getMeasuredWidth(), EXACTLY), makeMeasureSpec(newHeight, EXACTLY)); + } + + private _stretchViewHorizontally(view: View, crossSize: number) { + let lp = FlexboxLayout.getLayoutParams(view); + let newWidth = crossSize - lp.leftMargin - lp.rightMargin; + newWidth = Math.max(newWidth, 0); + view.measure(makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(view.getMeasuredHeight(), EXACTLY)); + } + + private _setMeasuredDimensionForFlex(flexDirection: FlexDirection, widthMeasureSpec: number, heightMeasureSpec: number, childState: number) { + let widthMode = getMeasureSpecMode(widthMeasureSpec); + let widthSize = getMeasureSpecSize(widthMeasureSpec); + let heightMode = getMeasureSpecMode(heightMeasureSpec); + let heightSize = getMeasureSpecSize(heightMeasureSpec); + let calculatedMaxHeight; + let calculatedMaxWidth; + switch (flexDirection) { + case FlexDirection.ROW: + case FlexDirection.ROW_REVERSE: + calculatedMaxHeight = this._getSumOfCrossSize() + this.paddingTop + this.paddingBottom; + calculatedMaxWidth = this._getLargestMainSize(); + break; + case FlexDirection.COLUMN: + case FlexDirection.COLUMN_REVERSE: + calculatedMaxHeight = this._getLargestMainSize(); + calculatedMaxWidth = this._getSumOfCrossSize() + this.paddingLeft + this.paddingRight; + break; + default: + throw new Error("Invalid flex direction: " + flexDirection); + } + + let widthSizeAndState; + switch (widthMode) { + case EXACTLY: + if (widthSize < calculatedMaxWidth) { + childState = View.combineMeasuredStates(childState, MEASURED_STATE_TOO_SMALL); + } + widthSizeAndState = View.resolveSizeAndState(widthSize, widthSize, widthMode, childState); + break; + case AT_MOST: { + if (widthSize < calculatedMaxWidth) { + childState = View.combineMeasuredStates(childState, MEASURED_STATE_TOO_SMALL); + } else { + widthSize = calculatedMaxWidth; + } + widthSizeAndState = View.resolveSizeAndState(widthSize, widthSize, widthMode, childState); + break; + } + case UNSPECIFIED: { + widthSizeAndState = View.resolveSizeAndState(calculatedMaxWidth, widthSize, widthMode, childState); + break; + } + default: + throw new Error("Unknown width mode is set: " + widthMode); + } + let heightSizeAndState; + switch (heightMode) { + case EXACTLY: + if (heightSize < calculatedMaxHeight) { + childState = View.combineMeasuredStates(childState, MEASURED_STATE_TOO_SMALL >> utils.layout.MEASURED_HEIGHT_STATE_SHIFT); + } + heightSizeAndState = View.resolveSizeAndState(heightSize, heightSize, heightMode, childState); + break; + case AT_MOST: { + if (heightSize < calculatedMaxHeight) { + childState = View.combineMeasuredStates(childState, MEASURED_STATE_TOO_SMALL >> utils.layout.MEASURED_HEIGHT_STATE_SHIFT); + } else { + heightSize = calculatedMaxHeight; + } + + heightSizeAndState = View.resolveSizeAndState(heightSize, heightSize, heightMode, childState); + break; + } + case UNSPECIFIED: { + heightSizeAndState = View.resolveSizeAndState(calculatedMaxHeight, heightSize, heightMode, childState); + break; + } + default: + throw new Error("Unknown height mode is set: " + heightMode); + } + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + private _isWrapRequired(mode: number, maxSize: number, currentLength: number, childLength: number, lp: FlexboxLayout.LayoutParams, childAbsoluteIndex: number, childRelativeIndexInFlexLine: number): boolean { + if (this.flexWrap === FlexWrap.NOWRAP) { + return false; + } + if (lp.wrapBefore) { + return true; + } + if (mode === UNSPECIFIED) { + return false; + } + + // Omit divider + + return maxSize < currentLength + childLength; + } + + private _getLargestMainSize(): number { + return this._flexLines.reduce((max, flexLine) => Math.max(max, flexLine.mainSize), Number.MIN_VALUE); + } + + private _getSumOfCrossSize(): number { + // Omit divider + return this._flexLines.reduce((sum, flexLine) => sum + flexLine._crossSize, 0); + } + + private _isMainAxisDirectionHorizontal(flexDirection: FlexDirection): boolean { + return flexDirection === FlexDirection.ROW || flexDirection === FlexDirection.ROW_REVERSE; + } + + public onLayout(left: number, top: number, right: number, bottom: number) { + let isRtl; + switch (this.flexDirection) { + case FlexDirection.ROW: + isRtl = false; + this._layoutHorizontal(isRtl, left, top, right, bottom); + break; + case FlexDirection.ROW_REVERSE: + isRtl = true; + this._layoutHorizontal(isRtl, left, top, right, bottom); + break; + case FlexDirection.COLUMN: + isRtl = false; + if (this.flexWrap === FlexWrap.WRAP_REVERSE) { + isRtl = !isRtl; + } + this._layoutVertical(isRtl, false, left, top, right, bottom); + break; + case FlexDirection.COLUMN_REVERSE: + isRtl = false; + if (this.flexWrap === FlexWrap.WRAP_REVERSE) { + isRtl = !isRtl; + } + this._layoutVertical(isRtl, true, left, top, right, bottom); + break; + default: + throw new Error("Invalid flex direction is set: " + this.flexDirection); + } + + LayoutBase.restoreOriginalParams(this); + } + + private _layoutHorizontal(isRtl: boolean, left: number, top: number, right: number, bottom: number) { + let paddingLeft = this.paddingLeft; + let paddingRight = this.paddingRight; + + let childLeft; + let currentViewIndex = 0; + + let height = bottom - top; + let width = right - left; + + let childBottom = height - this.paddingBottom; + let childTop = this.paddingTop; + + let childRight; + this._flexLines.forEach((flexLine, i) => { + + // Omit divider + + let spaceBetweenItem = 0.0; + switch (this.justifyContent) { + case JustifyContent.FLEX_START: + childLeft = paddingLeft; + childRight = width - paddingRight; + break; + case JustifyContent.FLEX_END: + childLeft = width - flexLine._mainSize + paddingRight; + childRight = flexLine._mainSize - paddingLeft; + break; + case JustifyContent.CENTER: + childLeft = paddingLeft + (width - flexLine._mainSize) / 2.0; + childRight = width - paddingRight - (width - flexLine._mainSize) / 2.0; + break; + case JustifyContent.SPACE_AROUND: + if (flexLine._itemCount !== 0) { + spaceBetweenItem = (width - flexLine.mainSize) / flexLine._itemCount; + } + childLeft = paddingLeft + spaceBetweenItem / 2.0; + childRight = width - paddingRight - spaceBetweenItem / 2.0; + break; + case JustifyContent.SPACE_BETWEEN: + childLeft = paddingLeft; + let denominator = flexLine.itemCount !== 1 ? flexLine.itemCount - 1 : 1.0; + spaceBetweenItem = (width - flexLine.mainSize) / denominator; + childRight = width - paddingRight; + break; + default: + throw new Error("Invalid justifyContent is set: " + this.justifyContent); + } + spaceBetweenItem = Math.max(spaceBetweenItem, 0); + + for (let j = 0; j < flexLine.itemCount; j++) { + let child = this._getReorderedChildAt(currentViewIndex); + if (child === null) { + continue; + } else if (FlexboxLayout.isGone(child)) { + currentViewIndex++; + continue; + } + let lp = FlexboxLayout.getLayoutParams(child); + childLeft += lp.leftMargin; + childRight -= lp.rightMargin; + + // Omit divider + + if (this.flexWrap === FlexWrap.WRAP_REVERSE) { + if (isRtl) { + this._layoutSingleChildHorizontal(child, flexLine, this.flexWrap, this.alignItems, + Math.round(childRight) - child.getMeasuredWidth(), + childBottom - child.getMeasuredHeight(), Math.round(childRight), + childBottom); + } else { + this._layoutSingleChildHorizontal(child, flexLine, this.flexWrap, this.alignItems, + Math.round(childLeft), childBottom - child.getMeasuredHeight(), + Math.round(childLeft) + child.getMeasuredWidth(), + childBottom); + } + } else { + if (isRtl) { + this._layoutSingleChildHorizontal(child, flexLine, this.flexWrap, this.alignItems, + Math.round(childRight) - child.getMeasuredWidth(), childTop, + Math.round(childRight), childTop + child.getMeasuredHeight()); + } else { + this._layoutSingleChildHorizontal(child, flexLine, this.flexWrap, this.alignItems, + Math.round(childLeft), childTop, + Math.round(childLeft) + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + childLeft += child.getMeasuredWidth() + spaceBetweenItem + lp.rightMargin; + childRight -= child.getMeasuredWidth() + spaceBetweenItem + lp.leftMargin; + currentViewIndex++; + + let bounds = child._getCurrentLayoutBounds(); + flexLine._left = Math.min(flexLine._left, bounds.left - lp.leftMargin); + flexLine._top = Math.min(flexLine._top, bounds.top - lp.topMargin); + flexLine._right = Math.max(flexLine._right, bounds.right + lp.rightMargin); + flexLine._bottom = Math.max(flexLine._bottom, bounds.bottom + lp.bottomMargin); + } + + childTop += flexLine._crossSize; + childBottom -= flexLine._crossSize; + }); + } + + private _layoutSingleChildHorizontal(view: View, flexLine: FlexLine, flexWrap: FlexWrap, alignItems: AlignItems, left: number, top: number, right: number, bottom: number): void { + let lp = FlexboxLayout.getLayoutParams(view); + + if (lp.alignSelf !== AlignSelf.AUTO) { + alignItems = lp.alignSelf; + } + + let crossSize = flexLine._crossSize; + switch (alignItems) { + case AlignItems.FLEX_START: + case AlignItems.STRETCH: + if (flexWrap !== FlexWrap.WRAP_REVERSE) { + view.layout(left, top + lp.topMargin, right, bottom + lp.topMargin); + } else { + view.layout(left, top - lp.bottomMargin, right, bottom - lp.bottomMargin); + } + break; + case AlignItems.BASELINE: + if (flexWrap !== FlexWrap.WRAP_REVERSE) { + let marginTop = flexLine._maxBaseline - FlexboxLayout.getBaseline(view); + marginTop = Math.max(marginTop, lp.topMargin); + view.layout(left, top + marginTop, right, bottom + marginTop); + } else { + let marginBottom = flexLine._maxBaseline - view.getMeasuredHeight() + FlexboxLayout.getBaseline(view); + marginBottom = Math.max(marginBottom, lp.bottomMargin); + view.layout(left, top - marginBottom, right, bottom - marginBottom); + } + break; + case AlignItems.FLEX_END: + if (flexWrap !== FlexWrap.WRAP_REVERSE) { + view.layout(left, + top + crossSize - view.getMeasuredHeight() - lp.bottomMargin, + right, top + crossSize - lp.bottomMargin); + } else { + view.layout(left, top - crossSize + view.getMeasuredHeight() + lp.topMargin, + right, bottom - crossSize + view.getMeasuredHeight() + lp.topMargin); + } + break; + case AlignItems.CENTER: + let topFromCrossAxis = (crossSize - view.getMeasuredHeight()) / 2; + if (flexWrap !== FlexWrap.WRAP_REVERSE) { + view.layout(left, top + topFromCrossAxis + lp.topMargin - lp.bottomMargin, + right, top + topFromCrossAxis + view.getMeasuredHeight() + lp.topMargin + - lp.bottomMargin); + } else { + view.layout(left, top - topFromCrossAxis + lp.topMargin - lp.bottomMargin, + right, top - topFromCrossAxis + view.getMeasuredHeight() + lp.topMargin + - lp.bottomMargin); + } + break; + } + } + + private _layoutVertical(isRtl: boolean, fromBottomToTop: boolean, left: number, top: number, right: number, bottom: number) { + let paddingTop = this.paddingTop; + let paddingBottom = this.paddingBottom; + + let paddingRight = this.paddingRight; + let childLeft = this.paddingLeft; + let currentViewIndex = 0; + + let width = right - left; + let height = bottom - top; + let childRight = width - paddingRight; + + let childTop; + let childBottom; + + this._flexLines.forEach(flexLine => { + + // Omit divider. + + let spaceBetweenItem = 0.0; + + switch (this.justifyContent) { + case JustifyContent.FLEX_START: + childTop = paddingTop; + childBottom = height - paddingBottom; + break; + case JustifyContent.FLEX_END: + childTop = height - flexLine._mainSize + paddingBottom; + childBottom = flexLine._mainSize - paddingTop; + break; + case JustifyContent.CENTER: + childTop = paddingTop + (height - flexLine._mainSize) / 2.0; + childBottom = height - paddingBottom - (height - flexLine._mainSize) / 2.0; + break; + case JustifyContent.SPACE_AROUND: + if (flexLine._itemCount !== 0) { + spaceBetweenItem = (height - flexLine._mainSize) / flexLine.itemCount; + } + childTop = paddingTop + spaceBetweenItem / 2.0; + childBottom = height - paddingBottom - spaceBetweenItem / 2.0; + break; + case JustifyContent.SPACE_BETWEEN: + childTop = paddingTop; + let denominator = flexLine.itemCount !== 1 ? flexLine.itemCount - 1 : 1.0; + spaceBetweenItem = (height - flexLine.mainSize) / denominator; + childBottom = height - paddingBottom; + break; + default: + throw new Error("Invalid justifyContent is set: " + this.justifyContent); + } + spaceBetweenItem = Math.max(spaceBetweenItem, 0); + + for (let j = 0; j < flexLine.itemCount; j++) { + let child = this._getReorderedChildAt(currentViewIndex); + if (child === null) { + continue; + } else if (FlexboxLayout.isGone(child)) { + currentViewIndex++; + continue; + } + let lp = FlexboxLayout.getLayoutParams(child); + childTop += lp.topMargin; + childBottom -= lp.bottomMargin; + + // Omit divider. + + if (isRtl) { + if (fromBottomToTop) { + this._layoutSingleChildVertical(child, flexLine, true, this.alignItems, + childRight - child.getMeasuredWidth(), + Math.round(childBottom) - child.getMeasuredHeight(), childRight, + Math.round(childBottom)); + } else { + this._layoutSingleChildVertical(child, flexLine, true, this.alignItems, + childRight - child.getMeasuredWidth(), Math.round(childTop), + childRight, Math.round(childTop) + child.getMeasuredHeight()); + } + } else { + if (fromBottomToTop) { + this._layoutSingleChildVertical(child, flexLine, false, this.alignItems, + childLeft, Math.round(childBottom) - child.getMeasuredHeight(), + childLeft + child.getMeasuredWidth(), Math.round(childBottom)); + } else { + this._layoutSingleChildVertical(child, flexLine, false, this.alignItems, + childLeft, Math.round(childTop), + childLeft + child.getMeasuredWidth(), + Math.round(childTop) + child.getMeasuredHeight()); + } + } + childTop += child.getMeasuredHeight() + spaceBetweenItem + lp.bottomMargin; + childBottom -= child.getMeasuredHeight() + spaceBetweenItem + lp.topMargin; + currentViewIndex++; + + let bounds = child._getCurrentLayoutBounds(); + flexLine._left = Math.min(flexLine._left, bounds.left - lp.leftMargin); + flexLine._top = Math.min(flexLine._top, bounds.top - lp.topMargin); + flexLine._right = Math.max(flexLine._right, bounds.right + lp.rightMargin); + flexLine._bottom = Math.max(flexLine._bottom, bounds.bottom + lp.bottomMargin); + } + + childLeft += flexLine.crossSize; + childRight -= flexLine.crossSize; + }); + } + + private _layoutSingleChildVertical(view: View, flexLine: FlexLine, isRtl: boolean, alignItems: AlignItems, left: number, top: number, right: number, bottom: number) { + let lp = FlexboxLayout.getLayoutParams(view); + if (lp.alignSelf !== AlignSelf.AUTO) { + alignItems = lp.alignSelf; + } + let crossSize = flexLine.crossSize; + switch (alignItems) { + case AlignItems.FLEX_START: + case AlignItems.STRETCH: + case AlignItems.BASELINE: + if (!isRtl) { + view.layout(left + lp.leftMargin, top, right + lp.leftMargin, bottom); + } else { + view.layout(left - lp.rightMargin, top, right - lp.rightMargin, bottom); + } + break; + case AlignItems.FLEX_END: + if (!isRtl) { + view.layout(left + crossSize - view.getMeasuredWidth() - lp.rightMargin, + top, right + crossSize - view.getMeasuredWidth() - lp.rightMargin, + bottom); + } else { + // If the flexWrap === FLEX_WRAP_WRAP_REVERSE, the direction of the + // flexEnd is flipped (from left to right). + view.layout(left - crossSize + view.getMeasuredWidth() + lp.leftMargin, top, + right - crossSize + view.getMeasuredWidth() + lp.leftMargin, + bottom); + } + break; + case AlignItems.CENTER: + let leftFromCrossAxis = (crossSize - view.getMeasuredWidth()) / 2; + if (!isRtl) { + view.layout(left + leftFromCrossAxis + lp.leftMargin - lp.rightMargin, + top, right + leftFromCrossAxis + lp.leftMargin - lp.rightMargin, + bottom); + } else { + view.layout(left - leftFromCrossAxis + lp.leftMargin - lp.rightMargin, + top, right - leftFromCrossAxis + lp.leftMargin - lp.rightMargin, + bottom); + } + break; + } + } + + // Omit divider in onDraw(), drawDividersHorizontal, drawDividersVertical, drawVerticalDivider + + // requestLayout on set flexDirection, set flexWrap, set justifyContent, set alignItems, set alignContent + + // NOTE Consider moving to View if frequently used + private static getChildMeasureSpec(spec: number, padding: number, childDimension: number): number { + let specMode = utils.layout.getMeasureSpecMode(spec); + let specSize = utils.layout.getMeasureSpecSize(spec); + + let size = Math.max(0, specSize - padding); + + let resultSize = 0; + let resultMode = 0; + + switch (specMode) { + // Parent has imposed an exact size on us + case EXACTLY: + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = EXACTLY; + } else if (childDimension === MATCH_PARENT) { + resultSize = size; + resultMode = EXACTLY; + } else if (childDimension === WRAP_CONTENT) { + resultSize = size; + resultMode = AT_MOST; + } + break; + + case AT_MOST: + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = EXACTLY; + } else if (childDimension === MATCH_PARENT) { + resultSize = size; + resultMode = AT_MOST; + } else if (childDimension === WRAP_CONTENT) { + resultSize = size; + resultMode = AT_MOST; + } + break; + + case UNSPECIFIED: + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = EXACTLY; + } else if (childDimension === MATCH_PARENT) { + resultSize = View_sUseZeroUnspecifiedMeasureSpec ? 0 : size; + resultMode = UNSPECIFIED; + } else if (childDimension === WRAP_CONTENT) { + resultSize = View_sUseZeroUnspecifiedMeasureSpec ? 0 : size; + resultMode = UNSPECIFIED; + } + break; + } + return utils.layout.makeMeasureSpec(resultSize, resultMode); + } +} + +export namespace FlexboxLayout { + export interface LayoutParams extends CommonLayoutParams { + order: number; + alignSelf: AlignSelf; + flexGrow: number; + flexShrink: number; + flexBasisPercent: number; + wrapBefore: boolean; + minWidth: number; + minHeight: number; + maxWidth: number; + maxHeight: number; + } + + export function getLayoutParams(child: View): FlexboxLayout.LayoutParams { + let lp: CommonLayoutParams = child.style._getValue(nativeLayoutParamsProperty); + let flp: FlexboxLayout.LayoutParams = { + width: lp.width, + height: lp.height, + + widthPercent: lp.widthPercent, + heightPercent: lp.heightPercent, + + leftMargin: lp.leftMargin, + topMargin: lp.topMargin, + rightMargin: lp.rightMargin, + bottomMargin: lp.bottomMargin, + + leftMarginPercent: lp.leftMarginPercent, + topMarginPercent: lp.topMarginPercent, + rightMarginPercent: lp.rightMarginPercent, + bottomMarginPercent: lp.bottomMarginPercent, + + horizontalAlignment: lp.horizontalAlignment, + verticalAlignment: lp.verticalAlignment, + + alignSelf: FlexboxLayout.getAlignSelf(child), + flexBasisPercent: FlexBasisPercent.DEFAULT, + flexGrow: FlexboxLayout.getFlexGrow(child), + flexShrink: FlexboxLayout.getFlexShrink(child), + maxHeight: MAX_SIZE, + maxWidth: MAX_SIZE, + minHeight: child.minHeight, + minWidth: child.minWidth, + order: FlexboxLayout.getOrder(child), + wrapBefore: FlexboxLayout.getFlexWrapBefore(child) + }; + + return flp; + } + + export function getBaseline(child: View): number { + // TODO: Check if we support baseline for iOS. + return 0; + } + + export function isGone(child: View): boolean { + return child.visibility !== enums.Visibility.visible; + } + + export function getPaddingStart(child: View): number { + return child.style.paddingLeft; + } + + export function getPaddingEnd(child: View): number { + return child.style.paddingRight; + } +} diff --git a/tns-core-modules/ui/layouts/layout.ios.ts b/tns-core-modules/ui/layouts/layout.ios.ts index f092d6c70..00019d103 100644 --- a/tns-core-modules/ui/layouts/layout.ios.ts +++ b/tns-core-modules/ui/layouts/layout.ios.ts @@ -21,4 +21,4 @@ export class Layout extends layoutBase.LayoutBase implements definition.Layout { public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void { // Don't call super because it will measure the native element. } -} \ No newline at end of file +} diff --git a/tns-core-modules/utils/utils-common.ts b/tns-core-modules/utils/utils-common.ts index b730bccd8..9f0721424 100644 --- a/tns-core-modules/utils/utils-common.ts +++ b/tns-core-modules/utils/utils-common.ts @@ -65,6 +65,7 @@ export module layout { export var EXACTLY = 1 << MODE_SHIFT; export var AT_MOST = 2 << MODE_SHIFT; + export var MEASURED_HEIGHT_STATE_SHIFT = 0x00000010; /* 16 */ export var MEASURED_STATE_TOO_SMALL = 0x01000000; export var MEASURED_STATE_MASK = 0xff000000; export var MEASURED_SIZE_MASK = 0x00ffffff; diff --git a/tns-core-modules/utils/utils.d.ts b/tns-core-modules/utils/utils.d.ts index 44b0d8e2c..056ae5ebb 100644 --- a/tns-core-modules/utils/utils.d.ts +++ b/tns-core-modules/utils/utils.d.ts @@ -28,6 +28,7 @@ /** * Bits that provide the actual measured size. */ + export var MEASURED_HEIGHT_STATE_SHIFT: number; export var MEASURED_SIZE_MASK: number; export var MEASURED_STATE_MASK: number; export var MEASURED_STATE_TOO_SMALL: number; diff --git a/tns-platform-declarations/android/org.nativescript.widgets.d.ts b/tns-platform-declarations/android/org.nativescript.widgets.d.ts index 85367e5de..8429f8609 100644 --- a/tns-platform-declarations/android/org.nativescript.widgets.d.ts +++ b/tns-platform-declarations/android/org.nativescript.widgets.d.ts @@ -305,6 +305,9 @@ public flexGrow: number; public flexShrink: number; public alignSelf: number; + public wrapBefore: boolean; + public minWidth: number; + public minHeight: number; } } diff --git a/tsconfig.json b/tsconfig.json index 8a7dda42f..675ff87ba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,8 @@ "experimentalDecorators": true, "diagnostics": true, "sourceMap": true, + "jsx": "react", + "reactNamespace": "UIBuilder", "lib": [ "es2016" ]