Flow.java

/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      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 androidx.constraintlayout.helper.widget;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.R;
import androidx.constraintlayout.widget.VirtualLayout;
import androidx.constraintlayout.core.widgets.ConstraintWidget;
import androidx.constraintlayout.core.widgets.HelperWidget;
import android.util.AttributeSet;
import android.util.SparseArray;

/**
 *
 * Flow VirtualLayout. <b>Added in 2.0</b>
 *
 * Allows positioning of referenced widgets horizontally or vertically, similar to a Chain.
 *
 * The elements referenced are indicated via constraint_referenced_ids, as with other ConstraintHelper implementations.
 *
 * Those referenced widgets are then laid out by the Flow virtual layout in three possible ways:
 * <ul>
 *     <li><a href="#wrap_none">wrap none</a> : simply create a chain out of the referenced elements</li>
 *     <li><a href="#wrap_chain">wrap chain</a> : create multiple chains (one after the other) if the referenced elements do not fit</li>
 *     <li><a href="#wrap_aligned">wrap aligned</a> : similar to wrap chain, but will align the elements by creating rows and columns</li>
 * </ul>
 *
 * As VirtualLayouts are ConstraintHelpers, they are normal views; you can thus treat them as such, and setting up
 * constraints on them (position, dimension) or some view attributes (background, padding) will work. The main difference between VirtualLayouts and ViewGroups is
 * that:
 * <ul>
 *     <li>VirtualLayout keep the hierarchy flat</li>
 *     <li>Other views can thus reference / constrain to not only the VirtualLayout, but also the views laid out by the VirtualLayout</li>
 *     <li>VirtualLayout allow on the fly behavior modifications (e.g. for Flow, changing the orientation)</li>
 * </ul>
 *
 * <h4 id="wrap_none">flow_wrapMode = "none"</h4>
 *
 * This will simply create an horizontal or vertical chain out of the referenced widgets. This is the default behavior of Flow.
 *
 * XML attributes that are allowed in this mode:
 *
 * <ul>
 *     <li>flow_horizontalStyle = "spread|spread_inside|packed"</li>
 *     <li>flow_verticalStyle = "spread|spread_inside|packed"</li>
 *     <li>flow_horizontalBias = "<i>float</i>"</li>
 *     <li>flow_verticalBias = "<i>float</i>"</li>
 *     <li>flow_horizontalGap = "<i>dimension</i>"</li>
 *     <li>flow_verticalGap = "<i>dimension</i>"</li>
 *     <li>flow_horizontalAlign = "start|end"</li>
 *     <li>flow_verticalAlign = "top|bottom|center|baseline</li>
 * </ul>
 *
 * While the elements are laid out as a chain in the orientation defined, the way they are laid out in the other dimension is controlled
 * by <i>flow_horizontalAlign</i> and <i>flow_verticalAlign</i> attributes.
 *
 * <h4 id="wrap_chain">flow_wrapMode = "chain"</h4>
 *
 * Similar to wrap none in terms of creating chains, but if the referenced widgets do not fit the horizontal or vertical dimension (depending
 * on the orientation picked), they will wrap around to the next line / column.
 *
 * XML attributes are the same same as in wrap_none, with the addition of attributes specifying chain style and chain bias applied
 * to the first chain. This way, it is possible to specify different chain behavior between the first chain and the rest of the chains eventually created.
 *
 * <ul>
 *     <li>flow_firstHorizontalStyle = "spread|spread_inside|packed"</li>
 *     <li>flow_firstVerticalStyle = "spread|spread_inside|packed"</li>
 *     <li>flow_firstHorizontalBias = "<i>float</i>"</li>
 *     <li>flow_firstVerticalBias = "<i>float</i>"</li>
 * </ul>
 *
 * One last important attribute is <i>flow_maxElementsWrap</i>, which specify the number of elements before wrapping, regardless if they
 * fit or not in the available space.
 *
 * <h4 id="wrap_aligned">flow_wrapMode = "aligned"</h4>
 *
 * Same XML attributes as for WRAP_CHAIN, with the difference that the elements are going to be laid out in a set of rows and columns instead of chains.
 * The attribute specifying chains style and bias are thus not going to be applied.
 */
