/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.material.internal; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static com.google.android.material.internal.FlexItem.FLEX_BASIS_PERCENT_DEFAULT; import static android.support.v7.widget.RecyclerView.NO_POSITION; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.util.SparseIntArray; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Offers various calculations for Flexbox to use the common logic between the classes such as * {@link FlexboxLayout}. * * @hide */ @RestrictTo(LIBRARY_GROUP) class FlexboxHelper { private static final int INITIAL_CAPACITY = 10; private final FlexContainer flexContainer; /** * Holds the 'frozen' state of children during measure. If a view is frozen it will no longer * expand or shrink regardless of flex grow/flex shrink attributes. */ private boolean[] childrenFrozen; FlexboxHelper(FlexContainer flexContainer) { this.flexContainer = flexContainer; } /** * Create an array, which indicates the reordered indices that {@link FlexItem#getOrder()} * attributes are taken into account. This method takes a View before that is added as the parent * ViewGroup's children. * * @param viewBeforeAdded the View instance before added to the array of children Views of the * parent ViewGroup * @param indexForViewBeforeAdded the index for the View before added to the array of the parent * ViewGroup * @param paramsForViewBeforeAdded the layout parameters for the View before added to the array of * the parent ViewGroup * @return an array which have the reordered indices */ int[] createReorderedIndices( View viewBeforeAdded, int indexForViewBeforeAdded, ViewGroup.LayoutParams paramsForViewBeforeAdded, SparseIntArray orderCache) { int childCount = flexContainer.getFlexItemCount(); List orders = createOrders(childCount); Order orderForViewToBeAdded = new Order(); if (viewBeforeAdded != null && paramsForViewBeforeAdded instanceof FlexItem) { orderForViewToBeAdded.order = ((FlexItem) paramsForViewBeforeAdded).getOrder(); } else { orderForViewToBeAdded.order = FlexItem.ORDER_DEFAULT; } if (indexForViewBeforeAdded == -1 || indexForViewBeforeAdded == childCount) { orderForViewToBeAdded.index = childCount; } else if (indexForViewBeforeAdded < flexContainer.getFlexItemCount()) { orderForViewToBeAdded.index = indexForViewBeforeAdded; for (int i = indexForViewBeforeAdded; i < childCount; i++) { orders.get(i).index++; } } else { // This path is not expected since OutOfBoundException will be thrown in the ViewGroup // But setting the index for fail-safe orderForViewToBeAdded.index = childCount; } orders.add(orderForViewToBeAdded); return sortOrdersIntoReorderedIndices(childCount + 1, orders, orderCache); } /** * Create an array, which indicates the reordered indices that {@link FlexItem#getOrder()} * attributes are taken into account. * * @return @return an array which have the reordered indices */ int[] createReorderedIndices(SparseIntArray orderCache) { int childCount = flexContainer.getFlexItemCount(); List orders = createOrders(childCount); return sortOrdersIntoReorderedIndices(childCount, orders, orderCache); } @NonNull private List createOrders(int childCount) { List orders = new ArrayList<>(childCount); for (int i = 0; i < childCount; i++) { View child = flexContainer.getFlexItemAt(i); FlexItem flexItem = (FlexItem) child.getLayoutParams(); Order order = new Order(); order.order = flexItem.getOrder(); order.index = i; orders.add(order); } return orders; } /** * Returns if any of the children's {@link FlexItem#getOrder()} attributes are changed from the * last measurement. * * @return {@code true} if changed from the last measurement, {@code false} otherwise. */ boolean isOrderChangedFromLastMeasurement(SparseIntArray orderCache) { int childCount = flexContainer.getFlexItemCount(); if (orderCache.size() != childCount) { return true; } for (int i = 0; i < childCount; i++) { View view = flexContainer.getFlexItemAt(i); if (view == null) { continue; } FlexItem flexItem = (FlexItem) view.getLayoutParams(); if (flexItem.getOrder() != orderCache.get(i)) { return true; } } return false; } private int[] sortOrdersIntoReorderedIndices( int childCount, List orders, SparseIntArray orderCache) { Collections.sort(orders); orderCache.clear(); int[] reorderedIndices = new int[childCount]; int i = 0; for (Order order : orders) { reorderedIndices[i] = order.index; orderCache.append(order.index, order.order); i++; } return reorderedIndices; } /** * Calculate how many flex lines are needed in the flex container. This method should calculate * all the flex lines from the existing flex items. * * @see #calculateFlexLines(FlexLinesResult, int, int, int, int, int, List) */ void calculateHorizontalFlexLines( FlexLinesResult result, int widthMeasureSpec, int heightMeasureSpec) { calculateFlexLines( result, widthMeasureSpec, heightMeasureSpec, Integer.MAX_VALUE, 0, NO_POSITION, null); } /** * Calculates how many flex lines are needed in the flex container layout by measuring each child. * Expanding or shrinking the flex items depending on the flex grow and flex shrink attributes are * done in a later procedure, so the views' measured width and measured height may be changed in a * later process. * * @param result an instance of {@link FlexLinesResult} that is going to contain a list of flex * lines and the child state used by {@link View#setMeasuredDimension(int, int)}. * @param mainMeasureSpec the main axis measure spec imposed by the flex container, width for * horizontal direction, height otherwise * @param crossMeasureSpec the cross axis measure spec imposed by the flex container, height for * horizontal direction, width otherwise * @param needsCalcAmount the amount of pixels where flex line calculation should be stopped this * is needed to avoid the expensive calculation if the calculation is needed only the small * part of the entire flex container. * @param fromIndex the index of the child from which the calculation starts * @param toIndex the index of the child to which the calculation ends (until the flex line which * include the which who has that index). If this and needsCalcAmount are both set, first flex * lines are calculated to the index, calculate the amount of pixels as the needsCalcAmount * argument in addition to that * @param existingLines If not null, calculated flex lines will be added to this instance */ private void calculateFlexLines( FlexLinesResult result, int mainMeasureSpec, int crossMeasureSpec, int needsCalcAmount, int fromIndex, int toIndex, @Nullable List existingLines) { boolean isMainHorizontal = flexContainer.isMainAxisDirectionHorizontal(); int mainMode = View.MeasureSpec.getMode(mainMeasureSpec); int mainSize = View.MeasureSpec.getSize(mainMeasureSpec); int childState = 0; List flexLines; if (existingLines == null) { flexLines = new ArrayList<>(); } else { flexLines = existingLines; } result.flexLines = flexLines; boolean reachedToIndex = toIndex == NO_POSITION; int mainPaddingStart = getPaddingStartMain(isMainHorizontal); int mainPaddingEnd = getPaddingEndMain(isMainHorizontal); int crossPaddingStart = getPaddingStartCross(isMainHorizontal); int crossPaddingEnd = getPaddingEndCross(isMainHorizontal); int largestSizeInCross = Integer.MIN_VALUE; // The amount of cross size calculated in this method call. int sumCrossSize = 0; // The index of the view in the flex line. int indexInFlexLine = 0; FlexLine flexLine = new FlexLine(); flexLine.firstIndex = fromIndex; flexLine.mainSize = mainPaddingStart + mainPaddingEnd; int childCount = flexContainer.getFlexItemCount(); for (int i = fromIndex; i < childCount; i++) { View child = flexContainer.getReorderedFlexItemAt(i); if (child == null) { if (isLastFlexItem(i, childCount, flexLine)) { addFlexLine(flexLines, flexLine, i, sumCrossSize); } continue; } else if (child.getVisibility() == View.GONE) { flexLine.goneItemCount++; flexLine.itemCount++; if (isLastFlexItem(i, childCount, flexLine)) { addFlexLine(flexLines, flexLine, i, sumCrossSize); } continue; } FlexItem flexItem = (FlexItem) child.getLayoutParams(); flexLine.indicesAlignSelfStretch.add(i); int childMainSize = getFlexItemSizeMain(flexItem, isMainHorizontal); if (flexItem.getFlexBasisPercent() != FLEX_BASIS_PERCENT_DEFAULT && mainMode == View.MeasureSpec.EXACTLY) { childMainSize = Math.round(mainSize * flexItem.getFlexBasisPercent()); // Use the dimension from the layout if the mainMode is not // MeasureSpec.EXACTLY even if any fraction value is set to // layout_flexBasisPercent. } int childMainMeasureSpec; int childCrossMeasureSpec; if (isMainHorizontal) { childMainMeasureSpec = flexContainer.getChildWidthMeasureSpec( mainMeasureSpec, mainPaddingStart + mainPaddingEnd + getFlexItemMarginStartMain(flexItem, true) + getFlexItemMarginEndMain(flexItem, true), childMainSize); childCrossMeasureSpec = flexContainer.getChildHeightMeasureSpec( crossMeasureSpec, crossPaddingStart + crossPaddingEnd + getFlexItemMarginStartCross(flexItem, true) + getFlexItemMarginEndCross(flexItem, true) + sumCrossSize, getFlexItemSizeCross(flexItem, true)); child.measure(childMainMeasureSpec, childCrossMeasureSpec); } else { childCrossMeasureSpec = flexContainer.getChildWidthMeasureSpec( crossMeasureSpec, crossPaddingStart + crossPaddingEnd + getFlexItemMarginStartCross(flexItem, false) + getFlexItemMarginEndCross(flexItem, false) + sumCrossSize, getFlexItemSizeCross(flexItem, false)); childMainMeasureSpec = flexContainer.getChildHeightMeasureSpec( mainMeasureSpec, mainPaddingStart + mainPaddingEnd + getFlexItemMarginStartMain(flexItem, false) + getFlexItemMarginEndMain(flexItem, false), childMainSize); child.measure(childCrossMeasureSpec, childMainMeasureSpec); } flexContainer.updateViewCache(i, child); // Check the size constraint after the first measurement for the child // To prevent the child's width/height violate the size constraints imposed by the // {@link FlexItem#getMinWidth()}, {@link FlexItem#getMinHeight()}, // {@link FlexItem#getMaxWidth()} and {@link FlexItem#getMaxHeight()} attributes. // E.g. When the child's layout_width is wrap_content the measured width may be // less than the min width after the first measurement. checkSizeConstraints(child, i); childState = View.combineMeasuredStates(childState, child.getMeasuredState()); if (isWrapRequired( child, mainMode, mainSize, flexLine.mainSize, getViewMeasuredSizeMain(child, isMainHorizontal) + getFlexItemMarginStartMain(flexItem, isMainHorizontal) + getFlexItemMarginEndMain(flexItem, isMainHorizontal), flexItem, i, indexInFlexLine)) { if (flexLine.getItemCountNotGone() > 0) { addFlexLine(flexLines, flexLine, i > 0 ? i - 1 : 0, sumCrossSize); sumCrossSize += flexLine.crossSize; } if (isMainHorizontal) { if (flexItem.getHeight() == ViewGroup.LayoutParams.MATCH_PARENT) { // This case takes care of the corner case where the cross size of the // child is affected by the just added flex line. // E.g. when the child's layout_height is set to match_parent, the height // of that child needs to be determined taking the total cross size used // so far into account. In that case, the height of the child needs to be // measured again note that we don't need to judge if the wrapping occurs // because it doesn't change the size along the main axis. childCrossMeasureSpec = flexContainer.getChildHeightMeasureSpec( crossMeasureSpec, flexContainer.getPaddingTop() + flexContainer.getPaddingBottom() + flexItem.getMarginTop() + flexItem.getMarginBottom() + sumCrossSize, flexItem.getHeight()); child.measure(childMainMeasureSpec, childCrossMeasureSpec); checkSizeConstraints(child, i); } } else { if (flexItem.getWidth() == ViewGroup.LayoutParams.MATCH_PARENT) { // This case takes care of the corner case where the cross size of the // child is affected by the just added flex line. // E.g. when the child's layout_width is set to match_parent, the width // of that child needs to be determined taking the total cross size used // so far into account. In that case, the width of the child needs to be // measured again note that we don't need to judge if the wrapping occurs // because it doesn't change the size along the main axis. childCrossMeasureSpec = flexContainer.getChildWidthMeasureSpec( crossMeasureSpec, flexContainer.getPaddingLeft() + flexContainer.getPaddingRight() + flexItem.getMarginLeft() + flexItem.getMarginRight() + sumCrossSize, flexItem.getWidth()); child.measure(childCrossMeasureSpec, childMainMeasureSpec); checkSizeConstraints(child, i); } } flexLine = new FlexLine(); flexLine.itemCount = 1; flexLine.mainSize = mainPaddingStart + mainPaddingEnd; flexLine.firstIndex = i; indexInFlexLine = 0; largestSizeInCross = Integer.MIN_VALUE; } else { flexLine.itemCount++; indexInFlexLine++; } flexLine.mainSize += getViewMeasuredSizeMain(child, isMainHorizontal) + getFlexItemMarginStartMain(flexItem, isMainHorizontal) + getFlexItemMarginEndMain(flexItem, isMainHorizontal); flexLine.motalFlexGrow += flexItem.getFlexGrow(); flexLine.totalFlexShrink += flexItem.getFlexShrink(); flexContainer.onNewFlexItemAdded(child, i, indexInFlexLine, flexLine); largestSizeInCross = Math.max( largestSizeInCross, getViewMeasuredSizeCross(child, isMainHorizontal) + getFlexItemMarginStartCross(flexItem, isMainHorizontal) + getFlexItemMarginEndCross(flexItem, isMainHorizontal) + flexContainer.getDecorationLengthCrossAxis(child)); // Temporarily set the cross axis length as the largest child in the flexLine // Expand along the cross axis depending on the mAlignContent property if needed // later flexLine.crossSize = Math.max(flexLine.crossSize, largestSizeInCross); if (isMainHorizontal) { flexLine.maxBaseline = Math.max(flexLine.maxBaseline, child.getBaseline() + flexItem.getMarginTop()); } if (isLastFlexItem(i, childCount, flexLine)) { addFlexLine(flexLines, flexLine, i, sumCrossSize); sumCrossSize += flexLine.crossSize; } if (toIndex != NO_POSITION && !flexLines.isEmpty() && flexLines.get(flexLines.size() - 1).lastIndex >= toIndex && i >= toIndex && !reachedToIndex) { // Calculated to include a flex line which includes the flex item having the // toIndex. // Let the sumCrossSize start from the negative value of the last flex line's // cross size because otherwise flex lines aren't calculated enough to fill the // visible area. sumCrossSize = -flexLine.getCrossSize(); reachedToIndex = true; } if (sumCrossSize > needsCalcAmount && reachedToIndex) { // Stop the calculation if the sum of cross size calculated reached to the point // beyond the needsCalcAmount value to avoid unneeded calculation in a // RecyclerView. // To be precise, the decoration length may be added to the sumCrossSize, // but we omit adding the decoration length because even without the decorator // length, it's guaranteed that calculation is done at least beyond the // needsCalcAmount break; } } result.childState = childState; } /** * Returns the container's start padding in the main axis. Either start or top. * * @param isMainHorizontal is the main axis horizontal * @return the start padding in the main axis */ private int getPaddingStartMain(boolean isMainHorizontal) { if (isMainHorizontal) { return flexContainer.getPaddingStart(); } return flexContainer.getPaddingTop(); } /** * Returns the container's end padding in the main axis. Either end or bottom. * * @param isMainHorizontal is the main axis horizontal * @return the end padding in the main axis */ private int getPaddingEndMain(boolean isMainHorizontal) { if (isMainHorizontal) { return flexContainer.getPaddingEnd(); } return flexContainer.getPaddingBottom(); } /** * Returns the container's start padding in the cross axis. Either start or top. * * @param isMainHorizontal is the main axis horizontal. * @return the start padding in the cross axis */ private int getPaddingStartCross(boolean isMainHorizontal) { if (isMainHorizontal) { return flexContainer.getPaddingTop(); } return flexContainer.getPaddingStart(); } /** * Returns the container's end padding in the cross axis. Either end or bottom. * * @param isMainHorizontal is the main axis horizontal * @return the end padding in the cross axis */ private int getPaddingEndCross(boolean isMainHorizontal) { if (isMainHorizontal) { return flexContainer.getPaddingBottom(); } return flexContainer.getPaddingEnd(); } /** * Returns the view's measured size in the main axis. Either width or height. * * @param view the view * @param isMainHorizontal is the main axis horizontal * @return the view's measured size in the main axis */ private int getViewMeasuredSizeMain(View view, boolean isMainHorizontal) { if (isMainHorizontal) { return view.getMeasuredWidth(); } return view.getMeasuredHeight(); } /** * Returns the view's measured size in the cross axis. Either width or height. * * @param view the view * @param isMainHorizontal is the main axis horizontal * @return the view's measured size in the cross axis */ private int getViewMeasuredSizeCross(View view, boolean isMainHorizontal) { if (isMainHorizontal) { return view.getMeasuredHeight(); } return view.getMeasuredWidth(); } /** * Returns the flexItem's size in the main axis. Either width or height. * * @param flexItem the flexItem * @param isMainHorizontal is the main axis horizontal * @return the flexItem's size in the main axis */ private int getFlexItemSizeMain(FlexItem flexItem, boolean isMainHorizontal) { if (isMainHorizontal) { return flexItem.getWidth(); } return flexItem.getHeight(); } /** * Returns the flexItem's size in the cross axis. Either width or height. * * @param flexItem the flexItem * @param isMainHorizontal is the main axis horizontal * @return the flexItem's size in the cross axis */ private int getFlexItemSizeCross(FlexItem flexItem, boolean isMainHorizontal) { if (isMainHorizontal) { return flexItem.getHeight(); } return flexItem.getWidth(); } /** * Returns the flexItem's start margin in the main axis. Either start or top. For the backward * compatibility for API level < 17, the horizontal margin is returned using {@link * FlexItem#getMarginLeft} (ViewGroup.MarginLayoutParams#getMarginStart isn't available in API * level < 17). Thus this method needs to be used with {@link #getFlexItemMarginEndMain} not to * misuse the margin in RTL. * * @param flexItem the flexItem * @param isMainHorizontal is the main axis horizontal * @return the flexItem's start margin in the main axis */ private int getFlexItemMarginStartMain(FlexItem flexItem, boolean isMainHorizontal) { if (isMainHorizontal) { return flexItem.getMarginLeft(); } return flexItem.getMarginTop(); } /** * Returns the flexItem's end margin in the main axis. Either end or bottom. For the backward * compatibility for API level < 17, the horizontal margin is returned using {@link * FlexItem#getMarginRight} (ViewGroup.MarginLayoutParams#getMarginEnd isn't available in API * level < 17). Thus this method needs to be used with {@link #getFlexItemMarginStartMain} not to * misuse the margin in RTL. * * @param flexItem the flexItem * @param isMainHorizontal is the main axis horizontal * @return the flexItem's end margin in the main axis */ private int getFlexItemMarginEndMain(FlexItem flexItem, boolean isMainHorizontal) { if (isMainHorizontal) { return flexItem.getMarginRight(); } return flexItem.getMarginBottom(); } /** * Returns the flexItem's start margin in the cross axis. Either start or top. For the backward * compatibility for API level < 17, the horizontal margin is returned using {@link * FlexItem#getMarginLeft} (ViewGroup.MarginLayoutParams#getMarginStart isn't available in API * level < 17). Thus this method needs to be used with {@link #getFlexItemMarginEndCross} to not * to misuse the margin in RTL. * * @param flexItem the flexItem * @param isMainHorizontal is the main axis horizontal * @return the flexItem's start margin in the cross axis */ private int getFlexItemMarginStartCross(FlexItem flexItem, boolean isMainHorizontal) { if (isMainHorizontal) { return flexItem.getMarginTop(); } return flexItem.getMarginLeft(); } /** * Returns the flexItem's end margin in the cross axis. Either end or bottom. For the backward * compatibility for API level < 17, the horizontal margin is returned using {@link * FlexItem#getMarginRight} (ViewGroup.MarginLayoutParams#getMarginEnd isn't available in API * level < 17). Thus this method needs to be used with {@link #getFlexItemMarginStartCross} to not * to misuse the margin in RTL. * * @param flexItem the flexItem * @param isMainHorizontal is the main axis horizontal * @return the flexItem's end margin in the cross axis */ private int getFlexItemMarginEndCross(FlexItem flexItem, boolean isMainHorizontal) { if (isMainHorizontal) { return flexItem.getMarginBottom(); } return flexItem.getMarginRight(); } /** * Determine if a wrap is required (add a new flex line). * * @param view the view being judged if the wrap required * @param mode the width or height mode along the main axis direction * @param maxSize the max size along the main axis direction * @param currentLength the accumulated current length * @param childLength the length of a child view which is to be collected to the flex line * @param flexItem the LayoutParams for the view being determined whether a new flex line is * needed * @return {@code true} if a wrap is required, {@code false} otherwise * @see FlexContainer#getFlexWrap() * @see FlexContainer#setFlexWrap(int) */ private boolean isWrapRequired( View view, int mode, int maxSize, int currentLength, int childLength, FlexItem flexItem, int index, int indexInFlexLine) { if (flexContainer.getFlexWrap() == FlexWrap.NOWRAP) { return false; } if (flexItem.isWrapBefore()) { return true; } if (mode == View.MeasureSpec.UNSPECIFIED) { return false; } int decorationLength = flexContainer.getDecorationLengthMainAxis(view, index, indexInFlexLine); if (decorationLength > 0) { childLength += decorationLength; } return maxSize < currentLength + childLength; } private boolean isLastFlexItem(int childIndex, int childCount, FlexLine flexLine) { return childIndex == childCount - 1 && flexLine.getItemCountNotGone() != 0; } private void addFlexLine( List flexLines, FlexLine flexLine, int viewIndex, int usedCrossSizeSoFar) { flexLine.sumCrossSizeBefore = usedCrossSizeSoFar; flexContainer.onNewFlexLineAdded(flexLine); flexLine.lastIndex = viewIndex; flexLines.add(flexLine); } /** * Checks if the view's width/height don't violate the minimum/maximum size constraints imposed by * the {@link FlexItem#getMinWidth()}, {@link FlexItem#getMinHeight()}, {@link * FlexItem#getMaxWidth()} and {@link FlexItem#getMaxHeight()} attributes. * * @param view the view to be checked * @param index index of the view */ private void checkSizeConstraints(View view, int index) { boolean needsMeasure = false; FlexItem flexItem = (FlexItem) view.getLayoutParams(); int childWidth = view.getMeasuredWidth(); int childHeight = view.getMeasuredHeight(); if (childWidth < flexItem.getMinWidth()) { needsMeasure = true; childWidth = flexItem.getMinWidth(); } else if (childWidth > flexItem.getMaxWidth()) { needsMeasure = true; childWidth = flexItem.getMaxWidth(); } if (childHeight < flexItem.getMinHeight()) { needsMeasure = true; childHeight = flexItem.getMinHeight(); } else if (childHeight > flexItem.getMaxHeight()) { needsMeasure = true; childHeight = flexItem.getMaxHeight(); } if (needsMeasure) { int widthSpec = View.MeasureSpec.makeMeasureSpec(childWidth, View.MeasureSpec.EXACTLY); int heightSpec = View.MeasureSpec.makeMeasureSpec(childHeight, View.MeasureSpec.EXACTLY); view.measure(widthSpec, heightSpec); flexContainer.updateViewCache(index, view); } } /** @see #determineMainSize(int, int, int) */ void determineMainSize(int widthMeasureSpec, int heightMeasureSpec) { determineMainSize(widthMeasureSpec, heightMeasureSpec, 0); } /** * Determine the main size by expanding (shrinking if negative remaining free space is given) an * individual child in each flex line if any children's mFlexGrow (or mFlexShrink if remaining * space is negative) properties are set to non-zero. * * @param widthMeasureSpec horizontal space requirements as imposed by the parent * @param heightMeasureSpec vertical space requirements as imposed by the parent */ private void determineMainSize(int widthMeasureSpec, int heightMeasureSpec, int fromIndex) { ensureChildrenFrozen(flexContainer.getFlexItemCount()); if (fromIndex >= flexContainer.getFlexItemCount()) { return; } int mainSize; int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); int widthSize = View.MeasureSpec.getSize(widthMeasureSpec); if (widthMode == View.MeasureSpec.EXACTLY) { mainSize = widthSize; } else { mainSize = flexContainer.getLargestMainSize(); } int paddingAlongMainAxis = flexContainer.getPaddingLeft() + flexContainer.getPaddingRight(); int flexLineIndex = 0; List flexLines = flexContainer.getFlexLinesInternal(); for (int i = flexLineIndex, size = flexLines.size(); i < size; i++) { FlexLine flexLine = flexLines.get(i); if (flexLine.mainSize < mainSize) { expandFlexItems(heightMeasureSpec, flexLine, mainSize, paddingAlongMainAxis, false); } else { shrinkFlexItems(heightMeasureSpec, flexLine, mainSize, paddingAlongMainAxis, false); } } } private void ensureChildrenFrozen(int size) { if (childrenFrozen == null) { childrenFrozen = new boolean[Math.max(INITIAL_CAPACITY, size)]; } else if (childrenFrozen.length < size) { int newCapacity = childrenFrozen.length * 2; childrenFrozen = new boolean[Math.max(newCapacity, size)]; } else { Arrays.fill(childrenFrozen, false); } } /** * Expand the flex items along the main axis based on the individual mFlexGrow attribute. * * @param heightMeasureSpec the vertical space requirements as imposed by the parent * @param flexLine the flex line to which flex items belong * @param maxMainSize the maximum main size. Expanded main size will be this size * @param paddingAlongMainAxis the padding value along the main axis * @param calledRecursively true if this method is called recursively, false otherwise * @see FlexItem#getFlexGrow() */ private void expandFlexItems( int heightMeasureSpec, FlexLine flexLine, int maxMainSize, int paddingAlongMainAxis, boolean calledRecursively) { if (flexLine.motalFlexGrow <= 0 || maxMainSize < flexLine.mainSize) { return; } int sizeBeforeExpand = flexLine.mainSize; boolean needsReexpand = false; float unitSpace = (maxMainSize - flexLine.mainSize) / flexLine.motalFlexGrow; flexLine.mainSize = paddingAlongMainAxis + flexLine.dividerLengthInMainSize; // Setting the cross size of the flex line as the temporal value since the cross size of // each flex item may be changed from the initial calculation // (in the measureHorizontal/measureVertical method) even this method is part of the main // size determination. // E.g. If a TextView's layout_width is set to 0dp, layout_height is set to wrap_content, // and layout_flexGrow is set to 1, the TextView is trying to expand to the vertical // direction to enclose its content (in the measureHorizontal method), but // the width will be expanded in this method. In that case, the height needs to be measured // again with the expanded width. int largestCrossSize = 0; if (!calledRecursively) { flexLine.crossSize = Integer.MIN_VALUE; } float accumulatedRoundError = 0; for (int i = 0; i < flexLine.itemCount; i++) { int index = flexLine.firstIndex + i; View child = flexContainer.getReorderedFlexItemAt(index); if (child == null || child.getVisibility() == View.GONE) { continue; } FlexItem flexItem = (FlexItem) child.getLayoutParams(); // The direction of the main axis is horizontal int childMeasuredWidth = child.getMeasuredWidth(); int childMeasuredHeight = child.getMeasuredHeight(); if (!childrenFrozen[index] && flexItem.getFlexGrow() > 0f) { float rawCalculatedWidth = childMeasuredWidth + unitSpace * flexItem.getFlexGrow(); if (i == flexLine.itemCount - 1) { rawCalculatedWidth += accumulatedRoundError; accumulatedRoundError = 0; } int newWidth = Math.round(rawCalculatedWidth); if (newWidth > flexItem.getMaxWidth()) { // This means the child can't expand beyond the value of the mMaxWidth // attribute. // To adjust the flex line length to the size of maxMainSize, remaining // positive free space needs to be re-distributed to other flex items // (children views). In that case, invoke this method again with the same // fromIndex. needsReexpand = true; newWidth = flexItem.getMaxWidth(); childrenFrozen[index] = true; flexLine.motalFlexGrow -= flexItem.getFlexGrow(); } else { accumulatedRoundError += (rawCalculatedWidth - newWidth); if (accumulatedRoundError > 1.0) { newWidth += 1; accumulatedRoundError -= 1.0f; } else if (accumulatedRoundError < -1.0) { newWidth -= 1; accumulatedRoundError += 1.0f; } } int childHeightMeasureSpec = getChildHeightMeasureSpecInternal( heightMeasureSpec, flexItem, flexLine.sumCrossSizeBefore); int childWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(newWidth, View.MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); childMeasuredWidth = child.getMeasuredWidth(); childMeasuredHeight = child.getMeasuredHeight(); flexContainer.updateViewCache(index, child); } largestCrossSize = Math.max( largestCrossSize, childMeasuredHeight + flexItem.getMarginTop() + flexItem.getMarginBottom() + flexContainer.getDecorationLengthCrossAxis(child)); flexLine.mainSize += childMeasuredWidth + flexItem.getMarginLeft() + flexItem.getMarginRight(); flexLine.crossSize = Math.max(flexLine.crossSize, largestCrossSize); } if (needsReexpand && sizeBeforeExpand != flexLine.mainSize) { // Re-invoke the method with the same flex line to distribute the positive free space // that wasn't fully distributed (because of maximum length constraint) expandFlexItems(heightMeasureSpec, flexLine, maxMainSize, paddingAlongMainAxis, true); } } /** * Shrink the flex items along the main axis based on the individual mFlexShrink attribute. * * @param heightMeasureSpec the vertical space requirements as imposed by the parent * @param flexLine the flex line to which flex items belong * @param maxMainSize the maximum main size. Shrank main size will be this size * @param paddingAlongMainAxis the padding value along the main axis * @param calledRecursively true if this method is called recursively, false otherwise * @see FlexItem#getFlexShrink() */ private void shrinkFlexItems( int heightMeasureSpec, FlexLine flexLine, int maxMainSize, int paddingAlongMainAxis, boolean calledRecursively) { int sizeBeforeShrink = flexLine.mainSize; if (flexLine.totalFlexShrink <= 0 || maxMainSize > flexLine.mainSize) { return; } boolean needsReshrink = false; float unitShrink = (flexLine.mainSize - maxMainSize) / flexLine.totalFlexShrink; float accumulatedRoundError = 0; flexLine.mainSize = paddingAlongMainAxis + flexLine.dividerLengthInMainSize; // Setting the cross size of the flex line as the temporal value since the cross size of // each flex item may be changed from the initial calculation // (in the measureHorizontal/measureVertical method) even this method is part of the main // size determination. // E.g. If a TextView's layout_width is set to 0dp, layout_height is set to wrap_content, // and layout_flexGrow is set to 1, the TextView is trying to expand to the vertical // direction to enclose its content (in the measureHorizontal method), but // the width will be expanded in this method. In that case, the height needs to be measured // again with the expanded width. int largestCrossSize = 0; if (!calledRecursively) { flexLine.crossSize = Integer.MIN_VALUE; } for (int i = 0; i < flexLine.itemCount; i++) { int index = flexLine.firstIndex + i; View child = flexContainer.getReorderedFlexItemAt(index); if (child == null || child.getVisibility() == View.GONE) { continue; } FlexItem flexItem = (FlexItem) child.getLayoutParams(); // The direction of main axis is horizontal int childMeasuredWidth = child.getMeasuredWidth(); int childMeasuredHeight = child.getMeasuredHeight(); if (!childrenFrozen[index] && flexItem.getFlexShrink() > 0f) { float rawCalculatedWidth = childMeasuredWidth - unitShrink * flexItem.getFlexShrink(); if (i == flexLine.itemCount - 1) { rawCalculatedWidth += accumulatedRoundError; accumulatedRoundError = 0; } int newWidth = Math.round(rawCalculatedWidth); if (newWidth < flexItem.getMinWidth()) { // This means the child doesn't have enough space to distribute the negative // free space. To adjust the flex line length down to the maxMainSize, // remaining // negative free space needs to be re-distributed to other flex items // (children views). In that case, invoke this method again with the same // fromIndex. needsReshrink = true; newWidth = flexItem.getMinWidth(); childrenFrozen[index] = true; flexLine.totalFlexShrink -= flexItem.getFlexShrink(); } else { accumulatedRoundError += (rawCalculatedWidth - newWidth); if (accumulatedRoundError > 1.0) { newWidth += 1; accumulatedRoundError -= 1; } else if (accumulatedRoundError < -1.0) { newWidth -= 1; accumulatedRoundError += 1; } } int childHeightMeasureSpec = getChildHeightMeasureSpecInternal( heightMeasureSpec, flexItem, flexLine.sumCrossSizeBefore); int childWidthMeasureSpec = View.MeasureSpec.makeMeasureSpec(newWidth, View.MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); childMeasuredWidth = child.getMeasuredWidth(); childMeasuredHeight = child.getMeasuredHeight(); flexContainer.updateViewCache(index, child); } largestCrossSize = Math.max( largestCrossSize, childMeasuredHeight + flexItem.getMarginTop() + flexItem.getMarginBottom() + flexContainer.getDecorationLengthCrossAxis(child)); flexLine.mainSize += childMeasuredWidth + flexItem.getMarginLeft() + flexItem.getMarginRight(); flexLine.crossSize = Math.max(flexLine.crossSize, largestCrossSize); } if (needsReshrink && sizeBeforeShrink != flexLine.mainSize) { // Re-invoke the method with the same fromIndex to distribute the negative free space // that wasn't fully distributed (because some views length were not enough) shrinkFlexItems(heightMeasureSpec, flexLine, maxMainSize, paddingAlongMainAxis, true); } } private int getChildHeightMeasureSpecInternal( int heightMeasureSpec, FlexItem flexItem, int padding) { int childHeightMeasureSpec = flexContainer.getChildHeightMeasureSpec( heightMeasureSpec, flexContainer.getPaddingTop() + flexContainer.getPaddingBottom() + flexItem.getMarginTop() + flexItem.getMarginBottom() + padding, flexItem.getHeight()); int childHeight = View.MeasureSpec.getSize(childHeightMeasureSpec); if (childHeight > flexItem.getMaxHeight()) { childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec( flexItem.getMaxHeight(), View.MeasureSpec.getMode(childHeightMeasureSpec)); } else if (childHeight < flexItem.getMinHeight()) { childHeightMeasureSpec = View.MeasureSpec.makeMeasureSpec( flexItem.getMinHeight(), View.MeasureSpec.getMode(childHeightMeasureSpec)); } return childHeightMeasureSpec; } /** * Determines the cross size (Calculate the length along the cross axis). Expand the cross size * only if the height mode is MeasureSpec.EXACTLY, otherwise use the sum of cross sizes of all * flex lines. * * @param heightMeasureSpec vertical space requirements as imposed by the parent * @param paddingAlongCrossAxis the padding value for the FlexboxLayout along the cross axis */ void determineCrossSize(int heightMeasureSpec, int paddingAlongCrossAxis) { // The MeasureSpec mode along the cross axis int mode = View.MeasureSpec.getMode(heightMeasureSpec); // The MeasureSpec size along the cross axis int size = View.MeasureSpec.getSize(heightMeasureSpec); List flexLines = flexContainer.getFlexLinesInternal(); if (mode == View.MeasureSpec.EXACTLY) { int totalCrossSize = flexContainer.getSumOfCrossSize() + paddingAlongCrossAxis; if (flexLines.size() == 1) { flexLines.get(0).crossSize = size - paddingAlongCrossAxis; // alignContent property is valid only if the Flexbox has at least two lines } else if (flexLines.size() >= 2) { if (totalCrossSize < size) { float freeSpaceUnit = (size - totalCrossSize) / (float) flexLines.size(); float accumulatedError = 0; for (int i = 0, flexLinesSize = flexLines.size(); i < flexLinesSize; i++) { FlexLine flexLine = flexLines.get(i); float newCrossSizeAsFloat = flexLine.crossSize + freeSpaceUnit; if (i == flexLines.size() - 1) { newCrossSizeAsFloat += accumulatedError; accumulatedError = 0; } int 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; } } } } } void stretchViews() { if (0 >= flexContainer.getFlexItemCount()) { return; } int flexLineIndex = 0; List flexLines = flexContainer.getFlexLinesInternal(); for (int i = flexLineIndex, size = flexLines.size(); i < size; i++) { FlexLine flexLine = flexLines.get(i); for (int j = 0, itemCount = flexLine.itemCount; j < itemCount; j++) { int viewIndex = flexLine.firstIndex + j; if (j >= flexContainer.getFlexItemCount()) { continue; } View view = flexContainer.getReorderedFlexItemAt(viewIndex); if (view == null || view.getVisibility() == View.GONE) { continue; } stretchViewVertically(view, flexLine.crossSize, viewIndex); } } } /** * Expand the view vertically to the size of the crossSize (considering the view margins) * * @param view the View to be stretched * @param crossSize the cross size * @param index the index of the view */ private void stretchViewVertically(View view, int crossSize, int index) { FlexItem flexItem = (FlexItem) view.getLayoutParams(); int newHeight = crossSize - flexItem.getMarginTop() - flexItem.getMarginBottom() - flexContainer.getDecorationLengthCrossAxis(view); newHeight = Math.max(newHeight, flexItem.getMinHeight()); newHeight = Math.min(newHeight, flexItem.getMaxHeight()); int measuredWidth = view.getMeasuredWidth(); int childWidthSpec = View.MeasureSpec.makeMeasureSpec(measuredWidth, View.MeasureSpec.EXACTLY); int childHeightSpec = View.MeasureSpec.makeMeasureSpec(newHeight, View.MeasureSpec.EXACTLY); view.measure(childWidthSpec, childHeightSpec); flexContainer.updateViewCache(index, view); } /** * Place a single View * * @param view the View to be placed * @param left the left position of the View, which the View's margin is already taken into * account * @param top the top position of the flex line where the View belongs to. The actual View's top * position is shifted depending on the flexWrap and alignItems attributes * @param right the right position of the View, which the View's margin is already taken into * account * @param bottom the bottom position of the flex line where the View belongs to. The actual View's * bottom position is shifted depending on the flexWrap and alignItems attributes */ void layoutSingleChildHorizontal(View view, int left, int top, int right, int bottom) { FlexItem flexItem = (FlexItem) view.getLayoutParams(); view.layout(left, top + flexItem.getMarginTop(), right, bottom + flexItem.getMarginTop()); } /** * A class that is used for calculating the view order which view's indices and order properties * from Flexbox are taken into account. */ private static class Order implements Comparable { /** {@link View}'s index */ int index; /** order property in the Flexbox */ int order; @Override public int compareTo(@NonNull Order another) { if (order != another.order) { return order - another.order; } return index - another.index; } @Override public String toString() { return "Order{" + "order=" + order + ", index=" + index + '}'; } } static class FlexLinesResult { List flexLines; int childState; void reset() { flexLines = null; childState = 0; } } }