UNPKG

@nativescript/core

Version:

A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.

978 lines (977 loc) • 63 kB
import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, FlexboxLayoutBase, FlexBasisPercent, orderProperty, flexGrowProperty, flexShrinkProperty, flexWrapBeforeProperty, alignSelfProperty } from './flexbox-layout-common'; import { View } from '../../core/view'; import { layout } from '../../../utils'; export * from './flexbox-layout-common'; var EXACTLY = layout.EXACTLY; var AT_MOST = layout.AT_MOST; var UNSPECIFIED = layout.UNSPECIFIED; var MEASURED_SIZE_MASK = layout.MEASURED_SIZE_MASK; var MEASURED_STATE_TOO_SMALL = layout.MEASURED_STATE_TOO_SMALL; function requestFlexboxLayout(value) { const flexbox = this.parent; if (flexbox instanceof FlexboxLayoutBase) { flexbox.requestLayout(); } } View.prototype[orderProperty.setNative] = requestFlexboxLayout; View.prototype[flexGrowProperty.setNative] = requestFlexboxLayout; View.prototype[flexShrinkProperty.setNative] = requestFlexboxLayout; View.prototype[flexWrapBeforeProperty.setNative] = requestFlexboxLayout; View.prototype[alignSelfProperty.setNative] = requestFlexboxLayout; 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; var makeMeasureSpec = layout.makeMeasureSpec; var getMeasureSpecMode = layout.getMeasureSpecMode; var getMeasureSpecSize = layout.getMeasureSpecSize; // `eachLayoutChild` iterates over children, and we need more - indexed access. // This class tries to accomodate that by collecting all children in an // array no more than once per measure. class MeasureContext { constructor(owner) { this.owner = owner; this.children = []; this.owner.eachLayoutChild((child) => { this.children.push(child); }); } get childrenCount() { return this.children.length; } childAt(index) { return this.children[index]; } } class FlexLine { constructor() { this._left = Number.MAX_VALUE; this._top = Number.MAX_VALUE; this._right = Number.MAX_VALUE; this._bottom = Number.MAX_VALUE; this._mainSize = 0; this._dividerLengthInMainSize = 0; this._crossSize = 0; this._itemCount = 0; this._totalFlexGrow = 0; this._totalFlexShrink = 0; this._maxBaseline = 0; this._indicesAlignSelfStretch = []; } get left() { return this._left; } get top() { return this._top; } get right() { return this._right; } get bottom() { return this._bottom; } get mainSize() { return this._mainSize; } get crossSize() { return this._crossSize; } get itemCount() { return this._itemCount; } get totalFlexGrow() { return this._totalFlexGrow; } get totalFlexShrink() { return this._totalFlexShrink; } } class Order { compareTo(another) { if (this.order !== another.order) { return this.order - another.order; } return this.index - another.index; } } export class FlexboxLayout extends FlexboxLayoutBase { constructor() { // Omit divider super(...arguments); this._flexLines = []; } onMeasure(widthMeasureSpec, heightMeasureSpec) { this.measureContext = new MeasureContext(this); // Omit: super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (this._isOrderChangedFromLastMeasurement) { this._reorderedIndices = this._createReorderedIndices(); } if (!this._childrenFrozen || this._childrenFrozen.length < this.measureContext.childrenCount) { this._childrenFrozen = new Array(this.measureContext.childrenCount); } 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); } this._childrenFrozen.length = 0; } _getReorderedChildAt(index) { let child; if (index < 0 || index >= this._reorderedIndices.length) { child = null; } else { const reorderedIndex = this._reorderedIndices[index]; child = this.measureContext.childAt(reorderedIndex); } return child; } _createReorderedIndices() { const childCount = this.measureContext.childrenCount; const orders = this._createOrders(childCount); return this._sortOrdersIntoReorderedIndices(childCount, orders); } _sortOrdersIntoReorderedIndices(childCount, orders) { orders.sort((a, b) => a.compareTo(b)); if (!this._orderCache) { this._orderCache = []; } this._orderCache.length = 0; const reorderedIndices = []; orders.forEach((order, i) => { reorderedIndices[i] = order.index; this._orderCache[i] = order.order; }); return reorderedIndices; } _createOrders(childCount) { const orders = []; for (let i = 0; i < childCount; i++) { const child = this.measureContext.childAt(i); const order = new Order(); order.order = FlexboxLayout.getOrder(child); order.index = i; orders.push(order); } return orders; } get _isOrderChangedFromLastMeasurement() { const childCount = this.measureContext.childrenCount; if (!this._orderCache) { this._orderCache = []; } if (this._orderCache.length !== childCount) { return true; } for (let i = 0; i < childCount; i++) { const view = this.measureContext.childAt(i); if (view === null) { continue; } if (FlexboxLayout.getOrder(view) !== this._orderCache[i]) { return true; } } return false; } _measureHorizontal(widthMeasureSpec, heightMeasureSpec) { const widthSize = getMeasureSpecSize(widthMeasureSpec); const widthMode = getMeasureSpecMode(widthMeasureSpec); const heightSize = getMeasureSpecSize(heightMeasureSpec); const heightMode = getMeasureSpecMode(heightMeasureSpec); let childState = 0; this._flexLines.length = 0; (() => { const childCount = this.measureContext.childrenCount; const paddingStart = FlexboxLayout.getPaddingStart(this); const 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++) { const child = this._getReorderedChildAt(i); if (child === null) { this._addFlexLineIfLastFlexItem(i, childCount, flexLine); continue; } else if (child.isCollapsed) { flexLine._itemCount++; this._addFlexLineIfLastFlexItem(i, childCount, flexLine); continue; } child._updateEffectiveLayoutValues(widthSize, widthMode, heightSize, heightMode); const lp = child; // child.style; if (FlexboxLayout.getAlignSelf(child) === 'stretch') { flexLine._indicesAlignSelfStretch.push(i); } let childWidth = lp.effectiveWidth; if (FlexBasisPercent.DEFAULT /*lp.flexBasisPercent*/ !== FlexBasisPercent.DEFAULT && widthMode === EXACTLY) { childWidth = Math.round(widthSize * FlexBasisPercent.DEFAULT /*lp.flexBasisPercent*/); } const childWidthMeasureSpec = FlexboxLayout.getChildMeasureSpec(widthMeasureSpec, lp.effectivePaddingLeft + lp.effectivePaddingRight + lp.effectiveMarginLeft + lp.effectiveMarginRight, childWidth < 0 ? WRAP_CONTENT : childWidth); const childHeightMeasureSpec = FlexboxLayout.getChildMeasureSpec(heightMeasureSpec, lp.effectivePaddingTop + lp.effectivePaddingBottom + lp.effectiveMarginTop + lp.effectiveMarginBottom, lp.effectiveHeight < 0 ? WRAP_CONTENT : lp.effectiveHeight); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); this._checkSizeConstraints(child); childState = View.combineMeasuredStates(childState, child.getMeasuredState()); largestHeightInRow = Math.max(largestHeightInRow, child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom); if (this._isWrapRequired(child, widthMode, widthSize, flexLine._mainSize, child.getMeasuredWidth() + lp.effectiveMarginLeft + lp.effectiveMarginRight, i, indexInFlexLine)) { if (flexLine.itemCount > 0) { this._addFlexLine(flexLine); } flexLine = new FlexLine(); flexLine._itemCount = 1; flexLine._mainSize = paddingStart + paddingEnd; largestHeightInRow = child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom; indexInFlexLine = 0; } else { flexLine._itemCount++; indexInFlexLine++; } flexLine._mainSize += child.getMeasuredWidth() + lp.effectiveMarginLeft + lp.effectiveMarginRight; flexLine._totalFlexGrow += FlexboxLayout.getFlexGrow(child); flexLine._totalFlexShrink += FlexboxLayout.getFlexShrink(child); 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.effectiveMarginTop); } else { flexLine._maxBaseline = Math.max(flexLine._maxBaseline, child.getMeasuredHeight() - FlexboxLayout.getBaseline(child) + lp.effectiveMarginBottom); } 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++) { const child = this._getReorderedChildAt(i); const lp = child; // .style; if (this.flexWrap !== FlexWrap.WRAP_REVERSE) { let marginTop = flexLine._maxBaseline - FlexboxLayout.getBaseline(child); marginTop = Math.max(marginTop, lp.effectiveMarginTop); largestHeightInLine = Math.max(largestHeightInLine, child.getActualSize().height + marginTop + lp.effectiveMarginBottom); } else { let marginBottom = flexLine._maxBaseline - child.getMeasuredHeight() + FlexboxLayout.getBaseline(child); marginBottom = Math.max(marginBottom, lp.effectiveMarginBottom); largestHeightInLine = Math.max(largestHeightInLine, child.getActualSize().height + lp.effectiveMarginTop + marginBottom); } } flexLine._crossSize = largestHeightInLine; viewIndex += flexLine.itemCount; }); } this._determineCrossSize(this.flexDirection, widthMeasureSpec, heightMeasureSpec, this.effectivePaddingTop + this.effectivePaddingBottom); this._stretchViews(this.flexDirection, this.alignItems); this._setMeasuredDimensionForFlex(this.flexDirection, widthMeasureSpec, heightMeasureSpec, childState); } _measureVertical(widthMeasureSpec, heightMeasureSpec) { const widthSize = getMeasureSpecSize(widthMeasureSpec); const widthMode = getMeasureSpecMode(widthMeasureSpec); const heightSize = getMeasureSpecSize(heightMeasureSpec); const heightMode = getMeasureSpecMode(heightMeasureSpec); let childState = 0; this._flexLines.length = 0; const childCount = this.measureContext.childrenCount; const paddingTop = this.effectivePaddingTop; const paddingBottom = this.effectivePaddingBottom; let largestWidthInColumn = Number.MIN_VALUE; let flexLine = new FlexLine(); flexLine._mainSize = paddingTop + paddingBottom; let indexInFlexLine = 0; for (let i = 0; i < childCount; i++) { const child = this._getReorderedChildAt(i); if (child === null) { this._addFlexLineIfLastFlexItem(i, childCount, flexLine); continue; } else if (child.isCollapsed) { flexLine._itemCount++; this._addFlexLineIfLastFlexItem(i, childCount, flexLine); continue; } child._updateEffectiveLayoutValues(widthSize, widthMode, heightSize, heightMode); const lp = child; // .style; if (FlexboxLayout.getAlignSelf(child) === 'stretch') { flexLine._indicesAlignSelfStretch.push(i); } let childHeight = lp.effectiveHeight; // TODO: This should always be false if (FlexBasisPercent.DEFAULT /* lp.flexBasisPercent */ !== FlexBasisPercent.DEFAULT && heightMode === EXACTLY) { childHeight = Math.round(heightSize * FlexBasisPercent.DEFAULT /* lp.flexBasisPercent */); } const childWidthMeasureSpec = FlexboxLayout.getChildMeasureSpec(widthMeasureSpec, this.effectivePaddingLeft + this.effectivePaddingRight + lp.effectiveMarginLeft + lp.effectiveMarginRight, lp.effectiveWidth < 0 ? WRAP_CONTENT : lp.effectiveWidth); const childHeightMeasureSpec = FlexboxLayout.getChildMeasureSpec(heightMeasureSpec, this.effectivePaddingTop + this.effectivePaddingBottom + lp.effectiveMarginTop + lp.effectiveMarginBottom, 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.effectiveMarginLeft + lp.effectiveMarginRight); if (this._isWrapRequired(child, heightMode, heightSize, flexLine.mainSize, child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom, i, indexInFlexLine)) { if (flexLine._itemCount > 0) { this._addFlexLine(flexLine); } flexLine = new FlexLine(); flexLine._itemCount = 1; flexLine._mainSize = paddingTop + paddingBottom; largestWidthInColumn = child.getMeasuredWidth() + lp.effectiveMarginLeft + lp.effectiveMarginRight; indexInFlexLine = 0; } else { flexLine._itemCount++; indexInFlexLine++; } flexLine._mainSize += child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom; flexLine._totalFlexGrow += FlexboxLayout.getFlexGrow(child); flexLine._totalFlexShrink += FlexboxLayout.getFlexShrink(child); 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.effectivePaddingLeft + this.effectivePaddingRight); this._stretchViews(this.flexDirection, this.alignItems); this._setMeasuredDimensionForFlex(this.flexDirection, widthMeasureSpec, heightMeasureSpec, childState); } _checkSizeConstraints(view) { let needsMeasure = false; let childWidth = view.getMeasuredWidth(); let childHeight = view.getMeasuredHeight(); const minWidth = view.effectiveMinWidth; view.effectiveMinWidth = 0; if (view.getMeasuredWidth() < minWidth) { needsMeasure = true; childWidth = minWidth; } else if (view.getMeasuredWidth() > MAX_SIZE /*lp.maxWidth*/) { needsMeasure = true; childWidth = MAX_SIZE /*lp.maxWidth*/; } const minHeight = view.effectiveMinHeight; view.effectiveMinHeight = 0; if (childHeight < minHeight) { needsMeasure = true; childHeight = minHeight; } else if (childHeight > MAX_SIZE /*lp.maxWidth*/) { needsMeasure = true; childHeight = MAX_SIZE /*lp.maxWidth*/; } if (needsMeasure) { view.measure(makeMeasureSpec(childWidth, EXACTLY), makeMeasureSpec(childHeight, EXACTLY)); } view.effectiveMinWidth = minWidth; view.effectiveMinHeight = minHeight; } _addFlexLineIfLastFlexItem(childIndex, childCount, flexLine) { if (childIndex === childCount - 1 && flexLine.itemCount !== 0) { this._addFlexLine(flexLine); } } _addFlexLine(flexLine) { // Omit divider this._flexLines.push(flexLine); } _determineMainSize(flexDirection, widthMeasureSpec, heightMeasureSpec) { let mainSize; let paddingAlongMainAxis; switch (flexDirection) { case FlexDirection.ROW: case FlexDirection.ROW_REVERSE: { const widthMode = getMeasureSpecMode(widthMeasureSpec); const widthSize = getMeasureSpecSize(widthMeasureSpec); if (widthMode === EXACTLY) { mainSize = widthSize; } else { mainSize = this._getLargestMainSize(); } paddingAlongMainAxis = this.effectivePaddingLeft + this.effectivePaddingRight; break; } case FlexDirection.COLUMN: case FlexDirection.COLUMN_REVERSE: { const heightMode = getMeasureSpecMode(heightMeasureSpec); const heightSize = getMeasureSpecSize(heightMeasureSpec); if (heightMode === EXACTLY) { mainSize = heightSize; } else { mainSize = this._getLargestMainSize(); } paddingAlongMainAxis = this.effectivePaddingTop + this.effectivePaddingBottom; 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); } }); } _expandFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex) { let childIndex = startIndex; if (flexLine._totalFlexGrow <= 0 || maxMainSize < flexLine._mainSize) { childIndex += flexLine._itemCount; return childIndex; } const sizeBeforeExpand = flexLine._mainSize; let needsReexpand = false; const pendingSpace = maxMainSize - flexLine._mainSize; const unitSpace = pendingSpace / flexLine._totalFlexGrow; flexLine._mainSize = paddingAlongMainAxis + flexLine._dividerLengthInMainSize; let accumulatedRoundError = 0; for (let i = 0; i < flexLine.itemCount; i++) { const child = this._getReorderedChildAt(childIndex); if (child === null) { continue; } else if (child.isCollapsed) { childIndex++; continue; } const lp = child; // .style; if (this._isMainAxisDirectionHorizontal(flexDirection)) { if (!this._childrenFrozen[childIndex]) { const flexGrow = FlexboxLayout.getFlexGrow(child); const rawCalculatedWidth = child.getMeasuredWidth() + unitSpace * flexGrow + accumulatedRoundError; let roundedCalculatedWidth = Math.round(rawCalculatedWidth); // TODO: MAX_SIZE is so big, this is always false: if (roundedCalculatedWidth > MAX_SIZE /* lp.maxWidth */) { needsReexpand = true; roundedCalculatedWidth = MAX_SIZE /* lp.maxWidth */; this._childrenFrozen[childIndex] = true; flexLine._totalFlexGrow -= flexGrow; } else { accumulatedRoundError = rawCalculatedWidth - roundedCalculatedWidth; } child.measure(makeMeasureSpec(roundedCalculatedWidth, EXACTLY), makeMeasureSpec(child.getMeasuredHeight(), EXACTLY)); } flexLine._mainSize += child.getMeasuredWidth() + lp.effectiveMarginLeft + lp.effectiveMarginRight; } else { if (!this._childrenFrozen[childIndex]) { const flexGrow = FlexboxLayout.getFlexGrow(child); const rawCalculatedHeight = child.getMeasuredHeight() + unitSpace * flexGrow + accumulatedRoundError; let roundedCalculatedHeight = Math.round(rawCalculatedHeight); // TODO: MAX_SIZE is so big this is always false: if (roundedCalculatedHeight > MAX_SIZE /*lp.maxHeight*/) { needsReexpand = true; roundedCalculatedHeight = MAX_SIZE /*lp.maxHeight*/; this._childrenFrozen[childIndex] = true; flexLine._totalFlexGrow -= flexGrow; } else { accumulatedRoundError = rawCalculatedHeight - roundedCalculatedHeight; } child.measure(makeMeasureSpec(child.getMeasuredWidth(), EXACTLY), makeMeasureSpec(roundedCalculatedHeight, EXACTLY)); } flexLine._mainSize += child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom; } childIndex++; } if (needsReexpand && sizeBeforeExpand !== flexLine._mainSize) { this._expandFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex); } return childIndex; } _shrinkFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex) { let childIndex = startIndex; const sizeBeforeShrink = flexLine._mainSize; if (flexLine._totalFlexShrink <= 0 || maxMainSize > flexLine._mainSize) { childIndex += flexLine.itemCount; return childIndex; } let needsReshrink = false; const unitShrink = (flexLine._mainSize - maxMainSize) / flexLine._totalFlexShrink; let accumulatedRoundError = 0; flexLine._mainSize = paddingAlongMainAxis + flexLine._dividerLengthInMainSize; for (let i = 0; i < flexLine.itemCount; i++) { const child = this._getReorderedChildAt(childIndex); if (child === null) { continue; } else if (child.isCollapsed) { childIndex++; continue; } const lp = child; // .style; if (this._isMainAxisDirectionHorizontal(flexDirection)) { // The direction of main axis is horizontal if (!this._childrenFrozen[childIndex]) { const flexShrink = FlexboxLayout.getFlexShrink(child); const rawCalculatedWidth = child.getMeasuredWidth() - unitShrink * flexShrink + accumulatedRoundError; let roundedCalculatedWidth = Math.round(rawCalculatedWidth); const minWidth = child.effectiveMinWidth; child.effectiveMinWidth = 0; if (roundedCalculatedWidth < minWidth) { needsReshrink = true; roundedCalculatedWidth = minWidth; this._childrenFrozen[childIndex] = true; flexLine._totalFlexShrink -= flexShrink; } else { accumulatedRoundError = rawCalculatedWidth - roundedCalculatedWidth; } const childWidthMeasureSpec = makeMeasureSpec(roundedCalculatedWidth, EXACTLY); // NOTE: for controls that support internal content wrapping (e.g. UILabel) reducing the width // might result in increased height e.g. text that could be shown on one line for larger // width needs to be wrapped in two when width is reduced. // As a result we cannot unconditionally measure with EXACTLY the current measured height const childHeightMeasureSpec = FlexboxLayout.getChildMeasureSpec(this._currentHeightMeasureSpec, lp.effectivePaddingTop + lp.effectivePaddingBottom + lp.effectiveMarginTop + lp.effectiveMarginBottom, lp.effectiveHeight < 0 ? WRAP_CONTENT : lp.effectiveHeight); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); child.effectiveMinWidth = minWidth; // make sure crossSize is up-to-date as child calculated height might have increased flexLine._crossSize = Math.max(flexLine._crossSize, child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom); } flexLine._mainSize += child.getMeasuredWidth() + lp.effectiveMarginLeft + lp.effectiveMarginRight; } else { if (!this._childrenFrozen[childIndex]) { const flexShrink = FlexboxLayout.getFlexShrink(child); const rawCalculatedHeight = child.getMeasuredHeight() - unitShrink * flexShrink + accumulatedRoundError; let roundedCalculatedHeight = Math.round(rawCalculatedHeight); const minHeight = child.effectiveMinHeight; child.effectiveMinHeight = 0; if (roundedCalculatedHeight < minHeight) { needsReshrink = true; roundedCalculatedHeight = minHeight; this._childrenFrozen[childIndex] = true; flexLine._totalFlexShrink -= flexShrink; } else { accumulatedRoundError = rawCalculatedHeight - roundedCalculatedHeight; } child.measure(makeMeasureSpec(child.getMeasuredWidth(), EXACTLY), makeMeasureSpec(roundedCalculatedHeight, EXACTLY)); child.effectiveMinHeight = minHeight; } flexLine._mainSize += child.getMeasuredHeight() + lp.effectiveMarginTop + lp.effectiveMarginBottom; } childIndex++; } if (needsReshrink && sizeBeforeShrink !== flexLine._mainSize) { this._shrinkFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex); } return childIndex; } _determineCrossSize(flexDirection, widthMeasureSpec, heightMeasureSpec, paddingAlongCrossAxis) { 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) { const 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: (() => { const freeSpaceUnit = (size - totalCrossSize) / this._flexLines.length; let accumulatedError = 0; for (let i = 0, flexLinesSize = this._flexLines.length; i < flexLinesSize; i++) { const 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; const numberOfSpaces = this._flexLines.length * 2; spaceTopAndBottom = spaceTopAndBottom / numberOfSpaces; const newFlexLines = []; const 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; const numberOfSpaces = this._flexLines.length - 1; spaceBetweenFlexLine = spaceBetweenFlexLine / numberOfSpaces; let accumulatedError = 0; const newFlexLines = []; for (let i = 0, flexLineSize = this._flexLines.length; i < flexLineSize; i++) { const flexLine = this._flexLines[i]; newFlexLines.push(flexLine); if (i !== this._flexLines.length - 1) { const 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; const newFlexLines = []; const dummySpaceFlexLine = new FlexLine(); dummySpaceFlexLine._crossSize = spaceAboveAndBottom; for (let i = 0, flexLineSize = this._flexLines.length; i < flexLineSize; i++) { if (i === 0) { newFlexLines.push(dummySpaceFlexLine); } const flexLine = this._flexLines[i]; newFlexLines.push(flexLine); if (i === this._flexLines.length - 1) { newFlexLines.push(dummySpaceFlexLine); } } this._flexLines = newFlexLines; break; } case AlignContent.FLEX_END: { const spaceTop = size - totalCrossSize; const dummySpaceFlexLine = new FlexLine(); dummySpaceFlexLine._crossSize = spaceTop; this._flexLines.unshift(dummySpaceFlexLine); break; } } } } } _stretchViews(flexDirection, alignItems) { if (alignItems === AlignItems.STRETCH) { let viewIndex = 0; this._flexLines.forEach((flexLine) => { for (let i = 0; i < flexLine.itemCount; i++, viewIndex++) { const view = this._getReorderedChildAt(viewIndex); const alignSelf = FlexboxLayout.getAlignSelf(view); if (alignSelf !== 'auto' && 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) => { const 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); } }); }); } } _stretchViewVertically(view, crossSize) { let newHeight = crossSize - view.effectiveMarginTop - view.effectiveMarginBottom; newHeight = Math.max(newHeight, 0); const originalMeasuredWidth = view.getMeasuredWidth(); let childWidthMeasureSpec = FlexboxLayout.getChildMeasureSpec(this._currentWidthMeasureSpec, view.effectivePaddingLeft + view.effectivePaddingRight + view.effectiveMarginLeft + view.effectiveMarginRight, view.effectiveWidth < 0 ? WRAP_CONTENT : Math.min(view.effectiveWidth, originalMeasuredWidth)); view.measure(childWidthMeasureSpec, makeMeasureSpec(newHeight, EXACTLY)); if (originalMeasuredWidth > view.getMeasuredWidth()) { childWidthMeasureSpec = makeMeasureSpec(originalMeasuredWidth, EXACTLY); view.measure(childWidthMeasureSpec, makeMeasureSpec(newHeight, EXACTLY)); } } _stretchViewHorizontally(view, crossSize) { let newWidth = crossSize - view.effectiveMarginLeft - view.effectiveMarginRight; newWidth = Math.max(newWidth, 0); view.measure(makeMeasureSpec(newWidth, EXACTLY), makeMeasureSpec(view.getMeasuredHeight(), EXACTLY)); } _setMeasuredDimensionForFlex(flexDirection, widthMeasureSpec, heightMeasureSpec, childState) { const widthMode = getMeasureSpecMode(widthMeasureSpec); let widthSize = getMeasureSpecSize(widthMeasureSpec); const heightMode = getMeasureSpecMode(heightMeasureSpec); let heightSize = getMeasureSpecSize(heightMeasureSpec); let calculatedMaxHeight; let calculatedMaxWidth; switch (flexDirection) { case FlexDirection.ROW: case FlexDirection.ROW_REVERSE: calculatedMaxHeight = this._getSumOfCrossSize() + this.effectivePaddingTop + this.effectivePaddingBottom; calculatedMaxWidth = this._getLargestMainSize(); break; case FlexDirection.COLUMN: case FlexDirection.COLUMN_REVERSE: calculatedMaxHeight = this._getLargestMainSize(); calculatedMaxWidth = this._getSumOfCrossSize() + this.effectivePaddingLeft + this.effectivePaddingRight; 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 >> 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 >> 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); } _isWrapRequired(child, mode, maxSize, currentLength, childLength, childAbsoluteIndex, childRelativeIndexInFlexLine) { if (this.flexWrap === FlexWrap.NOWRAP) { return false; } if (FlexboxLayout.getFlexWrapBefore(child)) { return true; } if (mode === UNSPECIFIED) { return false; } // Omit divider return maxSize < currentLength + childLength; } _getLargestMainSize() { return this._flexLines.reduce((max, flexLine) => Math.max(max, flexLine.mainSize), Number.MIN_VALUE); } _getSumOfCrossSize() { // Omit divider return this._flexLines.reduce((sum, flexLine) => sum + flexLine._crossSize, 0); } _isMainAxisDirectionHorizontal(flexDirection) { return flexDirection === FlexDirection.ROW || flexDirection === FlexDirection.ROW_REVERSE; } onLayout(left, top, right, bottom) { const insets = this.getSafeAreaInsets(); let isRtl; switch (this.flexDirection) { case FlexDirection.ROW: isRtl = false; this._layoutHorizontal(isRtl, left, top, right, bottom, insets); break; case FlexDirection.ROW_REVERSE: isRtl = true; this._layoutHorizontal(isRtl, left, top, right, bottom, insets); break; case FlexDirection.COLUMN: isRtl = false; if (this.flexWrap === FlexWrap.WRAP_REVERSE) { isRtl = !isRtl; } this._layoutVertical(isRtl, false, left, top, right, bottom, insets); break; case FlexDirection.COLUMN_REVERSE: isRtl = false; if (this.flexWrap === FlexWrap.WRAP_REVERSE) { isRtl = !isRtl; } this._layoutVertical(isRtl, true, left, top, right, bottom, insets); break; default: throw new Error('Invalid flex direction is set: ' + this.flexDirection); } } _layoutHorizontal(isRtl, left, top, right, bottom, insets) { // include insets const paddingLeft = this.effectivePaddingLeft + insets.left; const paddingTop = this.effectivePaddingTop + insets.top; const paddingRight = this.effectivePaddingRight + insets.right; const paddingBottom = this.effectivePaddingBottom + insets.bottom; let childLeft; let currentViewIndex = 0; const height = bottom - top; const width = right - left; // include insets let childBottom = height - paddingBottom; let childTop = 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 - insets.left - insets.right - flexLine._mainSize) / 2.0; childRight = width - paddingRight - (width - insets.left - insets.right - flexLine._mainSize) / 2.0; break; case JustifyContent.SPACE_AROUND: if (flexLine._itemCount !== 0) { spaceBetweenItem = (width - insets.left - insets.right - flexLine.mainSize) / flexLine._itemCount; } childLeft = paddingLeft + spaceBetweenItem / 2.0; childRight = width - paddingRight - spaceBetweenItem / 2.0; break; case JustifyContent.SPACE_BETWEEN: { childLeft = paddingLeft; const denominator = flexLine.itemCount !== 1 ? flexLine.itemCount - 1 : 1.0; spaceBetweenItem = (width - insets.left - insets.right - 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++) { const child = this._getReorderedChildAt(currentViewIndex); if (child === null) { continue; } else if (child.isCollapsed) { currentViewIndex++; continue; } const lp = child; // .style; childLeft += lp.effectiveMarginLeft; childRight -= lp.effectiveMarginRight; // 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.effectiveMarginRight; childRight -= child.getMeasuredWidth() + spaceBetweenItem + lp.effectiveMarginLeft; currentViewIndex++; const bounds = child._getCurrentLayoutBounds(); flexLine._left = Math.min(flexLine._left, bounds.left - lp.effectiveMarginLeft); flexLine._top = Math.min(flexLine._top, bounds.top - lp.effectiveMarginTop); flexLine._right = Math.max(flexLine._right, bounds.right + lp.effectiveMarginRight); flexLine._bottom = Math.max(flexLine._bottom, bounds.bottom + lp.effectiveMarginBottom);