public class Flow extends VirtualLayout {
    private static final String TAG = "Flow";

    private androidx.constraintlayout.core.widgets.Flow mFlow;

    public static final int HORIZONTAL = androidx.constraintlayout.core.widgets.Flow.HORIZONTAL;
    public static final int VERTICAL = androidx.constraintlayout.core.widgets.Flow.VERTICAL;
    public static final int WRAP_NONE = androidx.constraintlayout.core.widgets.Flow.WRAP_NONE;
    public static final int WRAP_CHAIN = androidx.constraintlayout.core.widgets.Flow.WRAP_CHAIN;
    public static final int WRAP_ALIGNED = androidx.constraintlayout.core.widgets.Flow.WRAP_ALIGNED;

    public static final int CHAIN_SPREAD = ConstraintWidget.CHAIN_SPREAD;
    public static final int CHAIN_SPREAD_INSIDE = ConstraintWidget.CHAIN_SPREAD_INSIDE;
    public static final int CHAIN_PACKED = ConstraintWidget.CHAIN_PACKED;

    public static final int HORIZONTAL_ALIGN_START = androidx.constraintlayout.core.widgets.Flow.HORIZONTAL_ALIGN_START;
    public static final int HORIZONTAL_ALIGN_END = androidx.constraintlayout.core.widgets.Flow.HORIZONTAL_ALIGN_END;
    public static final int HORIZONTAL_ALIGN_CENTER = androidx.constraintlayout.core.widgets.Flow.HORIZONTAL_ALIGN_CENTER;

    public static final int VERTICAL_ALIGN_TOP = androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_TOP;
    public static final int VERTICAL_ALIGN_BOTTOM = androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_BOTTOM;
    public static final int VERTICAL_ALIGN_CENTER = androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_CENTER;
    public static final int VERTICAL_ALIGN_BASELINE = androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_BASELINE;

    public Flow(Context context) {
        super(context);
    }

