Grid.java

/*
 * 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.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.constraintlayout.widget.R;
import androidx.constraintlayout.widget.VirtualLayout;

import java.util.Arrays;
import java.util.HashSet;
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_rowWeights</td>
 *     <td>Specifies the weight of each row in the grid form (default value is 1).</td>
 *   </tr>
 *   <tr>
 *     <td>grid_columnWeights</td>
 *     <td>Specifies the weight of each column in the grid form (default value is 1).</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";
    public static final int VERTICAL = 1;
    public static final int HORIZONTAL = 0;
    private static final boolean DEBUG_BOXES = false;
    private final int mMaxRows = 50; // maximum number of rows can be specified.
    private final int mMaxColumns = 50; // maximum number of columns can be specified.
    // private final ConstraintSet mConstraintSet = new ConstraintSet();

    private View[] mBoxViews;
    ConstraintLayout mContainer;

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

    /**
     * number of rows set by the XML or API
     */
    private int mRowsSet;

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

    /**
     * number of columns set by the XML or API
     */
    private int mColumnsSet;

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

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

    /**
     * string format of the row weight
     */
    private String mStrRowWeights;

    /**
     * string format of the column weight
     */
    private String mStrColumnWeights;

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

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

    /**
     * orientation of the view arrangement - vertical or horizontal
     */
    private int 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
     */
    @SuppressWarnings("unused")
    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<>();

    /**
     * Ids of the boxViews
     */
    private int[] mBoxViewIds;

    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);
        mUseViewMeasure = true;

        // 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) {
                    mRowsSet = a.getInteger(attr, 0);
                } else if (attr == R.styleable.Grid_grid_columns) {
                    mColumnsSet = a.getInteger(attr, 0);
                } 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_rowWeights) {
                    mStrRowWeights = a.getString(attr);
                } else if (attr == R.styleable.Grid_grid_columnWeights) {
                    mStrColumnWeights = a.getString(attr);
                } else if (attr == R.styleable.Grid_grid_orientation) {
                    mOrientation = a.getInt(attr, 0);
                } else if (attr == R.styleable.Grid_grid_horizontalGaps) {
                    mHorizontalGaps = a.getDimension(attr, 0);
                } else if (attr == R.styleable.Grid_grid_verticalGaps) {
                    mVerticalGaps = a.getDimension(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);
                }
            }

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

    /**
     * Compute the actual rows and columns given what was set
     * if 0,0 find the most square rows and columns that fits
     * if 0,n or n,0 scale to fit
     */
    private void updateActualRowsAndColumns() {
        if (mRowsSet == 0 || mColumnsSet == 0) {
            if (mColumnsSet > 0) {
                mColumns = mColumnsSet;
                mRows = (mCount + mColumns - 1) / mColumnsSet; // round up
            } else if (mRowsSet > 0) {
                mRows = mRowsSet;
                mColumns = (mCount + mRowsSet - 1) / mRowsSet; // round up
            } else { // as close to square as possible favoring more rows
                mRows = (int) (1.5 + Math.sqrt(mCount));
                mColumns = (mCount + mRows - 1) / mRows;
            }
        } else {
            mRows = mRowsSet;
            mColumns = mColumnsSet;
        }
    }

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

        mContainer = (ConstraintLayout) getParent();

        generateGrid(false);
    }

    /**
     * generate the Grid form based on the input attributes
     *
     * @param isUpdate whether to update the existing grid (true) or create a new one (false)
     * @return true if all the inputs are valid else false
     */
    private boolean generateGrid(boolean isUpdate) {
        if (mContainer == null || mRows < 1 || mColumns < 1) {
            return false;
        }

        if (isUpdate) {
            for (int i = 0; i < mPositionMatrix.length; i++) {
                for (int j = 0; j < mPositionMatrix[0].length; j++) {
                    mPositionMatrix[i][j] = true;
                }
            }
            mSpanIds.clear();
        }

        mNextAvailableIndex = 0;
        boolean isSuccess = true;

        buildBoxes();

        if (mStrSkips != null && !mStrSkips.trim().isEmpty()) {
            int[][] mSkips = parseSpans(mStrSkips);
            if (mSkips != null) {
                isSuccess &= handleSkips(mSkips);
            }
        }

        if (mStrSpans != null && !mStrSpans.trim().isEmpty()) {
            int[][] mSpans = parseSpans(mStrSpans);
            if (mSpans != null) {
                isSuccess &= handleSpans(mIds, mSpans);
            }
        }
        isSuccess &= arrangeWidgets();
        return isSuccess || !mValidateInputs;
    }

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

    /**
     * parse the weights/pads in the string format into a float array
     *
     * @param size size of the return array
     * @param str  weights/pads in a string format
     * @return a float array with weights/pads values
     */
    private float[] parseWeights(int size, String str) {
        if (str == null || str.trim().isEmpty()) {
            return null;
        }

        String[] values = str.split(",");
        if (values.length != size) {
            return null;
        }

        float[] arr = new float[size];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = Float.parseFloat(values[i].trim());
        }
        return arr;
    }

    private ConstraintLayout.LayoutParams params(View v) {
        return (ConstraintLayout.LayoutParams) v.getLayoutParams();
    }

    /**
     * Connect the view to the corresponding viewBoxes based on the input params
     *
     * @param view   the Id of the view
     * @param row    row position to place the view
     * @param column column position to place the view
     */

    private void connectView(View view, int row, int column, int rowSpan, int columnSpan) {
        ConstraintLayout.LayoutParams params = params(view);
        // @TODO handle RTL
        // Connect the 4 sides
        params.leftToLeft = mBoxViewIds[column];
        params.topToTop = mBoxViewIds[row];
        params.rightToRight = mBoxViewIds[column + columnSpan - 1];
        params.bottomToBottom = mBoxViewIds[row + rowSpan - 1];
        view.setLayoutParams(params);
    }

    /**
     * Arrange the views in the constraint_referenced_ids
     *
     * @return true if all the widgets can be arranged properly else false
     */
    private boolean arrangeWidgets() {
        int position;
        View[] views = getViews(mContainer);
        // @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();
            int row = getRowByIndex(position);
            int col = getColByIndex(position);
            if (position == -1) {
                // no more available position.
                return false;
            }

            connectView(views[i], row, col, 1, 1);
        }
        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 row as its values.
     */
    private int getRowByIndex(int index) {
        if (mOrientation == 1) {
            return index % mRows;
        } else {
            return index / mColumns;
        }
    }

    /**
     * Convert a 1D index to a 2D index that has index for row and index for column
     *
     * @param index index in 1D
     * @return column as its values.
     */
    private int getColByIndex(int index) {
        if (mOrientation == 1) {
            return index / mRows;
        } else {
            return index % mColumns;
        }
    }

    /**
     * Get the next available position for widget arrangement.
     *
     * @return int[] -> [row, column]
     */
    private int getNextPosition() {
        //  int[] position = new int[] {0, 0};
        int position = 0;
        boolean positionFound = false;

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

            // position = getPositionByIndex(mNextAvailableIndex);
            position = mNextAvailableIndex;
            int row = getRowByIndex(mNextAvailableIndex);
            int col = getColByIndex(mNextAvailableIndex);
            if (mPositionMatrix[row][col]) {
                mPositionMatrix[row][col] = false;
                positionFound = true;
            }

            mNextAvailableIndex++;
        }
        return position;
    }

    /**
     * Check if the value of the spans/skips is valid
     *
     * @param str spans/skips in string format
     * @return true if it is valid else false
     */
    private boolean isSpansValid(@SuppressWarnings("unused") CharSequence str) {
        // TODO: check string has a valid format.
        return true;
    }

    /**
     * Check if the value of the rowWeights or columnsWeights is valid
     *
     * @param str rowWeights/columnsWeights in string format
     * @return true if it is valid else false
     */
    private boolean isWeightsValid(@SuppressWarnings("unused") String str) {
        // TODO: check string has a valid format.
        return true;
    }

    /**
     * parse the skips/spans in the string format into a int matrix
     * that each row has the information - [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 str string format of skips or spans
     * @return a int matrix that contains skip information.
     */
    private int[][] parseSpans(String str) {
        if (!isSpansValid(str)) {
            return null;
        }

        String[] spans = str.split(",");
        int[][] spanMatrix = new int[spans.length][3];

        String[] indexAndSpan;
        String[] rowAndCol;
        for (int i = 0; i < spans.length; i++) {
            indexAndSpan = spans[i].trim().split(":");
            rowAndCol = indexAndSpan[1].split("x");
            spanMatrix[i][0] = Integer.parseInt(indexAndSpan[0]);
            spanMatrix[i][1] = Integer.parseInt(rowAndCol[0]);
            spanMatrix[i][2] = Integer.parseInt(rowAndCol[1]);
        }
        return spanMatrix;
    }

    /**
     * Handle the span use cases
     *
     * @param spansMatrix a int matrix that contains span information
     * @return true if the input spans is valid else false
     */
    private boolean handleSpans(int[] mId, int[][] spansMatrix) {
        View[] views = getViews(mContainer);
        for (int i = 0; i < spansMatrix.length; i++) {
            int row = getRowByIndex(spansMatrix[i][0]);
            int col = getColByIndex(spansMatrix[i][0]);
            if (!invalidatePositions(row, col,
                    spansMatrix[i][1], spansMatrix[i][2])) {
                return false;
            }

            connectView(views[i], row, col,
                    spansMatrix[i][1], spansMatrix[i][2]);

            mSpanIds.add(mId[i]);
        }
        return true;
    }

    /**
     * Make positions in the grid unavailable based on the skips attr
     *
     * @param skipsMatrix a int matrix that contains skip information
     * @return true if all the skips are valid else false
     */
    private boolean handleSkips(int[][] skipsMatrix) {
        for (int i = 0; i < skipsMatrix.length; i++) {
            int row = getRowByIndex(skipsMatrix[i][0]);
            int col = getColByIndex(skipsMatrix[i][0]);
            if (!invalidatePositions(row, col,
                    skipsMatrix[i][1], skipsMatrix[i][2])) {
                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 else 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;
    }

    /**
     * Visualize the boxViews that are used to constraint widgets.
     *
     * @param canvas canvas to visualize the boxViews
     */
    @Override
    public void onDraw(@NonNull Canvas canvas) {
        super.onDraw(canvas);
        // Visualize the viewBoxes if isInEditMode() is true
        if (!isInEditMode()) {
            return;
        }
        @SuppressLint("DrawAllocation")
        Paint paint = new Paint(); // used only during design time
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        int myTop = getTop();
        int myLeft = getLeft();
        int myBottom = getBottom();
        int myRight = getRight();
        for (View box : mBoxViews) {
            int l = box.getLeft() - myLeft;
            int t = box.getTop() - myTop;
            int r = box.getRight() - myLeft;
            int b = box.getBottom() - myTop;
            canvas.drawRect(l, 0, r, myBottom - myTop, paint);
            canvas.drawRect(0, t, myRight - myLeft, b, paint);
        }
    }

    /**
     * Set chain between boxView horizontally
     */
    private void setBoxViewHorizontalChains() {
        int gridId = getId();
        int maxVal = Math.max(mRows, mColumns);

        float[] columnWeights = parseWeights(mColumns, mStrColumnWeights);
        ConstraintLayout.LayoutParams params = params(mBoxViews[0]);
        // chain all the views on the longer side (either horizontal or vertical)
        if (mColumns == 1) {
            clearHParams(mBoxViews[0]);
            params.leftToLeft = gridId;
            params.rightToRight = gridId;
            mBoxViews[0].setLayoutParams(params);
            return;
        }


        //  chains are grid <- box <-> box <-> box -> grid

        for (int i = 0; i < mColumns; i++) {
            params = params(mBoxViews[i]);
            clearHParams(mBoxViews[i]);
            if (columnWeights != null) {
                params.horizontalWeight = columnWeights[i];
            }
            if (i > 0) {
                params.leftToRight = mBoxViewIds[i - 1];
            } else {
                params.leftToLeft = gridId;
            }
            if (i < mColumns - 1) {
                params.rightToLeft = mBoxViewIds[i + 1];
            } else {
                params.rightToRight = gridId;
            }
            if (i > 0) {
                params.leftMargin = (int) mHorizontalGaps;
            }
            mBoxViews[i].setLayoutParams(params);
        }
        // excess boxes are connected to grid those sides are not use
        // for efficiency they should be connected to parent
        for (int i = mColumns; i < maxVal; i++) {
            params = params(mBoxViews[i]);
            clearHParams(mBoxViews[i]);
            params.leftToLeft = gridId;
            params.rightToRight = gridId;
            mBoxViews[i].setLayoutParams(params);
        }
    }

    /**
     * Set chain between boxView vertically
     */
    private void setBoxViewVerticalChains() {
        int gridId = getId();
        int maxVal = Math.max(mRows, mColumns);

        float[] rowWeights = parseWeights(mRows, mStrRowWeights);
        ConstraintLayout.LayoutParams params;
        // chain all the views on the longer side (either horizontal or vertical)
        if (mRows == 1) {
            params = params(mBoxViews[0]);
            clearVParams(mBoxViews[0]);
            params.topToTop = gridId;
            params.bottomToBottom = gridId;
            mBoxViews[0].setLayoutParams(params);
            return;
        }
        // chains are constrained like this: grid <- box <-> box <-> box -> grid
        for (int i = 0; i < mRows; i++) {
            params = params(mBoxViews[i]);
            clearVParams(mBoxViews[i]);
            if (rowWeights != null) {
                params.verticalWeight = rowWeights[i];
            }
            if (i > 0) {
                params.topToBottom = mBoxViewIds[i - 1];
            } else {
                params.topToTop = gridId;
            }
            if (i < mRows - 1) {
                params.bottomToTop = mBoxViewIds[i + 1];
            } else {
                params.bottomToBottom = gridId;
            }
            if (i > 0) {
                params.topMargin = (int) mHorizontalGaps;
            }
            mBoxViews[i].setLayoutParams(params);
        }

        // excess boxes are connected to grid those sides are not use
        // for efficiency they should be connected to parent
        for (int i = mRows; i < maxVal; i++) {
            params = params(mBoxViews[i]);
            clearVParams(mBoxViews[i]);
            params.topToTop = gridId;
            params.bottomToBottom = gridId;
            mBoxViews[i].setLayoutParams(params);
        }
    }

    /**
     * Create a new boxView
     * @return boxView
     */
    private View makeNewView() {
        View v = new View(getContext());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            v.setId(View.generateViewId());
        }
        v.setVisibility(INVISIBLE);
        if (DEBUG_BOXES) {
            v.setVisibility(VISIBLE);
            v.setBackgroundColor(0xFF880088);
        }
        ConstraintLayout.LayoutParams params =
                new ConstraintLayout.LayoutParams(0, 0);

        mContainer.addView(v, params);
        return v;
    }

    /**
     * Clear vertical related layout params
     * @param view view that has the layout params to be cleared
     */
    private void clearVParams(View view) {
        ConstraintLayout.LayoutParams params = params(view);

        params.verticalWeight  = ConstraintSet.UNSET;
        params.topToBottom = ConstraintSet.UNSET;
        params.topToTop = ConstraintSet.UNSET;
        params.bottomToTop = ConstraintSet.UNSET;
        params.bottomToBottom  = ConstraintSet.UNSET;
        params.topMargin = ConstraintSet.UNSET;

        view.setLayoutParams(params);
    }

    /**
     * Clear horizontal related layout params
     * @param view view that has the layout params to be cleared
     */
    private void clearHParams(View view) {
        ConstraintLayout.LayoutParams params = params(view);

        params.horizontalWeight = ConstraintSet.UNSET;
        params.leftToRight = ConstraintSet.UNSET;
        params.leftToLeft = ConstraintSet.UNSET;
        params.rightToLeft  = ConstraintSet.UNSET;
        params.rightToRight = ConstraintSet.UNSET;
        params.leftMargin  = ConstraintSet.UNSET;

        view.setLayoutParams(params);
    }

    /**
     * create boxViews for constraining widgets
     */
    private void buildBoxes() {
        int boxCount = Math.max(mRows, mColumns);
        if (mBoxViews == null) { // no box views build all
            mBoxViews = new View[boxCount];
            for (int i = 0; i < mBoxViews.length; i++) {
                mBoxViews[i] = makeNewView(); // need to remove old Views
            }
        } else {
            if (boxCount != mBoxViews.length) {
                View[] temp = new View[boxCount];
                for (int i = 0; i < boxCount; i++) {
                    if (i < mBoxViews.length) { // use old one
                        temp[i] = mBoxViews[i];
                    } else { // make new one
                        temp[i] = makeNewView();
                    }
                }
                // remove excess
                for (int j = boxCount; j < mBoxViews.length; j++) {
                    View view = mBoxViews[j];
                    mContainer.removeView(view);
                }
                mBoxViews = temp;
            }
        }

        mBoxViewIds = new int[boxCount];
        for (int i = 0; i < mBoxViews.length; i++) {
            mBoxViewIds[i] = mBoxViews[i].getId();
        }

        setBoxViewVerticalChains();
        setBoxViewHorizontalChains();
    }

    /**
     * get the value of rows
     *
     * @return the value of rows
     */
    public int getRows() {
        return mRowsSet;
    }

    /**
     * set new rows value and also invoke initVariables and invalidate
     *
     * @param rows new rows value
     */
    public void setRows(int rows) {
        if (rows > mMaxRows) {
            return;
        }

        if (mRowsSet == rows) {
            return;
        }

        mRowsSet = rows;
        updateActualRowsAndColumns();

        initVariables();
        generateGrid(false);
        invalidate();
    }

    /**
     * get the value of columns
     *
     * @return the value of columns
     */
    public int getColumns() {
        return mColumnsSet;
    }

    /**
     * set new columns value and also invoke initVariables and invalidate
     *
     * @param columns new rows value
     */
    public void setColumns(int columns) {
        if (columns > mMaxColumns) {
            return;
        }

        if (mColumnsSet == columns) {
            return;
        }

        mColumnsSet = columns;
        updateActualRowsAndColumns();

        initVariables();
        generateGrid(false);
        invalidate();
    }

    /**
     * get the value of orientation
     *
     * @return the value of orientation
     */
    public int getOrientation() {
        return mOrientation;
    }

    /**
     * set new orientation value and also invoke invalidate
     *
     * @param orientation new orientation value
     */
    public void setOrientation(int orientation) {
        if (!(orientation == HORIZONTAL || orientation == VERTICAL)) {
            return;
        }

        if (mOrientation == orientation) {
            return;
        }

        mOrientation = orientation;
        generateGrid(true);
        invalidate();
    }

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

    /**
     * set new spans value and also invoke invalidate
     *
     * @param spans new spans value
     */
    public void setSpans(CharSequence spans) {
        if (!isSpansValid(spans)) {
            return;
        }

        if (mStrSpans != null && mStrSpans.contentEquals(spans)) {
            return;
        }

        mStrSpans = spans.toString();
        generateGrid(true);
        invalidate();
    }

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

    /**
     * set new skips value and also invoke invalidate
     *
     * @param skips new spans value
     */
    public void setSkips(String skips) {
        if (!isSpansValid(skips)) {
            return;
        }

        if (mStrSkips != null && mStrSkips.equals(skips)) {
            return;
        }

        mStrSkips = skips;
        generateGrid(true);
        invalidate();
    }

    /**
     * get the string value of rowWeights
     *
     * @return the string value of rowWeights
     */
    public String getRowWeights() {
        return mStrRowWeights;
    }

    /**
     * set new rowWeights value and also invoke invalidate
     *
     * @param rowWeights new rowWeights value
     */
    public void setRowWeights(String rowWeights) {
        if (!isWeightsValid(rowWeights)) {
            return;
        }

        if (mStrRowWeights != null && mStrRowWeights.equals(rowWeights)) {
            return;
        }

        mStrRowWeights = rowWeights;
        generateGrid(true);
        invalidate();
    }

    /**
     * get the string value of columnWeights
     *
     * @return the string value of columnWeights
     */
    public String getColumnWeights() {
        return mStrColumnWeights;
    }

    /**
     * set new columnWeights value and also invoke invalidate
     *
     * @param columnWeights new columnWeights value
     */
    public void setColumnWeights(String columnWeights) {
        if (!isWeightsValid(columnWeights)) {
            return;
        }

        if (mStrColumnWeights != null && mStrColumnWeights.equals(columnWeights)) {
            return;
        }

        mStrColumnWeights = columnWeights;
        generateGrid(true);
        invalidate();
    }

    /**
     * get the value of horizontalGaps
     *
     * @return the value of horizontalGaps
     */
    public float getHorizontalGaps() {
        return mHorizontalGaps;
    }

    /**
     * set new horizontalGaps value and also invoke invalidate
     *
     * @param horizontalGaps new horizontalGaps value
     */
    public void setHorizontalGaps(float horizontalGaps) {
        if (horizontalGaps < 0) {
            return;
        }

        if (mHorizontalGaps == horizontalGaps) {
            return;
        }

        mHorizontalGaps = horizontalGaps;
        generateGrid(true);
        invalidate();
    }

    /**
     * get the value of verticalGaps
     *
     * @return the value of verticalGaps
     */
    public float getVerticalGaps() {
        return mVerticalGaps;
    }

    /**
     * set new verticalGaps value and also invoke invalidate
     *
     * @param verticalGaps new verticalGaps value
     */
    public void setVerticalGaps(float verticalGaps) {
        if (verticalGaps < 0) {
            return;
        }

        if (mVerticalGaps == verticalGaps) {
            return;
        }

        mVerticalGaps = verticalGaps;
        generateGrid(true);
        invalidate();
    }
}