public class

Grid

extends VirtualLayout

 java.lang.Object

↳View

androidx.constraintlayout.widget.ConstraintHelper

androidx.constraintlayout.widget.VirtualLayout

↳androidx.constraintlayout.helper.widget.Grid

Gradle dependencies

compile group: 'androidx.constraintlayout', name: 'constraintlayout', version: '2.2.0-alpha01'

  • groupId: androidx.constraintlayout
  • artifactId: constraintlayout
  • version: 2.2.0-alpha01

Artifact androidx.constraintlayout:constraintlayout:2.2.0-alpha01 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.constraintlayout:constraintlayout com.android.support.constraint:constraint-layout

Overview

A helper class that helps arrange widgets in a grid form

Grid

AttributesDescription
grid_rows Indicates the number of rows will be created for the grid form.
grid_columns Indicates the number of columns will be created for the grid form.
grid_spans Offers the capability to span a widget across multiple rows and columns
grid_skips Enables skip certain positions in the grid and leave them empty
grid_orientation Defines how the associated widgets will be arranged - vertically or horizontally
grid_horizontalGaps Adds margin horizontally between widgets
grid_verticalGaps Adds margin vertically between widgets

Summary

Fields
from ConstraintHelpermCount, mHelperWidget, mIds[], mMap, mReferenceIds, mReferenceTags, mUseViewMeasure, myContext
Constructors
publicGrid(Context context)

publicGrid(Context context, AttributeSet attrs)

publicGrid(Context context, AttributeSet attrs, int defStyleAttr)

Methods
public java.lang.StringgetStrSkips()

get the string value of skips

public java.lang.StringgetStrSpans()

get the string value of spans

protected voidinit(AttributeSet attrs)

public voidonAttachedToWindow()

from VirtualLayoutapplyLayoutFeaturesInConstraintSet, onMeasure, setElevation, setVisibility
from ConstraintHelperaddView, applyLayoutFeatures, applyLayoutFeatures, containsId, getReferencedIds, getViews, indexFromId, loadParameters, onDraw, onMeasure, removeView, resolveRtl, setIds, setReferencedIds, setReferenceTags, setTag, updatePostConstraints, updatePostLayout, updatePostMeasure, updatePreDraw, updatePreLayout, updatePreLayout, validateParams
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public Grid(Context context)

public Grid(Context context, AttributeSet attrs)

public Grid(Context context, AttributeSet attrs, int defStyleAttr)

Methods

protected void init(AttributeSet attrs)

public void onAttachedToWindow()

public java.lang.String getStrSpans()

get the string value of spans

Returns:

the string value of spans

public java.lang.String getStrSkips()

get the string value of skips

Returns:

the string value of skips

Source

/*
 * Copyright (C) 2022 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.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.View;

import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.Guideline;
import androidx.constraintlayout.widget.R;
import androidx.constraintlayout.widget.VirtualLayout;
import androidx.core.view.ViewCompat;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * A helper class that helps arrange widgets in a grid form
 *
 * <h2>Grid</h2>
 * <table summary="Grid attributes">
 *   <tr>
 *     <th>Attributes</th><th>Description</th>
 *   </tr>
 *   <tr>
 *     <td>grid_rows</td>
 *     <td>Indicates the number of rows will be created for the grid form.</td>
 *   </tr>
 *   <tr>
 *     <td>grid_columns</td>
 *     <td>Indicates the number of columns will be created for the grid form.</td>
 *   </tr>
 *   <tr>
 *     <td>grid_spans</td>
 *     <td>Offers the capability to span a widget across multiple rows and columns</td>
 *   </tr>
 *   <tr>
 *     <td>grid_skips</td>
 *     <td>Enables skip certain positions in the grid and leave them empty</td>
 *   </tr>
 *   <tr>
 *     <td>grid_orientation</td>
 *     <td>Defines how the associated widgets will be arranged - vertically or horizontally</td>
 *   </tr>
 *   <tr>
 *     <td>grid_horizontalGaps</td>
 *     <td>Adds margin horizontally between widgets</td>
 *   </tr>
 *   <tr>
 *      <td>grid_verticalGaps</td>
 *     <td>Adds margin vertically between widgets</td>
 *   </tr>
 * </table>
 */
public class Grid extends VirtualLayout {
    private static final String TAG = "Grid";
    private static final String VERTICAL = "vertical";
    private final ConstraintSet mConstraintSet = new ConstraintSet();
    ConstraintLayout mContainer;

    /**
     * number of rows of the grid
     */
    private int mRows;