    public Flow(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Flow(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * @hide
     *
     * @param widget
     * @param isRtl
     */
    @Override
    public void resolveRtl(ConstraintWidget widget, boolean isRtl) {
        mFlow.applyRtl(isRtl);
    }

    @SuppressLint("WrongCall")
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        onMeasure(mFlow, widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * @hide
     *
     * @param layout
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    public void onMeasure(androidx.constraintlayout.core.widgets.VirtualLayout layout, int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (layout != null) {
            layout.measure(widthMode, widthSize, heightMode, heightSize);
            setMeasuredDimension(layout.getMeasuredWidth(), layout.getMeasuredHeight());
        } else {
            setMeasuredDimension(0,0);
        }
    }

    /**
     * @hide
     *
     * @param constraint
     * @param child
     * @param layoutParams
     * @param mapIdToWidget
     */
    @Override
    public void loadParameters(ConstraintSet.Constraint constraint, HelperWidget child, ConstraintLayout.LayoutParams layoutParams, SparseArray<ConstraintWidget> mapIdToWidget) {
        super.loadParameters(constraint,child,layoutParams, mapIdToWidget);
        if (child instanceof androidx.constraintlayout.core.widgets.Flow) {
            androidx.constraintlayout.core.widgets.Flow flow = (androidx.constraintlayout.core.widgets.Flow) child;
            if (layoutParams.orientation != -1) {
                flow.setOrientation(layoutParams.orientation);
            }
        }
    }

    /**
     * @hide
     *
     * @param attrs
     */
    @Override
    protected void init(AttributeSet attrs) {
        super.init(attrs);
        mFlow = new androidx.constraintlayout.core.widgets.Flow();
        if (attrs != null) {
            TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ConstraintLayout_Layout);
            final int N = a.getIndexCount();
            for (int i = 0; i < N; i++) {
                int attr = a.getIndex(i);
                if (attr == R.styleable.ConstraintLayout_Layout_android_orientation) {
                    mFlow.setOrientation(a.getInt(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_android_padding) {
                    mFlow.setPadding(a.getDimensionPixelSize(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_android_paddingStart) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                        mFlow.setPaddingStart(a.getDimensionPixelSize(attr, 0));
                    }
                } else if (attr == R.styleable.ConstraintLayout_Layout_android_paddingEnd) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                        mFlow.setPaddingEnd(a.getDimensionPixelSize(attr, 0));
                    }
                } else if (attr == R.styleable.ConstraintLayout_Layout_android_paddingLeft) {
                    mFlow.setPaddingLeft(a.getDimensionPixelSize(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_android_paddingTop) {
                    mFlow.setPaddingTop(a.getDimensionPixelSize(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_android_paddingRight) {
                    mFlow.setPaddingRight(a.getDimensionPixelSize(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_android_paddingBottom) {
                    mFlow.setPaddingBottom(a.getDimensionPixelSize(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_wrapMode) {
                    mFlow.setWrapMode(a.getInt(attr, androidx.constraintlayout.core.widgets.Flow.WRAP_NONE));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_horizontalStyle) {
                    mFlow.setHorizontalStyle(a.getInt(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_verticalStyle) {
                    mFlow.setVerticalStyle(a.getInt(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_firstHorizontalStyle) {
                    mFlow.setFirstHorizontalStyle(a.getInt(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_lastHorizontalStyle) {
                    mFlow.setLastHorizontalStyle(a.getInt(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_firstVerticalStyle) {
                    mFlow.setFirstVerticalStyle(a.getInt(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_lastVerticalStyle) {
                    mFlow.setLastVerticalStyle(a.getInt(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_horizontalBias) {
                    mFlow.setHorizontalBias(a.getFloat(attr, 0.5f));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_firstHorizontalBias) {
                    mFlow.setFirstHorizontalBias(a.getFloat(attr, 0.5f));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_lastHorizontalBias) {
                    mFlow.setLastHorizontalBias(a.getFloat(attr, 0.5f));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_firstVerticalBias) {
                    mFlow.setFirstVerticalBias(a.getFloat(attr, 0.5f));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_lastVerticalBias) {
                    mFlow.setLastVerticalBias(a.getFloat(attr, 0.5f));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_verticalBias) {
                    mFlow.setVerticalBias(a.getFloat(attr, 0.5f));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_horizontalAlign) {
                    mFlow.setHorizontalAlign(a.getInt(attr, androidx.constraintlayout.core.widgets.Flow.HORIZONTAL_ALIGN_CENTER));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_verticalAlign) {
                    mFlow.setVerticalAlign(a.getInt(attr, androidx.constraintlayout.core.widgets.Flow.VERTICAL_ALIGN_CENTER));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_horizontalGap) {
                    mFlow.setHorizontalGap(a.getDimensionPixelSize(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_verticalGap) {
                    mFlow.setVerticalGap(a.getDimensionPixelSize(attr, 0));
                } else if (attr == R.styleable.ConstraintLayout_Layout_flow_maxElementsWrap) {
                    mFlow.setMaxElementsWrap(a.getInt(attr, -1));
                }
            }
            a.recycle();
        }

        mHelperWidget = mFlow;
        validateParams();
    }

    /**
     * Set the orientation of the layout
     *
     * @param orientation either Flow.HORIZONTAL or FLow.VERTICAL
     */
    public void setOrientation(int orientation) {
        mFlow.setOrientation(orientation);
        requestLayout();
    }

    /**
     * Set padding around the content
     *
     * @param padding
     */
    public void setPadding(int padding) {
        mFlow.setPadding(padding);
        requestLayout();
    }

    /**
     * Set padding left around the content
     *
     * @param paddingLeft
     */
    public void setPaddingLeft(int paddingLeft) {
        mFlow.setPaddingLeft(paddingLeft);
        requestLayout();
    }

    /**
     * Set padding top around the content
     *
     * @param paddingTop
     */
    public void setPaddingTop(int paddingTop) {
        mFlow.setPaddingTop(paddingTop);
        requestLayout();
    }

    /**
     * Set padding right around the content
     *
     * @param paddingRight
     */
    public void setPaddingRight(int paddingRight) {
        mFlow.setPaddingRight(paddingRight);
        requestLayout();
    }

    /**
     * Set padding bottom around the content
     *
     * @param paddingBottom
     */
    public void setPaddingBottom(int paddingBottom) {
        mFlow.setPaddingBottom(paddingBottom);
        requestLayout();
    }

    /**
     * Set wrap mode for the layout. Can be:
     *
     * Flow.WRAP_NONE (default) -- no wrap behavior, create a single chain
     * Flow.WRAP_CHAIN -- if not enough space to fit the referenced elements, will create additional chains after the first one
     * Flow.WRAP_ALIGNED -- if not enough space to fit the referenced elements, will wrap the elements, keeping them aligned (like a table)
     *
     * @param mode
     */
    public void setWrapMode(int mode) {
        mFlow.setWrapMode(mode);
        requestLayout();
    }

    /**
     * Set horizontal chain style. Can be:
     *
     * Flow.CHAIN_SPREAD
     * Flow.CHAIN_SPREAD_INSIDE
     * Flow.CHAIN_PACKED
     *
     * @param style
     */
    public void setHorizontalStyle(int style) {
        mFlow.setHorizontalStyle(style);
        requestLayout();
    }

    /**
     * Set vertical chain style. Can be:
     *
     * Flow.CHAIN_SPREAD
     * Flow.CHAIN_SPREAD_INSIDE
     * Flow.CHAIN_PACKED
     *
     * @param style
     */
    public void setVerticalStyle(int style) {
        mFlow.setVerticalStyle(style);
        requestLayout();
    }

    /**
     * Set the horizontal bias applied to the chain
     *
     * @param bias from 0 to 1
     */
    public void setHorizontalBias(float bias) {
        mFlow.setHorizontalBias(bias);
        requestLayout();
    }

    /**
     * Set the vertical bias applied to the chain
     *
     * @param bias from 0 to 1
     */
    public void setVerticalBias(float bias) {
        mFlow.setVerticalBias(bias);
        requestLayout();
    }

    /**
     * Similar to setHorizontalStyle(), but only applies to the first chain.
     *
     * @param style
     */
    public void setFirstHorizontalStyle(int style) {
        mFlow.setFirstHorizontalStyle(style);
        requestLayout();
    }

    /**
     * Similar to setVerticalStyle(), but only applies to the first chain.
     *
     * @param style
     */
    public void setFirstVerticalStyle(int style) {
        mFlow.setFirstVerticalStyle(style);
        requestLayout();
    }

    /**
     * Similar to setHorizontalBias(), but only applied to the first chain.
     *
     * @param bias
     */
    public void setFirstHorizontalBias(float bias) {
        mFlow.setFirstHorizontalBias(bias);
        requestLayout();
    }

    /**
     * Similar to setVerticalBias(), but only applied to the first chain.
     *
     * @param bias
     */
    public void setFirstVerticalBias(float bias) {
        mFlow.setFirstVerticalBias(bias);
        requestLayout();
    }

    /**
     * Set up the horizontal alignment of the elements in the layout, if the layout orientation is set to Flow.VERTICAL
     *
     * Can be either:
     * Flow.HORIZONTAL_ALIGN_START
     * Flow.HORIZONTAL_ALIGN_END
     * Flow.HORIZONTAL_ALIGN_CENTER
     *
     * @param align
     */
    public void setHorizontalAlign(int align) {
        mFlow.setHorizontalAlign(align);
        requestLayout();
    }

    /**
     * Set up the vertical alignment of the elements in the layout, if the layout orientation is set to Flow.HORIZONTAL
     *
     * Can be either:
     * Flow.VERTICAL_ALIGN_TOP
     * Flow.VERTICAL_ALIGN_BOTTOM
     * Flow.VERTICAL_ALIGN_CENTER
     * Flow.VERTICAL_ALIGN_BASELINE
     *
     * @param align
     */
    public void setVerticalAlign(int align) {
        mFlow.setVerticalAlign(align);
        requestLayout();
    }

    /**
     * Set up the horizontal gap between elements
     *
     * @param gap
     */
    public void setHorizontalGap(int gap) {
        mFlow.setHorizontalGap(gap);
        requestLayout();
    }

    /**
     * Set up the vertical gap between elements
     *
     * @param gap
     */
    public void setVerticalGap(int gap) {
        mFlow.setVerticalGap(gap);
        requestLayout();
    }

    /**
     * Set up the maximum number of elements before wrapping.
     *
     * @param max
     */
    public void setMaxElementsWrap(int max) {
        mFlow.setMaxElementsWrap(max);
        requestLayout();
    }
}