diff --git a/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java b/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java new file mode 100644 index 000000000..b55415b21 --- /dev/null +++ b/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * 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 + * + * http://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 org.nativescript.widget; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds properties related to a single flex line. This class is not expected to be changed outside + * of the {@link FlexboxLayout}, thus only exposing the getter methods that may be useful for + * other classes using the {@link FlexboxLayout}. + */ +public class FlexLine { + + FlexLine() { + } + + /** @see {@link #getLeft()} */ + int mLeft = Integer.MAX_VALUE; + + /** @see {@link #getTop()} */ + int mTop = Integer.MAX_VALUE; + + /** @see {@link #getRight()} */ + int mRight = Integer.MIN_VALUE; + + /** @see {@link #getBottom()} */ + int mBottom = Integer.MIN_VALUE; + + /** @see {@link #getMainSize()} */ + int mMainSize; + + /** + * The sum of the lengths of dividers along the main axis. This value should be lower or + * than than the value of {@link #mMainSize}. + */ + int mDividerLengthInMainSize; + + /** @see {@link #getCrossSize()} */ + int mCrossSize; + + /** @see {@link #getItemCount()} */ + int mItemCount; + + /** @see {@link #getTotalFlexGrow()} */ + float mTotalFlexGrow; + + /** @see {@link #getTotalFlexShrink()} */ + float mTotalFlexShrink; + + /** + * The largest value of the individual child's baseline (obtained by View#getBaseline() + * if the {@link FlexboxLayout#mAlignItems} value is not {@link FlexboxLayout#ALIGN_ITEMS_BASELINE} + * or the flex direction is vertical, this value is not used. + * If the alignment direction is from the bottom to top, + * (e.g. flexWrap == FLEX_WRAP_WRAP_REVERSE and flexDirection == FLEX_DIRECTION_ROW) + * store this value from the distance from the bottom of the view minus baseline. + * (Calculated as view.getMeasuredHeight() - view.getBaseline - LayoutParams.bottomMargin) + */ + int mMaxBaseline; + + /** + * Store the indices of the children views whose alignSelf property is stretch. + * The stored indices are the absolute indices including all children in the Flexbox, + * not the relative indices in this flex line. + */ + List mIndicesAlignSelfStretch = new ArrayList<>(); + + /** + * @return the distance in pixels from the top edge of this view's parent + * to the top edge of this FlexLine. + */ + public int getLeft() { + return mLeft; + } + + /** + * @return the distance in pixels from the top edge of this view's parent + * to the top edge of this FlexLine. + */ + public int getTop() { + return mTop; + } + + /** + * @return the distance in pixels from the right edge of this view's parent + * to the right edge of this FlexLine. + */ + public int getRight() { + return mRight; + } + + /** + * @return the distance in pixels from the bottom edge of this view's parent + * to the bottom edge of this FlexLine. + */ + public int getBottom() { + return mBottom; + } + + /** + * @return the size of the flex line in pixels along the main axis of the flex container. + */ + public int getMainSize() { + return mMainSize; + } + + /** + * @return the size of the flex line in pixels along the cross axis of the flex container. + */ + public int getCrossSize() { + return mCrossSize; + } + + /** + * @return the count of the views contained in this flex line. + */ + public int getItemCount() { + return mItemCount; + } + + /** + * @return the sum of the flexGrow properties of the children included in this flex line + */ + public float getTotalFlexGrow() { + return mTotalFlexGrow; + } + + /** + * @return the sum of the flexShrink properties of the children included in this flex line + */ + public float getTotalFlexShrink() { + return mTotalFlexShrink; + } +} diff --git a/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java b/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java new file mode 100644 index 000000000..44049e5c8 --- /dev/null +++ b/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java @@ -0,0 +1,2747 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * 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 + * + * http://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 org.nativescript.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.SparseIntArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * A layout that arranges its children in a way its attributes can be specified like the + * CSS Flexible Box Layout Module. + * This class extends the {@link ViewGroup} like other layout classes such as {@link LinearLayout} + * or {@link RelativeLayout}, the attributes can be specified from a layout XML or from code. + * + * The supported attributes that you can use are: + * + * for the FlexboxLayout. + * + * And for the children of the FlexboxLayout, you can use: + * + */ +public class FlexboxLayout extends ViewGroup { + + @IntDef({FLEX_DIRECTION_ROW, FLEX_DIRECTION_ROW_REVERSE, FLEX_DIRECTION_COLUMN, + FLEX_DIRECTION_COLUMN_REVERSE}) + @Retention(RetentionPolicy.SOURCE) + public @interface FlexDirection { + + } + + public static final int FLEX_DIRECTION_ROW = 0; + + public static final int FLEX_DIRECTION_ROW_REVERSE = 1; + + public static final int FLEX_DIRECTION_COLUMN = 2; + + public static final int FLEX_DIRECTION_COLUMN_REVERSE = 3; + + /** + * The direction children items are placed inside the Flexbox layout, it determines the + * direction of the main axis (and the cross axis, perpendicular to the main axis). + * + * The default value is {@link #FLEX_DIRECTION_ROW}. + */ + private int mFlexDirection; + + + @IntDef({FLEX_WRAP_NOWRAP, FLEX_WRAP_WRAP, FLEX_WRAP_WRAP_REVERSE}) + @Retention(RetentionPolicy.SOURCE) + public @interface FlexWrap { + + } + + public static final int FLEX_WRAP_NOWRAP = 0; + + public static final int FLEX_WRAP_WRAP = 1; + + public static final int FLEX_WRAP_WRAP_REVERSE = 2; + + /** + * This attribute controls whether the flex container is single-line or multi-line, and the + * direction of the cross axis. + * + * The default value is {@link #FLEX_WRAP_NOWRAP}. + */ + private int mFlexWrap; + + + @IntDef({JUSTIFY_CONTENT_FLEX_START, JUSTIFY_CONTENT_FLEX_END, JUSTIFY_CONTENT_CENTER, + JUSTIFY_CONTENT_SPACE_BETWEEN, JUSTIFY_CONTENT_SPACE_AROUND}) + @Retention(RetentionPolicy.SOURCE) + public @interface JustifyContent { + + } + + public static final int JUSTIFY_CONTENT_FLEX_START = 0; + + public static final int JUSTIFY_CONTENT_FLEX_END = 1; + + public static final int JUSTIFY_CONTENT_CENTER = 2; + + public static final int JUSTIFY_CONTENT_SPACE_BETWEEN = 3; + + public static final int JUSTIFY_CONTENT_SPACE_AROUND = 4; + + /** + * This attribute controls the alignment along the main axis. + * The default value is {@link #JUSTIFY_CONTENT_FLEX_START}. + */ + private int mJustifyContent; + + + @IntDef({ALIGN_ITEMS_FLEX_START, ALIGN_ITEMS_FLEX_END, ALIGN_ITEMS_CENTER, + ALIGN_ITEMS_BASELINE, ALIGN_ITEMS_STRETCH}) + @Retention(RetentionPolicy.SOURCE) + public @interface AlignItems { + + } + + public static final int ALIGN_ITEMS_FLEX_START = 0; + + public static final int ALIGN_ITEMS_FLEX_END = 1; + + public static final int ALIGN_ITEMS_CENTER = 2; + + public static final int ALIGN_ITEMS_BASELINE = 3; + + public static final int ALIGN_ITEMS_STRETCH = 4; + + /** + * This attribute controls the alignment along the cross axis. + * The default value is {@link #ALIGN_ITEMS_STRETCH}. + */ + private int mAlignItems; + + + @IntDef({ALIGN_CONTENT_FLEX_START, ALIGN_CONTENT_FLEX_END, ALIGN_CONTENT_CENTER, + ALIGN_CONTENT_SPACE_BETWEEN, ALIGN_CONTENT_SPACE_AROUND, ALIGN_CONTENT_STRETCH}) + @Retention(RetentionPolicy.SOURCE) + public @interface AlignContent { + + } + + public static final int ALIGN_CONTENT_FLEX_START = 0; + + public static final int ALIGN_CONTENT_FLEX_END = 1; + + public static final int ALIGN_CONTENT_CENTER = 2; + + public static final int ALIGN_CONTENT_SPACE_BETWEEN = 3; + + public static final int ALIGN_CONTENT_SPACE_AROUND = 4; + + public static final int ALIGN_CONTENT_STRETCH = 5; + + /** + * This attribute controls the alignment of the flex lines in the flex container. + * The default value is {@link #ALIGN_CONTENT_STRETCH}. + */ + private int mAlignContent; + + /** + * The int definition to be used as the arguments for the {@link #setShowDivider(int)}, + * {@link #setShowDividerHorizontal(int)} or {@link #setShowDividerVertical(int)}. + * One or more of the values (such as + * {@link #SHOW_DIVIDER_BEGINNING} | {@link #SHOW_DIVIDER_MIDDLE}) can be passed to those set + * methods. + */ + @IntDef(flag = true, + value = { + SHOW_DIVIDER_NONE, + SHOW_DIVIDER_BEGINNING, + SHOW_DIVIDER_MIDDLE, + SHOW_DIVIDER_END + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DividerMode { + + } + + /** Constant to how no dividers */ + public static final int SHOW_DIVIDER_NONE = 0; + + /** Constant to show a divider at the beginning of the flex lines (or flex items). */ + public static final int SHOW_DIVIDER_BEGINNING = 1; + + /** Constant to show dividers between flex lines or flex items. */ + public static final int SHOW_DIVIDER_MIDDLE = 1 << 1; + + /** Constant to show a divider at the end of the flex lines or flex items. */ + public static final int SHOW_DIVIDER_END = 1 << 2; + + /** The drawable to be drawn for the horizontal dividers. */ + private Drawable mDividerDrawableHorizontal; + + /** The drawable to be drawn for the vertical dividers. */ + private Drawable mDividerDrawableVertical; + + /** + * Indicates the divider mode for the {@link #mDividerDrawableHorizontal}. The value needs to + * be the combination of the value of {@link #SHOW_DIVIDER_NONE}, + * {@link #SHOW_DIVIDER_BEGINNING}, {@link #SHOW_DIVIDER_MIDDLE} and {@link #SHOW_DIVIDER_END} + */ + private int mShowDividerHorizontal; + + /** + * Indicates the divider mode for the {@link #mDividerDrawableVertical}. The value needs to + * be the combination of the value of {@link #SHOW_DIVIDER_NONE}, + * {@link #SHOW_DIVIDER_BEGINNING}, {@link #SHOW_DIVIDER_MIDDLE} and {@link #SHOW_DIVIDER_END} + */ + private int mShowDividerVertical; + + /** The height of the {@link #mDividerDrawableHorizontal}. */ + private int mDividerHorizontalHeight; + + /** The width of the {@link #mDividerDrawableVertical}. */ + private int mDividerVerticalWidth; + + /** + * Holds reordered indices, which {@link LayoutParams#order} parameters are taken into account + */ + private int[] mReorderedIndices; + + /** + * Caches the {@link LayoutParams#order} attributes for children views. + * Key: the index of the view ({@link #mReorderedIndices} isn't taken into account) + * Value: the value for the order attribute + */ + private SparseIntArray mOrderCache; + + private List mFlexLines = new ArrayList<>(); + + /** + * Holds the 'frozen' state of children during measure. If a view is frozen it will no longer + * expand or shrink regardless of flexGrow/flexShrink. Items are indexed by the child's + * reordered index. + */ + private boolean[] mChildrenFrozen; + + public FlexboxLayout(Context context) { + this(context, null); + } + + public FlexboxLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FlexboxLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + // NOTE: We do not support android xml. + // TypedArray a = context.obtainStyledAttributes( + // attrs, R.styleable.FlexboxLayout, defStyleAttr, 0); + // mFlexDirection = a.getInt(R.styleable.FlexboxLayout_flexDirection, FLEX_DIRECTION_ROW); + // mFlexWrap = a.getInt(R.styleable.FlexboxLayout_flexWrap, FLEX_WRAP_NOWRAP); + // mJustifyContent = a + // .getInt(R.styleable.FlexboxLayout_justifyContent, JUSTIFY_CONTENT_FLEX_START); + // mAlignItems = a.getInt(R.styleable.FlexboxLayout_alignItems, ALIGN_ITEMS_STRETCH); + // mAlignContent = a.getInt(R.styleable.FlexboxLayout_alignContent, ALIGN_CONTENT_STRETCH); + // Drawable drawable = a.getDrawable(R.styleable.FlexboxLayout_dividerDrawable); + // if (drawable != null) { + // setDividerDrawableHorizontal(drawable); + // setDividerDrawableVertical(drawable); + // } + // Drawable drawableHorizontal = a + // .getDrawable(R.styleable.FlexboxLayout_dividerDrawableHorizontal); + // if (drawableHorizontal != null) { + // setDividerDrawableHorizontal(drawableHorizontal); + // } + // Drawable drawableVertical = a + // .getDrawable(R.styleable.FlexboxLayout_dividerDrawableVertical); + // if (drawableVertical != null) { + // setDividerDrawableVertical(drawableVertical); + // } + // int dividerMode = a.getInt(R.styleable.FlexboxLayout_showDivider, SHOW_DIVIDER_NONE); + // if (dividerMode != SHOW_DIVIDER_NONE) { + // mShowDividerVertical = dividerMode; + // mShowDividerHorizontal = dividerMode; + // } + // int dividerModeVertical = a + // .getInt(R.styleable.FlexboxLayout_showDividerVertical, SHOW_DIVIDER_NONE); + // if (dividerModeVertical != SHOW_DIVIDER_NONE) { + // mShowDividerVertical = dividerModeVertical; + // } + // int dividerModeHorizontal = a + // .getInt(R.styleable.FlexboxLayout_showDividerHorizontal, SHOW_DIVIDER_NONE); + // if (dividerModeHorizontal != SHOW_DIVIDER_NONE) { + // mShowDividerHorizontal = dividerModeHorizontal; + // } + // a.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (isOrderChangedFromLastMeasurement()) { + mReorderedIndices = createReorderedIndices(); + } + if (mChildrenFrozen == null || mChildrenFrozen.length < getChildCount()) { + mChildrenFrozen = new boolean[getChildCount()]; + } + + // TODO: Only calculate the children views which are affected from the last measure. + + switch (mFlexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + measureHorizontal(widthMeasureSpec, heightMeasureSpec); + break; + case FLEX_DIRECTION_COLUMN: // Intentional fall through + case FLEX_DIRECTION_COLUMN_REVERSE: + measureVertical(widthMeasureSpec, heightMeasureSpec); + break; + default: + throw new IllegalStateException( + "Invalid value for the flex direction is set: " + mFlexDirection); + } + + Arrays.fill(mChildrenFrozen, false); + } + + /** + * Returns a View, which is reordered by taking {@link LayoutParams#order} parameters + * into account. + * + * @param index the index of the view + * @return the reordered view, which {@link LayoutParams@order} is taken into account. + * If the index is negative or out of bounds of the number of contained views, + * returns {@code null}. + */ + public View getReorderedChildAt(int index) { + if (index < 0 || index >= mReorderedIndices.length) { + return null; + } + return getChildAt(mReorderedIndices[index]); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + // Create an array for the reordered indices before the View is added in the parent + // ViewGroup since otherwise reordered indices won't be in effect before the + // FlexboxLayout's onMeasure is called. + // Because requestLayout is requested in the super.addView method. + mReorderedIndices = createReorderedIndices(child, index, params); + super.addView(child, index, params); + } + + /** + * Create an array, which indicates the reordered indices that {@link LayoutParams#order} + * 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 + */ + private int[] createReorderedIndices(View viewBeforeAdded, int indexForViewBeforeAdded, + ViewGroup.LayoutParams paramsForViewBeforeAdded) { + int childCount = getChildCount(); + List orders = createOrders(childCount); + Order orderForViewToBeAdded = new Order(); + if (viewBeforeAdded != null + && paramsForViewBeforeAdded instanceof FlexboxLayout.LayoutParams) { + orderForViewToBeAdded.order = ((LayoutParams) paramsForViewBeforeAdded).order; + } else { + orderForViewToBeAdded.order = LayoutParams.ORDER_DEFAULT; + } + + if (indexForViewBeforeAdded == -1 || indexForViewBeforeAdded == childCount) { + orderForViewToBeAdded.index = childCount; + } else if (indexForViewBeforeAdded < getChildCount()) { + 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); + } + + /** + * Create an array, which indicates the reordered indices that {@link LayoutParams#order} + * attributes are taken into account. + * + * @return @return an array which have the reordered indices + */ + private int[] createReorderedIndices() { + int childCount = getChildCount(); + List orders = createOrders(childCount); + return sortOrdersIntoReorderedIndices(childCount, orders); + } + + private int[] sortOrdersIntoReorderedIndices(int childCount, List orders) { + Collections.sort(orders); + if (mOrderCache == null) { + mOrderCache = new SparseIntArray(childCount); + } + mOrderCache.clear(); + int[] reorderedIndices = new int[childCount]; + int i = 0; + for (Order order : orders) { + reorderedIndices[i] = order.index; + mOrderCache.append(i, order.order); + i++; + } + return reorderedIndices; + } + + @NonNull + private List createOrders(int childCount) { + List orders = new ArrayList<>(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + LayoutParams params = (LayoutParams) child.getLayoutParams(); + Order order = new Order(); + order.order = params.order; + order.index = i; + orders.add(order); + } + return orders; + } + + /** + * Returns if any of the children's {@link LayoutParams#order} attributes are changed + * from the last measurement. + * + * @return {@code true} if changed from the last measurement, {@code false} otherwise. + */ + private boolean isOrderChangedFromLastMeasurement() { + int childCount = getChildCount(); + if (mOrderCache == null) { + mOrderCache = new SparseIntArray(childCount); + } + if (mOrderCache.size() != childCount) { + return true; + } + for (int i = 0; i < childCount; i++) { + View view = getChildAt(i); + if (view == null) { + continue; + } + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.order != mOrderCache.get(i)) { + return true; + } + } + return false; + } + + /** + * Sub method for {@link #onMeasure(int, int)}, when the main axis direction is horizontal + * (either left to right or right to left). + * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @see #onMeasure(int, int) + * @see #setFlexDirection(int) + * @see #setFlexWrap(int) + * @see #setAlignItems(int) + * @see #setAlignContent(int) + */ + private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int childState = 0; + + mFlexLines.clear(); + + // Determine how many flex lines are needed in this layout by measuring each child. + // (Expand or shrink the view depending on the flexGrow and flexShrink attributes in a later + // loop) + { + int childCount = getChildCount(); + int paddingStart = ViewCompat.getPaddingStart(this); + int paddingEnd = ViewCompat.getPaddingEnd(this); + int largestHeightInRow = Integer.MIN_VALUE; + FlexLine flexLine = new FlexLine(); + + // The index of the view in a same flex line. + int indexInFlexLine = 0; + flexLine.mMainSize = paddingStart + paddingEnd; + for (int i = 0; i < childCount; i++) { + View child = getReorderedChildAt(i); + if (child == null) { + addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } else if (child.getVisibility() == View.GONE) { + flexLine.mItemCount++; + addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } + + FlexboxLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.alignSelf == LayoutParams.ALIGN_SELF_STRETCH) { + flexLine.mIndicesAlignSelfStretch.add(i); + } + + int childWidth = lp.width; + if (lp.flexBasisPercent != LayoutParams.FLEX_BASIS_PERCENT_DEFAULT + && widthMode == MeasureSpec.EXACTLY) { + childWidth = Math.round(widthSize * lp.flexBasisPercent); + // Use the dimension from the layout_width attribute if the widthMode is not + // MeasureSpec.EXACTLY even if any fraction value is set to + // layout_flexBasisPercent. + // There are likely quite few use cases where assigning any fraction values + // with widthMode is not MeasureSpec.EXACTLY (e.g. FlexboxLayout's layout_width + // is set to wrap_content) + } + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + + lp.rightMargin, childWidth); + int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTop() + getPaddingBottom() + lp.topMargin + + lp.bottomMargin, lp.height); + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + // 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 LayoutParams#minWidth}, {@link LayoutParams#minHeight}, + // {@link LayoutParams#maxWidth} and {@link LayoutParams#maxHeight} 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); + + childState = ViewCompat + .combineMeasuredStates(childState, ViewCompat.getMeasuredState(child)); + largestHeightInRow = Math.max(largestHeightInRow, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + + if (isWrapRequired(widthMode, widthSize, flexLine.mMainSize, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin, lp, + i, indexInFlexLine)) { + if (flexLine.mItemCount > 0) { + addFlexLine(flexLine); + } + + flexLine = new FlexLine(); + flexLine.mItemCount = 1; + flexLine.mMainSize = paddingStart + paddingEnd; + largestHeightInRow = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + indexInFlexLine = 0; + } else { + flexLine.mItemCount++; + indexInFlexLine++; + } + flexLine.mMainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + flexLine.mTotalFlexGrow += lp.flexGrow; + flexLine.mTotalFlexShrink += lp.flexShrink; + // Temporarily set the cross axis length as the largest child in the row + // Expand along the cross axis depending on the mAlignContent property if needed + // later + flexLine.mCrossSize = Math.max(flexLine.mCrossSize, largestHeightInRow); + + // Check if the beginning or middle divider is required for the flex item + if (hasDividerBeforeChildAtAlongMainAxis(i, indexInFlexLine)) { + flexLine.mMainSize += mDividerVerticalWidth; + flexLine.mDividerLengthInMainSize += mDividerVerticalWidth; + } + + if (mFlexWrap != FLEX_WRAP_WRAP_REVERSE) { + flexLine.mMaxBaseline = Math + .max(flexLine.mMaxBaseline, child.getBaseline() + lp.topMargin); + } else { + // if the flex wrap property is FLEX_WRAP_WRAP_REVERSE, calculate the + // baseline as the distance from the cross end and the baseline + // since the cross size calculation is based on the distance from the cross end + flexLine.mMaxBaseline = Math + .max(flexLine.mMaxBaseline, + child.getMeasuredHeight() - child.getBaseline() + + lp.bottomMargin); + } + addFlexLineIfLastFlexItem(i, childCount, flexLine); + } + } + + determineMainSize(mFlexDirection, widthMeasureSpec, heightMeasureSpec); + + // TODO: Consider the case any individual child's alignSelf is set to ALIGN_SELF_BASELINE + if (mAlignItems == ALIGN_ITEMS_BASELINE) { + int viewIndex = 0; + for (FlexLine flexLine : mFlexLines) { + // The largest height value that also take the baseline shift into account + int largestHeightInLine = Integer.MIN_VALUE; + for (int i = viewIndex; i < viewIndex + flexLine.mItemCount; i++) { + View child = getReorderedChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (mFlexWrap != FLEX_WRAP_WRAP_REVERSE) { + int marginTop = flexLine.mMaxBaseline - child.getBaseline(); + marginTop = Math.max(marginTop, lp.topMargin); + largestHeightInLine = Math.max(largestHeightInLine, + child.getHeight() + marginTop + lp.bottomMargin); + } else { + int marginBottom = flexLine.mMaxBaseline - child.getMeasuredHeight() + + child.getBaseline(); + marginBottom = Math.max(marginBottom, lp.bottomMargin); + largestHeightInLine = Math.max(largestHeightInLine, + child.getHeight() + lp.topMargin + marginBottom); + } + } + flexLine.mCrossSize = largestHeightInLine; + viewIndex += flexLine.mItemCount; + } + } + + determineCrossSize(mFlexDirection, widthMeasureSpec, heightMeasureSpec, + getPaddingTop() + getPaddingBottom()); + // Now cross size for each flex line is determined. + // Expand the views if alignItems (or alignSelf in each child view) is set to stretch + stretchViews(mFlexDirection, mAlignItems); + setMeasuredDimensionForFlex(mFlexDirection, widthMeasureSpec, heightMeasureSpec, + childState); + } + + /** + * Sub method for {@link #onMeasure(int, int)} when the main axis direction is vertical + * (either from top to bottom or bottom to top). + * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @see #onMeasure(int, int) + * @see #setFlexDirection(int) + * @see #setFlexWrap(int) + * @see #setAlignItems(int) + * @see #setAlignContent(int) + */ + private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int childState = 0; + + mFlexLines.clear(); + + // Determine how many flex lines are needed in this layout by measuring each child. + // (Expand or shrink the view depending on the flexGrow and flexShrink attributes in a later + // loop) + int childCount = getChildCount(); + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + int largestWidthInColumn = Integer.MIN_VALUE; + FlexLine flexLine = new FlexLine(); + flexLine.mMainSize = paddingTop + paddingBottom; + // The index of the view in a same flex line. + int indexInFlexLine = 0; + for (int i = 0; i < childCount; i++) { + View child = getReorderedChildAt(i); + if (child == null) { + addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } else if (child.getVisibility() == View.GONE) { + flexLine.mItemCount++; + addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } + + FlexboxLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.alignSelf == LayoutParams.ALIGN_SELF_STRETCH) { + flexLine.mIndicesAlignSelfStretch.add(i); + } + + int childHeight = lp.height; + if (lp.flexBasisPercent != LayoutParams.FLEX_BASIS_PERCENT_DEFAULT + && heightMode == MeasureSpec.EXACTLY) { + childHeight = Math.round(heightSize * lp.flexBasisPercent); + // Use the dimension from the layout_height attribute if the heightMode is not + // MeasureSpec.EXACTLY even if any fraction value is set to layout_flexBasisPercent. + // There are likely quite few use cases where assigning any fraction values + // with heightMode is not MeasureSpec.EXACTLY (e.g. FlexboxLayout's layout_height + // is set to wrap_content) + } + + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + + lp.rightMargin, lp.width); + int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTop() + getPaddingBottom() + lp.topMargin + + lp.bottomMargin, childHeight); + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + // 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 LayoutParams#minWidth}, {@link LayoutParams#minHeight}, + // {@link LayoutParams#maxWidth} and {@link LayoutParams#maxHeight} attributes. + // E.g. When the child's layout_height is wrap_content the measured height may be + // less than the min height after the first measurement. + checkSizeConstraints(child); + + childState = ViewCompat + .combineMeasuredStates(childState, ViewCompat.getMeasuredState(child)); + largestWidthInColumn = Math.max(largestWidthInColumn, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); + + if (isWrapRequired(heightMode, heightSize, flexLine.mMainSize, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin, lp, + i, indexInFlexLine)) { + if (flexLine.mItemCount > 0) { + addFlexLine(flexLine); + } + + flexLine = new FlexLine(); + flexLine.mItemCount = 1; + flexLine.mMainSize = paddingTop + paddingBottom; + largestWidthInColumn = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + indexInFlexLine = 0; + } else { + flexLine.mItemCount++; + indexInFlexLine++; + } + flexLine.mMainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + flexLine.mTotalFlexGrow += lp.flexGrow; + flexLine.mTotalFlexShrink += lp.flexShrink; + // Temporarily set the cross axis length as the largest child width in the column + // Expand along the cross axis depending on the mAlignContent property if needed + // later + flexLine.mCrossSize = Math.max(flexLine.mCrossSize, largestWidthInColumn); + + if (hasDividerBeforeChildAtAlongMainAxis(i, indexInFlexLine)) { + flexLine.mMainSize += mDividerHorizontalHeight; + } + addFlexLineIfLastFlexItem(i, childCount, flexLine); + } + + determineMainSize(mFlexDirection, widthMeasureSpec, heightMeasureSpec); + determineCrossSize(mFlexDirection, widthMeasureSpec, heightMeasureSpec, + getPaddingLeft() + getPaddingRight()); + // Now cross size for each flex line is determined. + // Expand the views if alignItems (or alignSelf in each child view) is set to stretch + stretchViews(mFlexDirection, mAlignItems); + setMeasuredDimensionForFlex(mFlexDirection, widthMeasureSpec, heightMeasureSpec, + childState); + } + + /** + * Checks if the view's width/height don't violate the minimum/maximum size constraints imposed + * by the {@link LayoutParams#minWidth}, {@link LayoutParams#minHeight}, + * {@link LayoutParams#maxWidth} and {@link LayoutParams#maxHeight} attributes. + * + * @param view the view to be checked + */ + private void checkSizeConstraints(View view) { + boolean needsMeasure = false; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int childWidth = view.getMeasuredWidth(); + int 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(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); + } + } + + private void addFlexLineIfLastFlexItem(int childIndex, int childCount, FlexLine flexLine) { + if (childIndex == childCount - 1 && flexLine.mItemCount != 0) { + // Add the flex line if this item is the last item + addFlexLine(flexLine); + } + } + + private void addFlexLine(FlexLine flexLine) { + // The size of the end divider isn't added until the flexLine is added to the flex container + // take the divider width (or height) into account when adding the flex line. + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + if ((mShowDividerVertical & SHOW_DIVIDER_END) > 0) { + flexLine.mMainSize += mDividerVerticalWidth; + flexLine.mDividerLengthInMainSize += mDividerVerticalWidth; + } + } else { + if ((mShowDividerHorizontal & SHOW_DIVIDER_END) > 0) { + flexLine.mMainSize += mDividerHorizontalHeight; + flexLine.mDividerLengthInMainSize += mDividerHorizontalHeight; + } + } + mFlexLines.add(flexLine); + } + + /** + * 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 flexGrow (or flexShrink if remaining + * space is negative) properties are set to non-zero. + * + * @param flexDirection the value of the flex direction + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @see #setFlexDirection(int) + * @see #getFlexDirection() + */ + private void determineMainSize(@FlexDirection int flexDirection, int widthMeasureSpec, + int heightMeasureSpec) { + int mainSize; + int paddingAlongMainAxis; + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + if (widthMode == MeasureSpec.EXACTLY) { + mainSize = widthSize; + } else { + mainSize = getLargestMainSize(); + } + paddingAlongMainAxis = getPaddingLeft() + getPaddingRight(); + break; + case FLEX_DIRECTION_COLUMN: // Intentional fall through + case FLEX_DIRECTION_COLUMN_REVERSE: + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + if (heightMode == MeasureSpec.EXACTLY) { + mainSize = heightSize; + } else { + mainSize = getLargestMainSize(); + } + paddingAlongMainAxis = getPaddingTop() + getPaddingBottom(); + break; + default: + throw new IllegalArgumentException("Invalid flex direction: " + flexDirection); + } + + int childIndex = 0; + for (FlexLine flexLine : mFlexLines) { + if (flexLine.mMainSize < mainSize) { + childIndex = expandFlexItems(flexLine, flexDirection, mainSize, + paddingAlongMainAxis, childIndex); + } else { + childIndex = shrinkFlexItems(flexLine, flexDirection, mainSize, + paddingAlongMainAxis, childIndex); + } + } + } + + /** + * Expand the flex items along the main axis based on the individual flexGrow attribute. + * + * @param flexLine the flex line to which flex items belong + * @param flexDirection the flexDirection value for this FlexboxLayout + * @param maxMainSize the maximum main size. Expanded main size will be this size + * @param paddingAlongMainAxis the padding value along the main axis + * @param startIndex the start index of the children views to be expanded. This index + * needs to + * be an absolute index in the flex container (FlexboxLayout), + * not the relative index in the flex line. + * @return the next index, the next flex line's first flex item starts from the returned index + * @see #getFlexDirection() + * @see #setFlexDirection(int) + * @see LayoutParams#flexGrow + */ + private int expandFlexItems(FlexLine flexLine, @FlexDirection int flexDirection, + int maxMainSize, int paddingAlongMainAxis, int startIndex) { + int childIndex = startIndex; + if (flexLine.mTotalFlexGrow <= 0 || maxMainSize < flexLine.mMainSize) { + childIndex += flexLine.mItemCount; + return childIndex; + } + int sizeBeforeExpand = flexLine.mMainSize; + boolean needsReexpand = false; + float unitSpace = (maxMainSize - flexLine.mMainSize) / flexLine.mTotalFlexGrow; + flexLine.mMainSize = paddingAlongMainAxis + flexLine.mDividerLengthInMainSize; + float accumulatedRoundError = 0; + for (int i = 0; i < flexLine.mItemCount; i++) { + View child = getReorderedChildAt(childIndex); + if (child == null) { + continue; + } else if (child.getVisibility() == View.GONE) { + childIndex++; + continue; + } + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (isMainAxisDirectionHorizontal(flexDirection)) { + // The direction of the main axis is horizontal + if (!mChildrenFrozen[childIndex]) { + float rawCalculatedWidth = child.getMeasuredWidth() + unitSpace * lp.flexGrow; + if (i == flexLine.mItemCount - 1) { + rawCalculatedWidth += accumulatedRoundError; + accumulatedRoundError = 0; + } + int newWidth = Math.round(rawCalculatedWidth); + if (newWidth > lp.maxWidth) { + // This means the child can't expand beyond the value of the maxWidth 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 + // startIndex. + needsReexpand = true; + newWidth = lp.maxWidth; + mChildrenFrozen[childIndex] = true; + flexLine.mTotalFlexGrow -= 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(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), + MeasureSpec + .makeMeasureSpec(child.getMeasuredHeight(), + MeasureSpec.EXACTLY)); + } + flexLine.mMainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } else { + // The direction of the main axis is vertical + if (!mChildrenFrozen[childIndex]) { + float rawCalculatedHeight = child.getMeasuredHeight() + unitSpace * lp.flexGrow; + if (i == flexLine.mItemCount - 1) { + rawCalculatedHeight += accumulatedRoundError; + accumulatedRoundError = 0; + } + int newHeight = Math.round(rawCalculatedHeight); + if (newHeight > lp.maxHeight) { + // This means the child can't expand beyond the value of the maxHeight + // 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 + // startIndex. + needsReexpand = true; + newHeight = lp.maxHeight; + mChildrenFrozen[childIndex] = true; + flexLine.mTotalFlexGrow -= 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(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); + } + flexLine.mMainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + childIndex++; + } + + if (needsReexpand && sizeBeforeExpand != flexLine.mMainSize) { + // Re-invoke the method with the same startIndex to distribute the positive free space + // that wasn't fully distributed (because of maximum length constraint) + expandFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex); + } + return childIndex; + } + + /** + * Shrink the flex items along the main axis based on the individual flexShrink attribute. + * + * @param flexLine the flex line to which flex items belong + * @param flexDirection the flexDirection value for this FlexboxLayout + * @param maxMainSize the maximum main size. Shrank main size will be this size + * @param paddingAlongMainAxis the padding value along the main axis + * @param startIndex the start index of the children views to be shrank. This index + * needs to + * be an absolute index in the flex container (FlexboxLayout), + * not the relative index in the flex line. + * @return the next index, the next flex line's first flex item starts from the returned index + * @see #getFlexDirection() + * @see #setFlexDirection(int) + * @see LayoutParams#flexShrink + */ + private int shrinkFlexItems(FlexLine flexLine, @FlexDirection int flexDirection, + int maxMainSize, int paddingAlongMainAxis, int startIndex) { + int childIndex = startIndex; + int sizeBeforeShrink = flexLine.mMainSize; + if (flexLine.mTotalFlexShrink <= 0 || maxMainSize > flexLine.mMainSize) { + childIndex += flexLine.mItemCount; + return childIndex; + } + boolean needsReshrink = false; + float unitShrink = (flexLine.mMainSize - maxMainSize) / flexLine.mTotalFlexShrink; + float accumulatedRoundError = 0; + flexLine.mMainSize = paddingAlongMainAxis + flexLine.mDividerLengthInMainSize; + for (int i = 0; i < flexLine.mItemCount; i++) { + View child = getReorderedChildAt(childIndex); + if (child == null) { + continue; + } else if (child.getVisibility() == View.GONE) { + childIndex++; + continue; + } + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (isMainAxisDirectionHorizontal(flexDirection)) { + // The direction of main axis is horizontal + if (!mChildrenFrozen[childIndex]) { + float rawCalculatedWidth = child.getMeasuredWidth() + - unitShrink * lp.flexShrink; + if (i == flexLine.mItemCount - 1) { + rawCalculatedWidth += accumulatedRoundError; + accumulatedRoundError = 0; + } + int newWidth = Math.round(rawCalculatedWidth); + if (newWidth < lp.minWidth) { + // 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 + // startIndex. + needsReshrink = true; + newWidth = lp.minWidth; + mChildrenFrozen[childIndex] = true; + flexLine.mTotalFlexShrink -= 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(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), + MeasureSpec.EXACTLY)); + } + flexLine.mMainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } else { + // The direction of main axis is vertical + if (!mChildrenFrozen[childIndex]) { + float rawCalculatedHeight = child.getMeasuredHeight() + - unitShrink * lp.flexShrink; + if (i == flexLine.mItemCount - 1) { + rawCalculatedHeight += accumulatedRoundError; + accumulatedRoundError = 0; + } + int newHeight = Math.round(rawCalculatedHeight); + if (newHeight < lp.minHeight) { + // Need to invoke this method again like the case flex direction is vertical + needsReshrink = true; + newHeight = lp.minHeight; + mChildrenFrozen[childIndex] = true; + flexLine.mTotalFlexShrink -= 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(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); + } + flexLine.mMainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + childIndex++; + } + + if (needsReshrink && sizeBeforeShrink != flexLine.mMainSize) { + // Re-invoke the method with the same startIndex to distribute the negative free space + // that wasn't fully distributed (because some views length were not enough) + shrinkFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex); + } + return childIndex; + } + + /** + * 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 flexDirection the flex direction attribute + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @param paddingAlongCrossAxis the padding value for the FlexboxLayout along the cross axis + * @see #getFlexDirection() + * @see #setFlexDirection(int) + * @see #getAlignContent() + * @see #setAlignContent(int) + */ + private void determineCrossSize(int flexDirection, int widthMeasureSpec, + int heightMeasureSpec, int paddingAlongCrossAxis) { + // The MeasureSpec mode along the cross axis + int mode; + // The MeasureSpec size along the cross axis + int size; + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + mode = MeasureSpec.getMode(heightMeasureSpec); + size = MeasureSpec.getSize(heightMeasureSpec); + break; + case FLEX_DIRECTION_COLUMN: // Intentional fall through + case FLEX_DIRECTION_COLUMN_REVERSE: + mode = MeasureSpec.getMode(widthMeasureSpec); + size = MeasureSpec.getSize(widthMeasureSpec); + break; + default: + throw new IllegalArgumentException("Invalid flex direction: " + flexDirection); + } + if (mode == MeasureSpec.EXACTLY) { + int totalCrossSize = getSumOfCrossSize() + paddingAlongCrossAxis; + if (mFlexLines.size() == 1) { + mFlexLines.get(0).mCrossSize = size - paddingAlongCrossAxis; + // alignContent property is valid only if the Flexbox has at least two lines + } else if (mFlexLines.size() >= 2 && totalCrossSize < size) { + switch (mAlignContent) { + case ALIGN_CONTENT_STRETCH: { + float freeSpaceUnit = (size - totalCrossSize) / (float) mFlexLines.size(); + float accumulatedError = 0; + for (int i = 0, flexLinesSize = mFlexLines.size(); i < flexLinesSize; i++) { + FlexLine flexLine = mFlexLines.get(i); + float newCrossSizeAsFloat = flexLine.mCrossSize + freeSpaceUnit; + if (i == mFlexLines.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.mCrossSize = newCrossSize; + } + break; + } + case ALIGN_CONTENT_SPACE_AROUND: { + // The value of free space along the cross axis which needs to be put on top + // and below the bottom of each flex line. + int spaceTopAndBottom = size - totalCrossSize; + // The number of spaces along the cross axis + int numberOfSpaces = mFlexLines.size() * 2; + spaceTopAndBottom = spaceTopAndBottom / numberOfSpaces; + List newFlexLines = new ArrayList<>(); + FlexLine dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine.mCrossSize = spaceTopAndBottom; + for (FlexLine flexLine : mFlexLines) { + newFlexLines.add(dummySpaceFlexLine); + newFlexLines.add(flexLine); + newFlexLines.add(dummySpaceFlexLine); + } + mFlexLines = newFlexLines; + break; + } + case ALIGN_CONTENT_SPACE_BETWEEN: { + // The value of free space along the cross axis between each flex line. + float spaceBetweenFlexLine = size - totalCrossSize; + int numberOfSpaces = mFlexLines.size() - 1; + spaceBetweenFlexLine = spaceBetweenFlexLine / (float) numberOfSpaces; + float accumulatedError = 0; + List newFlexLines = new ArrayList<>(); + for (int i = 0, flexLineSize = mFlexLines.size(); i < flexLineSize; i++) { + FlexLine flexLine = mFlexLines.get(i); + newFlexLines.add(flexLine); + + if (i != mFlexLines.size() - 1) { + FlexLine dummySpaceFlexLine = new FlexLine(); + if (i == mFlexLines.size() - 2) { + // The last dummy space block in the flex container. + // Adjust the cross size by the accumulated error. + dummySpaceFlexLine.mCrossSize = Math + .round(spaceBetweenFlexLine + accumulatedError); + accumulatedError = 0; + } else { + dummySpaceFlexLine.mCrossSize = Math + .round(spaceBetweenFlexLine); + } + accumulatedError += (spaceBetweenFlexLine + - dummySpaceFlexLine.mCrossSize); + if (accumulatedError > 1) { + dummySpaceFlexLine.mCrossSize += 1; + accumulatedError -= 1; + } else if (accumulatedError < -1) { + dummySpaceFlexLine.mCrossSize -= 1; + accumulatedError += 1; + } + newFlexLines.add(dummySpaceFlexLine); + } + } + mFlexLines = newFlexLines; + break; + } + case ALIGN_CONTENT_CENTER: { + int spaceAboveAndBottom = size - totalCrossSize; + spaceAboveAndBottom = spaceAboveAndBottom / 2; + List newFlexLines = new ArrayList<>(); + FlexLine dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine.mCrossSize = spaceAboveAndBottom; + for (int i = 0, flexLineSize = mFlexLines.size(); i < flexLineSize; i++) { + if (i == 0) { + newFlexLines.add(dummySpaceFlexLine); + } + FlexLine flexLine = mFlexLines.get(i); + newFlexLines.add(flexLine); + if (i == mFlexLines.size() - 1) { + newFlexLines.add(dummySpaceFlexLine); + } + } + mFlexLines = newFlexLines; + break; + } + case ALIGN_CONTENT_FLEX_END: { + int spaceTop = size - totalCrossSize; + FlexLine dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine.mCrossSize = spaceTop; + mFlexLines.add(0, dummySpaceFlexLine); + break; + } + } + } + } + } + + /** + * Expand the view if the {@link #mAlignItems} attribute is set to {@link #ALIGN_ITEMS_STRETCH} + * or {@link LayoutParams#ALIGN_SELF_STRETCH} is set to an individual child view. + * + * @param flexDirection the flex direction attribute + * @param alignItems the align items attribute + * @see #getFlexDirection() + * @see #setFlexDirection(int) + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void stretchViews(int flexDirection, int alignItems) { + if (alignItems == ALIGN_ITEMS_STRETCH) { + int viewIndex = 0; + for (FlexLine flexLine : mFlexLines) { + for (int i = 0; i < flexLine.mItemCount; i++, viewIndex++) { + View view = getReorderedChildAt(viewIndex); + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.alignSelf != LayoutParams.ALIGN_SELF_AUTO && + lp.alignSelf != LayoutParams.ALIGN_SELF_STRETCH) { + continue; + } + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + stretchViewVertically(view, flexLine.mCrossSize); + break; + case FLEX_DIRECTION_COLUMN: + case FLEX_DIRECTION_COLUMN_REVERSE: + stretchViewHorizontally(view, flexLine.mCrossSize); + break; + default: + throw new IllegalArgumentException( + "Invalid flex direction: " + flexDirection); + } + } + } + } else { + for (FlexLine flexLine : mFlexLines) { + for (Integer index : flexLine.mIndicesAlignSelfStretch) { + View view = getReorderedChildAt(index); + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + stretchViewVertically(view, flexLine.mCrossSize); + break; + case FLEX_DIRECTION_COLUMN: + case FLEX_DIRECTION_COLUMN_REVERSE: + stretchViewHorizontally(view, flexLine.mCrossSize); + break; + default: + throw new IllegalArgumentException( + "Invalid flex direction: " + flexDirection); + } + } + } + } + } + + /** + * 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 + */ + private void stretchViewVertically(View view, int crossSize) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int newHeight = crossSize - lp.topMargin - lp.bottomMargin; + newHeight = Math.max(newHeight, 0); + view.measure(MeasureSpec + .makeMeasureSpec(view.getMeasuredWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); + } + + /** + * Expand the view horizontally to the size of the crossSize (considering the view margins) + * + * @param view the View to be stretched + * @param crossSize the cross size + */ + private void stretchViewHorizontally(View view, int crossSize) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int newWidth = crossSize - lp.leftMargin - lp.rightMargin; + newWidth = Math.max(newWidth, 0); + view.measure(MeasureSpec + .makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(view.getMeasuredHeight(), MeasureSpec.EXACTLY)); + } + + /** + * Set this FlexboxLayouts' width and height depending on the calculated size of main axis and + * cross axis. + * + * @param flexDirection the value of the flex direction + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @param childState the child state of the View + * @see #getFlexDirection() + * @see #setFlexDirection(int) + */ + private void setMeasuredDimensionForFlex(@FlexDirection int flexDirection, int widthMeasureSpec, + int heightMeasureSpec, int childState) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int calculatedMaxHeight; + int calculatedMaxWidth; + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + calculatedMaxHeight = getSumOfCrossSize() + getPaddingTop() + + getPaddingBottom(); + calculatedMaxWidth = getLargestMainSize(); + break; + case FLEX_DIRECTION_COLUMN: // Intentional fall through + case FLEX_DIRECTION_COLUMN_REVERSE: + calculatedMaxHeight = getLargestMainSize(); + calculatedMaxWidth = getSumOfCrossSize() + getPaddingLeft() + getPaddingRight(); + break; + default: + throw new IllegalArgumentException("Invalid flex direction: " + flexDirection); + } + + int widthSizeAndState; + switch (widthMode) { + case MeasureSpec.EXACTLY: + if (widthSize < calculatedMaxWidth) { + childState = ViewCompat + .combineMeasuredStates(childState, ViewCompat.MEASURED_STATE_TOO_SMALL); + } + widthSizeAndState = ViewCompat.resolveSizeAndState(widthSize, widthMeasureSpec, + childState); + break; + case MeasureSpec.AT_MOST: { + if (widthSize < calculatedMaxWidth) { + childState = ViewCompat + .combineMeasuredStates(childState, ViewCompat.MEASURED_STATE_TOO_SMALL); + } else { + widthSize = calculatedMaxWidth; + } + widthSizeAndState = ViewCompat.resolveSizeAndState(widthSize, widthMeasureSpec, + childState); + break; + } + case MeasureSpec.UNSPECIFIED: { + widthSizeAndState = ViewCompat + .resolveSizeAndState(calculatedMaxWidth, widthMeasureSpec, childState); + break; + } + default: + throw new IllegalStateException("Unknown width mode is set: " + widthMode); + } + int heightSizeAndState; + switch (heightMode) { + case MeasureSpec.EXACTLY: + if (heightSize < calculatedMaxHeight) { + childState = ViewCompat.combineMeasuredStates(childState, + ViewCompat.MEASURED_STATE_TOO_SMALL + >> ViewCompat.MEASURED_HEIGHT_STATE_SHIFT); + } + heightSizeAndState = ViewCompat.resolveSizeAndState(heightSize, heightMeasureSpec, + childState); + break; + case MeasureSpec.AT_MOST: { + if (heightSize < calculatedMaxHeight) { + childState = ViewCompat.combineMeasuredStates(childState, + ViewCompat.MEASURED_STATE_TOO_SMALL + >> ViewCompat.MEASURED_HEIGHT_STATE_SHIFT); + } else { + heightSize = calculatedMaxHeight; + } + heightSizeAndState = ViewCompat.resolveSizeAndState(heightSize, heightMeasureSpec, + childState); + break; + } + case MeasureSpec.UNSPECIFIED: { + heightSizeAndState = ViewCompat.resolveSizeAndState(calculatedMaxHeight, + heightMeasureSpec, childState); + break; + } + default: + throw new IllegalStateException("Unknown height mode is set: " + heightMode); + } + setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + /** + * Determine if a wrap is required (add a new flex line). + * + * @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 lp 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 #getFlexWrap() + * @see #setFlexWrap(int) + */ + private boolean isWrapRequired(int mode, int maxSize, int currentLength, int childLength, + LayoutParams lp, int childAbsoluteIndex, int childRelativeIndexInFlexLine) { + if (mFlexWrap == FLEX_WRAP_NOWRAP) { + return false; + } + if (lp.wrapBefore) { + return true; + } + if (mode == MeasureSpec.UNSPECIFIED) { + return false; + } + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + if (hasDividerBeforeChildAtAlongMainAxis(childAbsoluteIndex, + childRelativeIndexInFlexLine)) { + childLength += mDividerVerticalWidth; + } + if ((mShowDividerVertical & SHOW_DIVIDER_END) > 0) { + childLength += mDividerVerticalWidth; + } + } else { + if (hasDividerBeforeChildAtAlongMainAxis(childAbsoluteIndex, + childRelativeIndexInFlexLine)) { + childLength += mDividerHorizontalHeight; + } + if ((mShowDividerHorizontal & SHOW_DIVIDER_END) > 0) { + childLength += mDividerHorizontalHeight; + } + } + return maxSize < currentLength + childLength; + } + + /** + * Retrieve the largest main size of all flex lines. + * + * @return the largest main size + */ + private int getLargestMainSize() { + int largestSize = Integer.MIN_VALUE; + for (FlexLine flexLine : mFlexLines) { + largestSize = Math.max(largestSize, flexLine.mMainSize); + } + return largestSize; + } + + /** + * Retrieve the sum of the cross sizes of all flex lines including divider lengths. + * + * @return the sum of the cross sizes + */ + private int getSumOfCrossSize() { + int sum = 0; + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + + // Judge if the beginning or middle dividers are required + if (hasDividerBeforeFlexLine(i)) { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + sum += mDividerHorizontalHeight; + } else { + sum += mDividerVerticalWidth; + } + } + + // Judge if the end divider is required + if (hasEndDividerAfterFlexLine(i)) { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + sum += mDividerHorizontalHeight; + } else { + sum += mDividerVerticalWidth; + } + } + sum += flexLine.mCrossSize; + } + return sum; + } + + private boolean isMainAxisDirectionHorizontal(@FlexDirection int flexDirection) { + return flexDirection == FLEX_DIRECTION_ROW + || flexDirection == FLEX_DIRECTION_ROW_REVERSE; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int layoutDirection = ViewCompat.getLayoutDirection(this); + boolean isRtl; + switch (mFlexDirection) { + case FLEX_DIRECTION_ROW: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + layoutHorizontal(isRtl, left, top, right, bottom); + break; + case FLEX_DIRECTION_ROW_REVERSE: + isRtl = layoutDirection != ViewCompat.LAYOUT_DIRECTION_RTL; + layoutHorizontal(isRtl, left, top, right, bottom); + break; + case FLEX_DIRECTION_COLUMN: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + isRtl = !isRtl; + } + layoutVertical(isRtl, false, left, top, right, bottom); + break; + case FLEX_DIRECTION_COLUMN_REVERSE: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + isRtl = !isRtl; + } + layoutVertical(isRtl, true, left, top, right, bottom); + break; + default: + throw new IllegalStateException("Invalid flex direction is set: " + mFlexDirection); + } + } + + /** + * Sub method for {@link #onLayout(boolean, int, int, int, int)} when the + * {@link #mFlexDirection} is either {@link #FLEX_DIRECTION_ROW} or + * {@link #FLEX_DIRECTION_ROW_REVERSE}. + * + * @param isRtl {@code true} if the horizontal layout direction is right to left, {@code + * false} otherwise. + * @param left the left position of this View + * @param top the top position of this View + * @param right the right position of this View + * @param bottom the bottom position of this View + * @see #getFlexWrap() + * @see #setFlexWrap(int) + * @see #getJustifyContent() + * @see #setJustifyContent(int) + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void layoutHorizontal(boolean isRtl, int left, int top, int right, int bottom) { + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + // Use float to reduce the round error that may happen in when justifyContent == + // SPACE_BETWEEN or SPACE_AROUND + float childLeft; + int currentViewIndex = 0; + + int height = bottom - top; + int width = right - left; + // childBottom is used if the mFlexWrap is FLEX_WRAP_WRAP_REVERSE otherwise + // childTop is used to align the vertical position of the children views. + int childBottom = height - getPaddingBottom(); + int childTop = getPaddingTop(); + + // Used only for RTL layout + // Use float to reduce the round error that may happen in when justifyContent == + // SPACE_BETWEEN or SPACE_AROUND + float childRight; + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + if (hasDividerBeforeFlexLine(i)) { + childBottom -= mDividerHorizontalHeight; + childTop += mDividerHorizontalHeight; + } + float spaceBetweenItem = 0f; + switch (mJustifyContent) { + case JUSTIFY_CONTENT_FLEX_START: + childLeft = paddingLeft; + childRight = width - paddingRight; + break; + case JUSTIFY_CONTENT_FLEX_END: + childLeft = width - flexLine.mMainSize + paddingRight; + childRight = flexLine.mMainSize - paddingLeft; + break; + case JUSTIFY_CONTENT_CENTER: + childLeft = paddingLeft + (width - flexLine.mMainSize) / 2f; + childRight = width - paddingRight - (width - flexLine.mMainSize) / 2f; + break; + case JUSTIFY_CONTENT_SPACE_AROUND: + if (flexLine.mItemCount != 0) { + spaceBetweenItem = (width - flexLine.mMainSize) + / (float) flexLine.mItemCount; + } + childLeft = paddingLeft + spaceBetweenItem / 2f; + childRight = width - paddingRight - spaceBetweenItem / 2f; + break; + case JUSTIFY_CONTENT_SPACE_BETWEEN: + childLeft = paddingLeft; + float denominator = flexLine.mItemCount != 1 ? flexLine.mItemCount - 1 : 1f; + spaceBetweenItem = (width - flexLine.mMainSize) / denominator; + childRight = width - paddingRight; + break; + default: + throw new IllegalStateException( + "Invalid justifyContent is set: " + mJustifyContent); + } + spaceBetweenItem = Math.max(spaceBetweenItem, 0); + + for (int j = 0; j < flexLine.mItemCount; j++) { + View child = getReorderedChildAt(currentViewIndex); + if (child == null) { + continue; + } else if (child.getVisibility() == View.GONE) { + currentViewIndex++; + continue; + } + LayoutParams lp = ((LayoutParams) child.getLayoutParams()); + childLeft += lp.leftMargin; + childRight -= lp.rightMargin; + if (hasDividerBeforeChildAtAlongMainAxis(currentViewIndex, j)) { + childLeft += mDividerVerticalWidth; + childRight -= mDividerVerticalWidth; + } + + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + if (isRtl) { + layoutSingleChildHorizontal(child, flexLine, mFlexWrap, mAlignItems, + Math.round(childRight) - child.getMeasuredWidth(), + childBottom - child.getMeasuredHeight(), Math.round(childRight), + childBottom); + } else { + layoutSingleChildHorizontal(child, flexLine, mFlexWrap, mAlignItems, + Math.round(childLeft), childBottom - child.getMeasuredHeight(), + Math.round(childLeft) + child.getMeasuredWidth(), + childBottom); + } + } else { + if (isRtl) { + layoutSingleChildHorizontal(child, flexLine, mFlexWrap, mAlignItems, + Math.round(childRight) - child.getMeasuredWidth(), childTop, + Math.round(childRight), childTop + child.getMeasuredHeight()); + } else { + layoutSingleChildHorizontal(child, flexLine, mFlexWrap, mAlignItems, + 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++; + + flexLine.mLeft = Math.min(flexLine.mLeft, child.getLeft() - lp.leftMargin); + flexLine.mTop = Math.min(flexLine.mTop, child.getTop() - lp.topMargin); + flexLine.mRight = Math.max(flexLine.mRight, child.getRight() + lp.rightMargin); + flexLine.mBottom = Math.max(flexLine.mBottom, child.getBottom() + lp.bottomMargin); + } + childTop += flexLine.mCrossSize; + childBottom -= flexLine.mCrossSize; + } + } + + /** + * Place a single View when the layout direction is horizontal ({@link #mFlexDirection} is + * either {@link #FLEX_DIRECTION_ROW} or {@link #FLEX_DIRECTION_ROW_REVERSE}). + * + * @param view the View to be placed + * @param flexLine the {@link FlexLine} where the View belongs to + * @param flexWrap the flex wrap attribute of this FlexboxLayout + * @param alignItems the align items attribute of this FlexboxLayout + * @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 + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void layoutSingleChildHorizontal(View view, FlexLine flexLine, @FlexWrap int flexWrap, + int alignItems, int left, int top, int right, int bottom) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.alignSelf != LayoutParams.ALIGN_SELF_AUTO) { + // Expecting the values for alignItems and alignSelf match except for ALIGN_SELF_AUTO. + // Assigning the alignSelf value as alignItems should work. + alignItems = lp.alignSelf; + } + int crossSize = flexLine.mCrossSize; + switch (alignItems) { + case ALIGN_ITEMS_FLEX_START: // Intentional fall through + case ALIGN_ITEMS_STRETCH: + if (flexWrap != FLEX_WRAP_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 ALIGN_ITEMS_BASELINE: + if (flexWrap != FLEX_WRAP_WRAP_REVERSE) { + int marginTop = flexLine.mMaxBaseline - view.getBaseline(); + marginTop = Math.max(marginTop, lp.topMargin); + view.layout(left, top + marginTop, right, bottom + marginTop); + } else { + int marginBottom = flexLine.mMaxBaseline - view.getMeasuredHeight() + view + .getBaseline(); + marginBottom = Math.max(marginBottom, lp.bottomMargin); + view.layout(left, top - marginBottom, right, bottom - marginBottom); + } + break; + case ALIGN_ITEMS_FLEX_END: + if (flexWrap != FLEX_WRAP_WRAP_REVERSE) { + view.layout(left, + top + crossSize - view.getMeasuredHeight() - lp.bottomMargin, + right, top + crossSize - lp.bottomMargin); + } else { + // If the flexWrap == FLEX_WRAP_WRAP_REVERSE, the direction of the + // flexEnd is flipped (from top to bottom). + view.layout(left, top - crossSize + view.getMeasuredHeight() + lp.topMargin, + right, bottom - crossSize + view.getMeasuredHeight() + lp.topMargin); + } + break; + case ALIGN_ITEMS_CENTER: + int topFromCrossAxis = (crossSize - view.getMeasuredHeight()) / 2; + if (flexWrap != FLEX_WRAP_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; + } + } + + /** + * Sub method for {@link #onLayout(boolean, int, int, int, int)} when the + * {@link #mFlexDirection} is either {@link #FLEX_DIRECTION_COLUMN} or + * {@link #FLEX_DIRECTION_COLUMN_REVERSE}. + * + * @param isRtl {@code true} if the horizontal layout direction is right to left, + * {@code false} + * otherwise + * @param fromBottomToTop {@code true} if the layout direction is bottom to top, {@code false} + * otherwise + * @param left the left position of this View + * @param top the top position of this View + * @param right the right position of this View + * @param bottom the bottom position of this View + * @see #getFlexWrap() + * @see #setFlexWrap(int) + * @see #getJustifyContent() + * @see #setJustifyContent(int) + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void layoutVertical(boolean isRtl, boolean fromBottomToTop, int left, int top, + int right, int bottom) { + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + + int paddingRight = getPaddingRight(); + int childLeft = getPaddingLeft(); + int currentViewIndex = 0; + + int width = right - left; + int height = bottom - top; + // childRight is used if the mFlexWrap is FLEX_WRAP_WRAP_REVERSE otherwise + // childLeft is used to align the horizontal position of the children views. + int childRight = width - paddingRight; + + // Use float to reduce the round error that may happen in when justifyContent == + // SPACE_BETWEEN or SPACE_AROUND + float childTop; + + // Used only for if the direction is from bottom to top + float childBottom; + + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + if (hasDividerBeforeFlexLine(i)) { + childLeft += mDividerVerticalWidth; + childRight -= mDividerVerticalWidth; + } + float spaceBetweenItem = 0f; + switch (mJustifyContent) { + case JUSTIFY_CONTENT_FLEX_START: + childTop = paddingTop; + childBottom = height - paddingBottom; + break; + case JUSTIFY_CONTENT_FLEX_END: + childTop = height - flexLine.mMainSize + paddingBottom; + childBottom = flexLine.mMainSize - paddingTop; + break; + case JUSTIFY_CONTENT_CENTER: + childTop = paddingTop + (height - flexLine.mMainSize) / 2f; + childBottom = height - paddingBottom - (height - flexLine.mMainSize) / 2f; + break; + case JUSTIFY_CONTENT_SPACE_AROUND: + if (flexLine.mItemCount != 0) { + spaceBetweenItem = (height - flexLine.mMainSize) + / (float) flexLine.mItemCount; + } + childTop = paddingTop + spaceBetweenItem / 2f; + childBottom = height - paddingBottom - spaceBetweenItem / 2f; + break; + case JUSTIFY_CONTENT_SPACE_BETWEEN: + childTop = paddingTop; + float denominator = flexLine.mItemCount != 1 ? flexLine.mItemCount - 1 : 1f; + spaceBetweenItem = (height - flexLine.mMainSize) / denominator; + childBottom = height - paddingBottom; + break; + default: + throw new IllegalStateException( + "Invalid justifyContent is set: " + mJustifyContent); + } + spaceBetweenItem = Math.max(spaceBetweenItem, 0); + + for (int j = 0; j < flexLine.mItemCount; j++) { + View child = getReorderedChildAt(currentViewIndex); + if (child == null) { + continue; + } else if (child.getVisibility() == View.GONE) { + currentViewIndex++; + continue; + } + LayoutParams lp = ((LayoutParams) child.getLayoutParams()); + childTop += lp.topMargin; + childBottom -= lp.bottomMargin; + if (hasDividerBeforeChildAtAlongMainAxis(currentViewIndex, j)) { + childTop += mDividerHorizontalHeight; + childBottom -= mDividerHorizontalHeight; + } + if (isRtl) { + if (fromBottomToTop) { + layoutSingleChildVertical(child, flexLine, true, mAlignItems, + childRight - child.getMeasuredWidth(), + Math.round(childBottom) - child.getMeasuredHeight(), childRight, + Math.round(childBottom)); + } else { + layoutSingleChildVertical(child, flexLine, true, mAlignItems, + childRight - child.getMeasuredWidth(), Math.round(childTop), + childRight, Math.round(childTop) + child.getMeasuredHeight()); + } + } else { + if (fromBottomToTop) { + layoutSingleChildVertical(child, flexLine, false, mAlignItems, + childLeft, Math.round(childBottom) - child.getMeasuredHeight(), + childLeft + child.getMeasuredWidth(), Math.round(childBottom)); + } else { + layoutSingleChildVertical(child, flexLine, false, mAlignItems, + 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++; + + flexLine.mLeft = Math.min(flexLine.mLeft, child.getLeft() - lp.leftMargin); + flexLine.mTop = Math.min(flexLine.mTop, child.getTop() - lp.topMargin); + flexLine.mRight = Math.max(flexLine.mRight, child.getRight() + lp.rightMargin); + flexLine.mBottom = Math.max(flexLine.mBottom, child.getBottom() + lp.bottomMargin); + } + childLeft += flexLine.mCrossSize; + childRight -= flexLine.mCrossSize; + } + } + + /** + * Place a single View when the layout direction is vertical ({@link #mFlexDirection} is + * either {@link #FLEX_DIRECTION_COLUMN} or {@link #FLEX_DIRECTION_COLUMN_REVERSE}). + * + * @param view the View to be placed + * @param flexLine the {@link FlexLine} where the View belongs to + * @param isRtl {@code true} if the layout direction is right to left, {@code false} + * otherwise + * @param alignItems the align items attribute of this FlexboxLayout + * @param left the left position of the flex line where the View belongs to. The actual + * View's left position is shifted depending on the isRtl and alignItems + * attributes + * @param top the top position of the View, which the View's margin is already taken + * into account + * @param right the right position of the flex line where the View belongs to. The actual + * View's right position is shifted depending on the isRtl and alignItems + * attributes + * @param bottom the bottom position of the View, which the View's margin is already taken + * into account + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void layoutSingleChildVertical(View view, FlexLine flexLine, boolean isRtl, + int alignItems, int left, int top, int right, int bottom) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.alignSelf != LayoutParams.ALIGN_SELF_AUTO) { + // Expecting the values for alignItems and alignSelf match except for ALIGN_SELF_AUTO. + // Assigning the alignSelf value as alignItems should work. + alignItems = lp.alignSelf; + } + int crossSize = flexLine.mCrossSize; + switch (alignItems) { + case ALIGN_ITEMS_FLEX_START: // Intentional fall through + case ALIGN_ITEMS_STRETCH: // Intentional fall through + case ALIGN_ITEMS_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 ALIGN_ITEMS_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 ALIGN_ITEMS_CENTER: + int 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; + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDividerDrawableVertical == null && mDividerDrawableHorizontal == null) { + return; + } + if (mShowDividerHorizontal == SHOW_DIVIDER_NONE + && mShowDividerVertical == SHOW_DIVIDER_NONE) { + return; + } + + int layoutDirection = ViewCompat.getLayoutDirection(this); + boolean isRtl; + boolean fromBottomToTop = false; + switch (mFlexDirection) { + case FLEX_DIRECTION_ROW: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + fromBottomToTop = true; + } + drawDividersHorizontal(canvas, isRtl, fromBottomToTop); + break; + case FLEX_DIRECTION_ROW_REVERSE: + isRtl = layoutDirection != ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + fromBottomToTop = true; + } + drawDividersHorizontal(canvas, isRtl, fromBottomToTop); + break; + case FLEX_DIRECTION_COLUMN: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + isRtl = !isRtl; + } + fromBottomToTop = false; + drawDividersVertical(canvas, isRtl, fromBottomToTop); + break; + case FLEX_DIRECTION_COLUMN_REVERSE: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + isRtl = !isRtl; + } + fromBottomToTop = true; + drawDividersVertical(canvas, isRtl, fromBottomToTop); + break; + } + } + + /** + * Sub method for {@link #onDraw(Canvas)} when the main axis direction is horizontal + * ({@link #mFlexDirection} is either of {@link #FLEX_DIRECTION_ROW} or + * {@link #FLEX_DIRECTION_ROW_REVERSE}. + * + * @param canvas the canvas on which the background will be drawn + * @param isRtl {@code true} when the horizontal layout direction is right to left, + * {@code false} otherwise + * @param fromBottomToTop {@code true} when the vertical layout direction is bottom to top, + * {@code false} otherwise + */ + private void drawDividersHorizontal(Canvas canvas, boolean isRtl, boolean fromBottomToTop) { + int currentViewIndex = 0; + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + int horizontalDividerLength = Math.max(0, getWidth() - paddingRight - paddingLeft); + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + for (int j = 0; j < flexLine.mItemCount; j++) { + View view = getReorderedChildAt(currentViewIndex); + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + + // Judge if the beginning or middle divider is needed + if (hasDividerBeforeChildAtAlongMainAxis(currentViewIndex, j)) { + int dividerLeft; + if (isRtl) { + dividerLeft = view.getRight() + lp.rightMargin; + } else { + dividerLeft = view.getLeft() - lp.leftMargin - mDividerVerticalWidth; + } + + drawVerticalDivider(canvas, dividerLeft, flexLine.mTop, flexLine.mCrossSize); + } + + // Judge if the end divider is needed + if (j == flexLine.mItemCount - 1) { + if ((mShowDividerVertical & SHOW_DIVIDER_END) > 0) { + int dividerLeft; + if (isRtl) { + dividerLeft = view.getLeft() - lp.leftMargin - mDividerVerticalWidth; + } else { + dividerLeft = view.getRight() + lp.rightMargin; + } + + drawVerticalDivider(canvas, dividerLeft, flexLine.mTop, + flexLine.mCrossSize); + } + } + currentViewIndex++; + } + + // Judge if the beginning or middle dividers are needed before the flex line + if (hasDividerBeforeFlexLine(i)) { + int horizontalDividerTop; + if (fromBottomToTop) { + horizontalDividerTop = flexLine.mBottom; + } else { + horizontalDividerTop = flexLine.mTop - mDividerHorizontalHeight; + } + drawHorizontalDivider(canvas, paddingLeft, horizontalDividerTop, + horizontalDividerLength); + } + // Judge if the end divider is needed before the flex line + if (hasEndDividerAfterFlexLine(i)) { + if ((mShowDividerHorizontal & SHOW_DIVIDER_END) > 0) { + int horizontalDividerTop; + if (fromBottomToTop) { + horizontalDividerTop = flexLine.mTop - mDividerHorizontalHeight; + } else { + horizontalDividerTop = flexLine.mBottom; + } + drawHorizontalDivider(canvas, paddingLeft, horizontalDividerTop, + horizontalDividerLength); + } + } + } + } + + /** + * Sub method for {@link #onDraw(Canvas)} when the main axis direction is vertical + * ({@link #mFlexDirection} is either of {@link #FLEX_DIRECTION_COLUMN} or + * {@link #FLEX_DIRECTION_COLUMN_REVERSE}. + * + * @param canvas the canvas on which the background will be drawn + * @param isRtl {@code true} when the horizontal layout direction is right to left, + * {@code false} otherwise + * @param fromBottomToTop {@code true} when the vertical layout direction is bottom to top, + * {@code false} otherwise + */ + private void drawDividersVertical(Canvas canvas, boolean isRtl, boolean fromBottomToTop) { + int currentViewIndex = 0; + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + int verticalDividerLength = Math.max(0, getHeight() - paddingBottom - paddingTop); + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + + // Draw horizontal dividers if needed + for (int j = 0; j < flexLine.mItemCount; j++) { + View view = getReorderedChildAt(currentViewIndex); + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + + // Judge if the beginning or middle divider is needed + if (hasDividerBeforeChildAtAlongMainAxis(currentViewIndex, j)) { + int dividerTop; + if (fromBottomToTop) { + dividerTop = view.getBottom() + lp.bottomMargin; + } else { + dividerTop = view.getTop() - lp.topMargin - mDividerHorizontalHeight; + } + + drawHorizontalDivider(canvas, flexLine.mLeft, dividerTop, flexLine.mCrossSize); + } + + // Judge if the end divider is needed + if (j == flexLine.mItemCount - 1) { + if ((mShowDividerHorizontal & SHOW_DIVIDER_END) > 0) { + int dividerTop; + if (fromBottomToTop) { + dividerTop = view.getTop() - lp.topMargin - mDividerHorizontalHeight; + } else { + dividerTop = view.getBottom() + lp.bottomMargin; + } + + drawHorizontalDivider(canvas, flexLine.mLeft, dividerTop, + flexLine.mCrossSize); + } + } + currentViewIndex++; + } + + // Judge if the beginning or middle dividers are needed before the flex line + if (hasDividerBeforeFlexLine(i)) { + int verticalDividerLeft; + if (isRtl) { + verticalDividerLeft = flexLine.mRight; + } else { + verticalDividerLeft = flexLine.mLeft - mDividerVerticalWidth; + } + drawVerticalDivider(canvas, verticalDividerLeft, paddingTop, + verticalDividerLength); + } + if (hasEndDividerAfterFlexLine(i)) { + if ((mShowDividerVertical & SHOW_DIVIDER_END) > 0) { + int verticalDividerLeft; + if (isRtl) { + verticalDividerLeft = flexLine.mLeft - mDividerVerticalWidth; + } else { + verticalDividerLeft = flexLine.mRight; + } + drawVerticalDivider(canvas, verticalDividerLeft, paddingTop, + verticalDividerLength); + } + } + } + } + + private void drawVerticalDivider(Canvas canvas, int left, int top, int length) { + if (mDividerDrawableVertical == null) { + return; + } + mDividerDrawableVertical.setBounds(left, top, left + mDividerVerticalWidth, top + length); + mDividerDrawableVertical.draw(canvas); + } + + private void drawHorizontalDivider(Canvas canvas, int left, int top, int length) { + if (mDividerDrawableHorizontal == null) { + return; + } + mDividerDrawableHorizontal + .setBounds(left, top, left + length, top + mDividerHorizontalHeight); + mDividerDrawableHorizontal.draw(canvas); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof FlexboxLayout.LayoutParams; + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new FlexboxLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + @FlexDirection + public int getFlexDirection() { + return mFlexDirection; + } + + public void setFlexDirection(@FlexDirection int flexDirection) { + if (mFlexDirection != flexDirection) { + mFlexDirection = flexDirection; + requestLayout(); + } + } + + @FlexWrap + public int getFlexWrap() { + return mFlexWrap; + } + + public void setFlexWrap(@FlexWrap int flexWrap) { + if (mFlexWrap != flexWrap) { + mFlexWrap = flexWrap; + requestLayout(); + } + } + + @JustifyContent + public int getJustifyContent() { + return mJustifyContent; + } + + public void setJustifyContent(@JustifyContent int justifyContent) { + if (mJustifyContent != justifyContent) { + mJustifyContent = justifyContent; + requestLayout(); + } + } + + @AlignItems + public int getAlignItems() { + return mAlignItems; + } + + public void setAlignItems(@AlignItems int alignItems) { + if (mAlignItems != alignItems) { + mAlignItems = alignItems; + requestLayout(); + } + } + + @AlignContent + public int getAlignContent() { + return mAlignContent; + } + + public void setAlignContent(@AlignContent int alignContent) { + if (mAlignContent != alignContent) { + mAlignContent = alignContent; + requestLayout(); + } + } + + /** + * @return the flex lines composing this flex container. This method returns an unmodifiable + * list. Thus any changes of the returned list are not supported. + */ + public List getFlexLines() { + return Collections.unmodifiableList(mFlexLines); + } + + /** + * @return the horizontal divider drawable that will divide each item. + * @see #setDividerDrawable(Drawable) + * @see #setDividerDrawableHorizontal(Drawable) + */ + public Drawable getDividerDrawableHorizontal() { + return mDividerDrawableHorizontal; + } + + /** + * @return the vertical divider drawable that will divide each item. + * @see #setDividerDrawable(Drawable) + * @see #setDividerDrawableVertical(Drawable) + */ + public Drawable getDividerDrawableVertical() { + return mDividerDrawableVertical; + } + + /** + * Set a drawable to be used as a divider between items. The drawable is used for both + * horizontal and vertical dividers. + * + * @param divider Drawable that will divide each item for both horizontally and vertically. + * @see #setShowDivider(int) + */ + public void setDividerDrawable(Drawable divider) { + setDividerDrawableHorizontal(divider); + setDividerDrawableVertical(divider); + } + + /** + * Set a drawable to be used as a horizontal divider between items. + * + * @param divider Drawable that will divide each item. + * @see #setDividerDrawable(Drawable) + * @see #setShowDivider(int) + * @see #setShowDividerHorizontal(int) + */ + public void setDividerDrawableHorizontal(Drawable divider) { + if (divider == mDividerDrawableHorizontal) { + return; + } + mDividerDrawableHorizontal = divider; + if (divider != null) { + mDividerHorizontalHeight = divider.getIntrinsicHeight(); + } else { + mDividerHorizontalHeight = 0; + } + setWillNotDrawFlag(); + requestLayout(); + } + + /** + * Set a drawable to be used as a vertical divider between items. + * + * @param divider Drawable that will divide each item. + * @see #setDividerDrawable(Drawable) + * @see #setShowDivider(int) + * @see #setShowDividerVertical(int) + */ + public void setDividerDrawableVertical(Drawable divider) { + if (divider == mDividerDrawableVertical) { + return; + } + mDividerDrawableVertical = divider; + if (divider != null) { + mDividerVerticalWidth = divider.getIntrinsicWidth(); + } else { + mDividerVerticalWidth = 0; + } + setWillNotDrawFlag(); + requestLayout(); + } + + @FlexboxLayout.DividerMode + public int getShowDividerVertical() { + return mShowDividerVertical; + } + + @FlexboxLayout.DividerMode + public int getShowDividerHorizontal() { + return mShowDividerHorizontal; + } + + /** + * Set how dividers should be shown between items in this layout. This method sets the + * divider mode for both horizontally and vertically. + * + * @param dividerMode One or more of {@link #SHOW_DIVIDER_BEGINNING}, + * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}, + * or {@link #SHOW_DIVIDER_NONE} to show no dividers. + * @see #setShowDividerVertical(int) + * @see #setShowDividerHorizontal(int) + */ + public void setShowDivider(@DividerMode int dividerMode) { + setShowDividerVertical(dividerMode); + setShowDividerHorizontal(dividerMode); + } + + /** + * Set how vertical dividers should be shown between items in this layout + * + * @param dividerMode One or more of {@link #SHOW_DIVIDER_BEGINNING}, + * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}, + * or {@link #SHOW_DIVIDER_NONE} to show no dividers. + * @see #setShowDivider(int) + */ + public void setShowDividerVertical(@DividerMode int dividerMode) { + if (dividerMode != mShowDividerVertical) { + mShowDividerVertical = dividerMode; + requestLayout(); + } + } + + /** + * Set how horizontal dividers should be shown between items in this layout. + * + * @param dividerMode One or more of {@link #SHOW_DIVIDER_BEGINNING}, + * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}, + * or {@link #SHOW_DIVIDER_NONE} to show no dividers. + * @see #setShowDivider(int) + */ + public void setShowDividerHorizontal(@DividerMode int dividerMode) { + if (dividerMode != mShowDividerHorizontal) { + mShowDividerHorizontal = dividerMode; + requestLayout(); + } + } + + private void setWillNotDrawFlag() { + if (mDividerDrawableHorizontal == null && mDividerDrawableVertical == null) { + setWillNotDraw(true); + } else { + setWillNotDraw(false); + } + } + + /** + * Check if a divider is needed before the view whose indices are passed as arguments. + * + * @param childAbsoluteIndex the absolute index of the view to be judged + * @param childRelativeIndexInFlexLine the relative index in the flex line where the view + * belongs + * @return {@code true} if a divider is needed, {@code false} otherwise + */ + private boolean hasDividerBeforeChildAtAlongMainAxis(int childAbsoluteIndex, + int childRelativeIndexInFlexLine) { + if (allViewsAreGoneBefore(childAbsoluteIndex, childRelativeIndexInFlexLine)) { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerVertical & SHOW_DIVIDER_BEGINNING) != 0; + } else { + return (mShowDividerHorizontal & SHOW_DIVIDER_BEGINNING) != 0; + } + } else { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerVertical & SHOW_DIVIDER_MIDDLE) != 0; + } else { + return (mShowDividerHorizontal & SHOW_DIVIDER_MIDDLE) != 0; + } + } + } + + private boolean allViewsAreGoneBefore(int childAbsoluteIndex, + int childRelativeIndexInFlexLine) { + for (int i = 1; i <= childRelativeIndexInFlexLine; i++) { + View view = getReorderedChildAt(childAbsoluteIndex - i); + if (view != null && view.getVisibility() != View.GONE) { + return false; + } + } + return true; + } + + /** + * Check if a divider is needed before the flex line whose index is passed as an argument. + * + * @param flexLineIndex the index of the flex line to be checked + * @return {@code true} if a divider is needed, {@code false} otherwise + */ + private boolean hasDividerBeforeFlexLine(int flexLineIndex) { + if (flexLineIndex < 0 || flexLineIndex >= mFlexLines.size()) { + return false; + } + if (allFlexLinesAreDummyBefore(flexLineIndex)) { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerHorizontal & SHOW_DIVIDER_BEGINNING) != 0; + } else { + return (mShowDividerVertical & SHOW_DIVIDER_BEGINNING) != 0; + } + } else { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerHorizontal & SHOW_DIVIDER_MIDDLE) != 0; + } else { + return (mShowDividerVertical & SHOW_DIVIDER_MIDDLE) != 0; + } + } + } + + private boolean allFlexLinesAreDummyBefore(int flexLineIndex) { + for (int i = 0; i < flexLineIndex; i++) { + if (mFlexLines.get(i).mItemCount > 0) { + return false; + } + } + return true; + } + + /** + * Check if a end divider is needed after the flex line whose index is passed as an argument. + * + * @param flexLineIndex the index of the flex line to be checked + * @return {@code true} if a divider is needed, {@code false} otherwise + */ + private boolean hasEndDividerAfterFlexLine(int flexLineIndex) { + if (flexLineIndex < 0 || flexLineIndex >= mFlexLines.size()) { + return false; + } + + for (int i = flexLineIndex + 1; i < mFlexLines.size(); i++) { + if (mFlexLines.get(i).mItemCount > 0) { + return false; + } + } + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerHorizontal & SHOW_DIVIDER_END) != 0; + } else { + return (mShowDividerVertical & SHOW_DIVIDER_END) != 0; + } + + } + + /** + * Per child parameters for children views of the {@link FlexboxLayout}. + */ + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + + private static final int ORDER_DEFAULT = 1; + + private static final float FLEX_GROW_DEFAULT = 0f; + + private static final float FLEX_SHRINK_DEFAULT = 1f; + + public static final float FLEX_BASIS_PERCENT_DEFAULT = -1f; + + public static final int ALIGN_SELF_AUTO = -1; + + public static final int ALIGN_SELF_FLEX_START = ALIGN_ITEMS_FLEX_START; + + public static final int ALIGN_SELF_FLEX_END = ALIGN_ITEMS_FLEX_END; + + public static final int ALIGN_SELF_CENTER = ALIGN_ITEMS_CENTER; + + public static final int ALIGN_SELF_BASELINE = ALIGN_ITEMS_BASELINE; + + public static final int ALIGN_SELF_STRETCH = ALIGN_ITEMS_STRETCH; + + private static final int MAX_SIZE = Integer.MAX_VALUE & ViewCompat.MEASURED_SIZE_MASK; + + /** + * This attribute can change the ordering of the children views are laid out. + * By default, children are displayed and laid out in the same order as they appear in the + * layout XML. If not specified, {@link #ORDER_DEFAULT} is set as a default value. + */ + public int order = ORDER_DEFAULT; + + /** + * This attribute determines how much this child will grow if positive free space is + * distributed relative to the rest of other flex items included in the same flex line. + * If not specified, {@link #FLEX_GROW_DEFAULT} is set as a default value. + */ + public float flexGrow = FLEX_GROW_DEFAULT; + + /** + * This attributes determines how much this child will shrink is negative free space is + * distributed relative to the rest of other flex items included in the same flex line. + * If not specified, {@link #FLEX_SHRINK_DEFAULT} is set as a default value. + */ + public float flexShrink = FLEX_SHRINK_DEFAULT; + + /** + * This attributes determines the alignment along the cross axis (perpendicular to the + * main axis). The alignment in the same direction can be determined by the + * {@link #mAlignItems} in the parent, but if this is set to other than + * {@link #ALIGN_SELF_AUTO}, the cross axis alignment is overridden for this child. + * The value needs to be one of the values in ({@link #ALIGN_SELF_AUTO}, + * {@link #ALIGN_SELF_STRETCH}, {@link #ALIGN_SELF_FLEX_START}, {@link + * #ALIGN_SELF_FLEX_END}, {@link #ALIGN_SELF_CENTER}, or {@link #ALIGN_SELF_BASELINE}). + * If not specified, {@link #ALIGN_SELF_AUTO} is set as a default value. + */ + public int alignSelf = ALIGN_SELF_AUTO; + + /** + * The initial flex item length in a fraction format relative to its parent. + * The initial main size of this child View is trying to be expanded as the specified + * fraction against the parent main size. + * If this value is set, the length specified from layout_width + * (or layout_height) is overridden by the calculated value from this attribute. + * This attribute is only effective when the parent's MeasureSpec mode is + * MeasureSpec.EXACTLY. The default value is {@link #FLEX_BASIS_PERCENT_DEFAULT}, which + * means not set. + */ + public float flexBasisPercent = FLEX_BASIS_PERCENT_DEFAULT; + + /** + * This attribute determines the minimum width the child can shrink to. + */ + public int minWidth; + + /** + * This attribute determines the minimum height the child can shrink to. + */ + public int minHeight; + + /** + * This attribute determines the maximum width the child can expand to. + */ + public int maxWidth = MAX_SIZE; + + /** + * This attribute determines the maximum height the child can expand to. + */ + public int maxHeight = MAX_SIZE; + + /** + * This attribute forces a flex line wrapping. i.e. if this is set to {@code true} for a + * flex item, the item will become the first item of the new flex line. (A wrapping happens + * regardless of the flex items being processed in the the previous flex line) + * This attribute is ignored if the flex_wrap attribute is set as nowrap. + * The equivalent attribute isn't defined in the original CSS Flexible Box Module + * specification, but having this attribute is useful for Android developers to flatten + * the layouts when building a grid like layout or for a situation where developers want + * to put a new flex line to make a semantic difference from the previous one, etc. + */ + public boolean wrapBefore; + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + // // NOTE: We do not support android xml. + // TypedArray a = context + // .obtainStyledAttributes(attrs, R.styleable.FlexboxLayout_Layout); + // order = a.getInt(R.styleable.FlexboxLayout_Layout_layout_order, ORDER_DEFAULT); + // flexGrow = a + // .getFloat(R.styleable.FlexboxLayout_Layout_layout_flexGrow, FLEX_GROW_DEFAULT); + // flexShrink = a.getFloat(R.styleable.FlexboxLayout_Layout_layout_flexShrink, + // FLEX_SHRINK_DEFAULT); + // alignSelf = a + // .getInt(R.styleable.FlexboxLayout_Layout_layout_alignSelf, ALIGN_SELF_AUTO); + // flexBasisPercent = a + // .getFraction(R.styleable.FlexboxLayout_Layout_layout_flexBasisPercent, 1, 1, + // FLEX_BASIS_PERCENT_DEFAULT); + // minWidth = a.getDimensionPixelSize(R.styleable.FlexboxLayout_Layout_layout_minWidth, 0); + // minHeight = a + // .getDimensionPixelSize(R.styleable.FlexboxLayout_Layout_layout_minHeight, 0); + // maxWidth = a.getDimensionPixelSize(R.styleable.FlexboxLayout_Layout_layout_maxWidth, + // MAX_SIZE); + // maxHeight = a.getDimensionPixelSize(R.styleable.FlexboxLayout_Layout_layout_maxHeight, + // MAX_SIZE); + // wrapBefore = a.getBoolean(R.styleable.FlexboxLayout_Layout_layout_wrapBefore, false); + // a.recycle(); + } + + public LayoutParams(LayoutParams source) { + super(source); + + order = source.order; + flexGrow = source.flexGrow; + flexShrink = source.flexShrink; + alignSelf = source.alignSelf; + flexBasisPercent = source.flexBasisPercent; + minWidth = source.minWidth; + minHeight = source.minHeight; + maxWidth = source.maxWidth; + maxHeight = source.maxHeight; + wrapBefore = source.wrapBefore; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(int width, int height) { + super(new ViewGroup.LayoutParams(width, height)); + } + } + + /** + * 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 + + '}'; + } + } +} \ No newline at end of file