    /**
     * number of columns of the grid
     */
    private int mColumns;

    /**
     * an Guideline array to store all the vertical guidelines
     */
    private Guideline[] mVerticalGuideLines;

    /**
     * an Guideline array to store all the horizontal guidelines
     */
    private Guideline[] mHorizontalGuideLines;

    /**
     * string format of the input Spans
     */
    private String mStrSpans;

    /**
     * string format of the input Skips
     */
    private String mStrSkips;

    /**
     * Horizontal gaps in Dp
     */
    private int mHorizontalGaps;

    /**
     * Vertical gaps in Dp
     */
    private int mVerticalGaps;

    /**
     * orientation of the view arrangement - vertical or horizontal
     */
    private String mOrientation;

    /**
     * Indicates what is the next available position to place an widget
     */
    private int mNextAvailableIndex = 0;

    /**
     * Indicates whether the input attributes need to be validated
     */
    private boolean mValidateInputs;

    /**
     * Indicates whether to use RTL layout direction
     */
    private boolean mUseRtl;

    /**
     * A integer matrix that tracks the positions that are occupied by skips and spans
     * true: available position
     * false: non-available position
     */
    private boolean[][] mPositionMatrix;

    /**
     * Store the view ids of handled spans
     */
    Set<Integer> mSpanIds = new HashSet<>();

    /**
     * class that stores the relevant span information
     */
    static class Span {
        int mId;
        int mStartRow;
        int mStartColumn;
        int mRowSpan;
        int mColumnSpan;
        String mGravity;

        Span(int id, int startRow, int startColumn,
                    int rowSpan, int columnSpan, String gravity) {
            this.mId = id;
            this.mStartRow = startRow;
            this.mStartColumn = startColumn;
            this.mRowSpan = rowSpan;
            this.mColumnSpan = columnSpan;
            this.mGravity = gravity;
        }

        public int getId() {
            return mId;
        }

        public int getStartRow() {
            return mStartRow;
        }

        public int getStartColumn() {
            return mStartColumn;
        }

        public int getRowSpan() {
            return mRowSpan;
        }

        public int getColumnSpan() {
            return mColumnSpan;
        }

        public String getGravity() {
            return mGravity;
        }
    }

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

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

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

    @Override
    protected void init(AttributeSet attrs) {
        super.init(attrs);

        // Parse the relevant attributes from layout xml
        if (attrs != null) {
            TypedArray a = getContext().obtainStyledAttributes(attrs,
                    R.styleable.Grid);
            final int n = a.getIndexCount();

            for (int i = 0; i < n; i++) {
                int attr = a.getIndex(i);
                if (attr == R.styleable.Grid_grid_rows) {
                    mRows = a.getInteger(attr, 1);
                } else if (attr == R.styleable.Grid_grid_columns) {
                    mColumns = a.getInteger(attr, 1);
                } else if (attr == R.styleable.Grid_grid_spans) {
                    mStrSpans = a.getString(attr);
                } else if (attr == R.styleable.Grid_grid_skips) {
                    mStrSkips = a.getString(attr);
                } else if (attr == R.styleable.Grid_grid_orientation) {
                    mOrientation = a.getString(attr);
                } else if (attr == R.styleable.Grid_grid_horizontalGaps) {
                    mHorizontalGaps = a.getInteger(attr, 0);
                } else if (attr == R.styleable.Grid_grid_verticalGaps) {
                    mVerticalGaps = a.getInteger(attr, 0);
                } else if (attr == R.styleable.Grid_grid_validateInputs) {
                    // @TODO handle validation
                    mValidateInputs = a.getBoolean(attr, false);
                }  else if (attr == R.styleable.Grid_grid_useRtl) {
                    // @TODO handle RTL
                    mUseRtl = a.getBoolean(attr, false);
                }
            }

            initVariables();
            a.recycle();
        }
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();

        mContainer = (ConstraintLayout) getParent();
        mConstraintSet.clone(mContainer);
        createGuidelines(mRows, mColumns);

        if (mStrSkips != null && !mStrSkips.trim().isEmpty()) {
            HashMap<Integer, Pair<Integer, Integer>> mSkipMap = parseSkips(mStrSkips);
            if (mSkipMap != null) {
                handleSkips(mSkipMap);
            }
        }

        if (mStrSpans != null && !mStrSpans.trim().isEmpty()) {
            Span[] mSpans = parseSpans(mStrSpans);
            if (mSpans != null) {
                handleSpans(mSpans);
            }
        }

        arrangeWidgets();
    }

    /**
     * Initialize the relevant variables
     */
    private void initVariables() {
        mPositionMatrix = new boolean[mRows][mColumns];
        for (boolean[] row: mPositionMatrix) {
            Arrays.fill(row, true);
        }

        mHorizontalGuideLines = new Guideline[mRows + 1];
        mVerticalGuideLines = new Guideline[mColumns + 1];
    }

    /**
     * create vertical and horizontal guidelines based on mRows and mColumns
     * @param rows number of rows is required for grid
     * @param columns number of columns is required for grid
     */
    private void createGuidelines(int rows, int columns) {

        float[] horizontalPositions = getLinspace(0, 1, rows + 1);
        float[] verticalPositions = getLinspace(0, 1, columns + 1);

        for (int i = 0; i < mHorizontalGuideLines.length; i++) {
            mHorizontalGuideLines[i] = getNewGuideline(myContext,
                    ConstraintLayout.LayoutParams.HORIZONTAL, horizontalPositions[i]);
            mContainer.addView(mHorizontalGuideLines[i]);
        }
        for (int i = 0; i < mVerticalGuideLines.length; i++) {
            mVerticalGuideLines[i] = getNewGuideline(myContext,
                    ConstraintLayout.LayoutParams.VERTICAL, verticalPositions[i]);
            mContainer.addView(mVerticalGuideLines[i]);
        }
    }

    /**
     * get a new Guideline based on the specified orientation and position
     * @param context the context
     * @param orientation orientation of a Guideline
     * @param position position of a Guideline
     * @return a Guideline
     */
    private Guideline getNewGuideline(Context context, int orientation, float position) {
        Guideline guideline = new Guideline(context);
        guideline.setId(ViewCompat.generateViewId());
        ConstraintLayout.LayoutParams lp =
                new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.WRAP_CONTENT,
                        ConstraintLayout.LayoutParams.WRAP_CONTENT);
        lp.orientation = orientation;
        lp.guidePercent = position;
        guideline.setLayoutParams(lp);

        return guideline;
    }

    /**
     * Connect the view to the corresponding guidelines based on the input params
     * @param viewId the Id of the view
     * @param row row position to place the view
     * @param column column position to place the view
     * @param gravity gravity info, including  top, left, bottom, right, guideline,start,end
     */
    private void connectView(int viewId, int row, int column, int rowSpan, int columnSpan,
                             int horizontalGaps, int verticalGaps, String gravity) {

        // @TODO handle RTL
        // connect Start of the view
        mConstraintSet.connect(viewId, ConstraintSet.START,
                mVerticalGuideLines[column].getId(), ConstraintSet.END, horizontalGaps);

        // connect Top of the view
        mConstraintSet.connect(viewId, ConstraintSet.TOP,
                mHorizontalGuideLines[row].getId(), ConstraintSet.BOTTOM, verticalGaps);

        // connect End of the view
        mConstraintSet.connect(viewId, ConstraintSet.END,
                mVerticalGuideLines[column + columnSpan].getId(),
                ConstraintSet.START, horizontalGaps);

        // connect Bottom of the view
        mConstraintSet.connect(viewId, ConstraintSet.BOTTOM,
                mHorizontalGuideLines[row + rowSpan].getId(),
                ConstraintSet.TOP, verticalGaps);

        // handle gravity
        if (!gravity.trim().equals("")) {
            handleGravity(viewId, gravity);
        }

        mConstraintSet.applyTo(mContainer);
    }

    /**
     * Arrange the views in the constraint_referenced_ids
     * @return true if all the widgets can be arranged properly else false
     */
    private boolean arrangeWidgets() {
        Pair<Integer, Integer> position;

        // @TODO handle RTL
        for (int i = 0; i < mCount; i++) {
            if (mSpanIds.contains(mIds[i])) {
                // skip the viewId that's already handled by handleSpans
                continue;
            }

            position = getNextPosition();
            if (position.first == -1) {
                // no more available position.
                return false;
            }
            connectView(mIds[i], position.first, position.second,
                    1, 1, mHorizontalGaps, mVerticalGaps, "");
        }
        return true;
    }

    /**
     * Convert a 1D index to a 2D index that has index for row and index for column
     * @param index index in 1D
     * @return a Pair with row and column as its values.
     */
    private Pair<Integer, Integer> getPositionByIndex(int index) {
        // @TODO handle RTL
        int row;
        int col;

        if (mOrientation.equals(VERTICAL)) {
            row = index % mRows;
            col = index / mRows;
        } else {
            row = index / mColumns;
            col = index % mColumns;
        }
        return new Pair<>(row, col);
    }

    /**
     * Get the next available position for widget arrangement.
     * @return Pair<row, column>
     */
    private Pair<Integer, Integer> getNextPosition() {
        Pair<Integer, Integer> position = new Pair<>(0, 0);
        boolean positionFound = false;

        while (!positionFound) {
            if (mNextAvailableIndex >= mRows * mColumns) {
                return new Pair<>(-1,  -1);
            }

            position = getPositionByIndex(mNextAvailableIndex);

            if (mPositionMatrix[position.first][position.second]) {
                mPositionMatrix[position.first][position.second] = false;
                positionFound = true;
            }

            mNextAvailableIndex++;
        }
        return new Pair<>(position.first, position.second);
    }

    /**
     * Handle the gravity. The value could be t, r, b, l, s, e, tl, br, etc.
     * t = top, r = right, b = bottom l = left, s = start, e = end
     * @param viewId the id of a view
     * @param gravity the gravity
     */
    private void handleGravity(int viewId, String gravity) {
        for (int i = 0; i < gravity.length(); i++) {
            // @TODO handle RTL
            switch (gravity.charAt(i)) {
                case 't':
                    mConstraintSet.setVerticalBias(viewId, 0);
                    break;
                case 'r':
                    mConstraintSet.setHorizontalBias(viewId, 1);
                    break;
                case 'b':
                    mConstraintSet.setVerticalBias(viewId, 1);
                    break;
                case 'l':
                    mConstraintSet.setHorizontalBias(viewId, 0);
                    break;
                case 's':
                    mConstraintSet.setHorizontalBias(viewId, 0);
                    break;
                case 'e':
                    mConstraintSet.setHorizontalBias(viewId, 1);
                    break;
                default:
                    Log.w(TAG, "unknown gravity value: " + gravity.charAt(i));
            }
        }
    }

    /**
     * Check if the value of the Spans is valid
     * @param mStrSpans spans in string format
     * @return true if it is valid else false
     */
    private boolean isSpansValid(String mStrSpans) {
        // TODO: check string has a valid format.
        return true;
    }

    /**
     * Parse the spans in the string format into a span object
     * the format of a span is viewId|index:rowSpanxcolumnSpan-gravity
     * viewID - The id of a view in the constraint_referenced_ids list
     * index - the index of the starting position
     * row_span - The number of rows to span
     * col_span- The number of columns to span
     * gravity (optional) - letters t, l, b, r, s ,e = top, left, bottom, right, start, end.
     *  Two letters could be used together (e.g., tl, br, etc.)
     * @param strSpans Grid spans in the string format
     * @return a HashMap contains span information of individual views.
     */
    private Span[] parseSpans(String strSpans) {
        if (!isSpansValid(strSpans)) {
            return null;
        }

        String[] spans = strSpans.split(",");
        Span[] spanArray = new Span[spans.length];

        for (int i = 0; i < spans.length; i++) {
            String[] idAndRest = spans[i].trim().split(":");
            String[] startPositionAndRest = idAndRest[1].split("#");
            String[] rowSpanAndRest = startPositionAndRest[1].split("x");
            String[] colSpanAndGravity = rowSpanAndRest[1].split("-");

            int id = findId(mContainer, idAndRest[0]);
            Pair<Integer, Integer> startPosition =
                    getPositionByIndex(Integer.parseInt(startPositionAndRest[0]));
            int rowSpan = Integer.parseInt(rowSpanAndRest[0]);
            int columnSpan = Integer.parseInt(colSpanAndGravity[0]);
            String gravity = colSpanAndGravity.length > 1 ? colSpanAndGravity[1] : "";

            spanArray[i] = new Span(id, startPosition.first, startPosition.second,
                    rowSpan, columnSpan, gravity);
        }
        return spanArray;
    }

    /**
     * Handle the span use cases
     * @param spans a array of span object
     * @return true if the input spans is valid else false
     */
    private boolean handleSpans(Span[] spans) {
        for (Span span : spans) {
            if (!invalidatePositions(span.mStartRow, span.mStartColumn,
                    span.mRowSpan, span.mColumnSpan)) {
                // Try to place the widget to the skipped space
                return false;
            }
            connectView(span.mId, span.mStartRow, span.mStartColumn, span.mRowSpan,
                    span.mColumnSpan, mHorizontalGaps, mVerticalGaps, span.mGravity);
            mSpanIds.add(span.mId);
        }
        return true;
    }

    /**
     * Check if the value of the skips is valid
     * @param mStrSkips skips in string format
     * @return true if it is valid else false
     */
    private boolean isSkipsValid(String mStrSkips) {
        // TODO: check string has a valid format.
        return true;
    }

    /**
     * parse the skips in the string format into a HashMap<index, row_span, col_span>>
     * the format of the input string is index:row_spanxcol_span.
     * index - the index of the starting position
     * row_span - the number of rows to span
     * col_span- the number of columns to span
     * @param strSkips string format of skips
     * @return a hashmap that contains skip information.
     */
    private HashMap<Integer, Pair<Integer, Integer>> parseSkips(String strSkips) {
        // TODO: check string has a valid format.
        if (!isSkipsValid(strSkips)) {
            return null;
        }

        HashMap<Integer, Pair<Integer, Integer>> skipMap = new HashMap<>();

        String[] skips = strSkips.split(",");
        String[] indexAndSpan;
        String[] rowAndCol;
        for (String skip: skips) {
            indexAndSpan = skip.trim().split(":");
            rowAndCol = indexAndSpan[1].split("x");
            skipMap.put(Integer.parseInt(indexAndSpan[0]),
                    new Pair<>(Integer.parseInt(rowAndCol[0]), Integer.parseInt(rowAndCol[1])));
        }
        return skipMap;
    }

    /**
     * Make positions in the grid unavailable based on the skips attr
     * @param skipsMap a hashmap that contains skip information
     * @return true if all the skips are valid else false
     */
    private boolean handleSkips(HashMap<Integer, Pair<Integer, Integer>> skipsMap) {
        Pair<Integer, Integer> startPosition;
        for (Map.Entry<Integer, Pair<Integer, Integer>> entry : skipsMap.entrySet()) {
            startPosition = getPositionByIndex(entry.getKey());
            if (!invalidatePositions(startPosition.first, startPosition.second,
                     entry.getValue().first, entry.getValue().second)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Make the specified positions in the grid unavailable.
     * @param startRow the row of the staring position
     * @param startColumn the column of the staring position
     * @param rowSpan how many rows to span
     * @param columnSpan how many columns to span
     * @return true if we could properly invalidate the positions esle false
     */
    private boolean invalidatePositions(int startRow, int startColumn,
                                        int rowSpan, int columnSpan) {
        for (int i = startRow; i < startRow + rowSpan; i++) {
            for (int j = startColumn; j < startColumn + columnSpan; j++) {
                if (i >= mPositionMatrix.length || j >= mPositionMatrix[0].length
                        || !mPositionMatrix[i][j]) {
                    // the position is already occupied.
                    return false;
                }
                mPositionMatrix[i][j] = false;
            }
        }
        return true;
    }

    // From ConstraintHelper -> move to a util function
    /**
     * Iterate through the container's children to find a matching id.
     * Slow path, seems necessary to handle dynamic modules resolution...
     *
     * @param container the parent container - a ConstraintLayout in this case
     * @param idString the string format of a view Id
     * @return the actual viewId in Integer
     */
    private int findId(ConstraintLayout container, String idString) {
        if (idString == null || container == null) {
            return 0;
        }
        Resources resources = myContext.getResources();
        if (resources == null) {
            return 0;
        }
        final int count = container.getChildCount();
        for (int j = 0; j < count; j++) {
            View child = container.getChildAt(j);
            if (child.getId() != -1) {
                String res = null;
                try {
                    res = resources.getResourceEntryName(child.getId());
                } catch (android.content.res.Resources.NotFoundException e) {
                    // nothing
                }
                if (idString.equals(res)) {
                    return child.getId();
                }
            }
        }
        return 0;
    }

    /**
     * Generate linearly spaced positions (for the Guideline positioning)
     * @param min min value of the linear spaced positions
     * @param max max value of the linear spaced positions
     * @param positions number of positions in the space
     * @return an float array of the corresponding positions
     */
    private float[] getLinspace(float min, float max, int positions) {
        float[] d = new float[positions];
        for (int i = 0; i < positions; i++) {
            d[i] = min + i * (max - min) / (positions - 1);
        }
        return d;
    }

    /**
     * get the string value of spans
     * @return the string value of spans
     */
    public String getStrSpans() {
        return mStrSpans;
    }

    /**
     * get the string value of skips
     * @return the string value of skips
     */
    public String getStrSkips() {
        return mStrSkips;
    }
}