From 4618d931b5eaf84fe2401600f35db8d71039e87e Mon Sep 17 00:00:00 2001 From: "Jian-Syuan (Shane) Wong" Date: Mon, 23 May 2022 14:54:01 -0700 Subject: [PATCH] grid helper improvement part1 --- .../constraintlayout/helper/widget/Grid.java | 375 +++++++----------- .../src/main/res/values/attrs.xml | 2 + .../app/src/main/res/layout/activity_main.xml | 1 + .../app/src/main/res/layout/calculator.xml | 4 +- .../app/src/main/res/layout/dialer.xml | 4 +- 5 files changed, 161 insertions(+), 225 deletions(-) diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/Grid.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/Grid.java index b93ca34f4..6495df7c2 100644 --- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/Grid.java +++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/helper/widget/Grid.java @@ -16,12 +16,9 @@ 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; @@ -53,6 +50,14 @@ * Indicates the number of columns will be created for the grid form. * * + * grid_rowWeights + * Specifies the weight of each row in the grid form (default value is 1). + * + * + * grid_columnWeights + * Specifies the weight of each column in the grid form (default value is 1). + * + * * grid_spans * Offers the capability to span a widget across multiple rows and columns * @@ -110,6 +115,16 @@ public class Grid extends VirtualLayout { */ 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 */ @@ -152,52 +167,6 @@ public class Grid extends VirtualLayout { */ Set 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); } @@ -230,7 +199,11 @@ protected void init(AttributeSet attrs) { mStrSpans = a.getString(attr); } else if (attr == R.styleable.Grid_grid_skips) { mStrSkips = a.getString(attr); - } else if (attr == R.styleable.Grid_grid_orientation) { + } 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.getString(attr); } else if (attr == R.styleable.Grid_grid_horizontalGaps) { mHorizontalGaps = a.getInteger(attr, 0); @@ -256,23 +229,37 @@ public void onAttachedToWindow() { mContainer = (ConstraintLayout) getParent(); mConstraintSet.clone(mContainer); + + generateGrid(); + } + + /** + * generate the Grid form based on the input attributes + * @return true if all the inputs are valid else false + */ + private boolean generateGrid() { + boolean isSuccess = true; + createGuidelines(mRows, mColumns); if (mStrSkips != null && !mStrSkips.trim().isEmpty()) { - HashMap> mSkipMap = parseSkips(mStrSkips); + HashMap> mSkipMap = parseSpans(mStrSkips); if (mSkipMap != null) { - handleSkips(mSkipMap); + isSuccess &= handleSkips(mSkipMap); } } if (mStrSpans != null && !mStrSpans.trim().isEmpty()) { - Span[] mSpans = parseSpans(mStrSpans); + HashMap> mSpans = parseSpans(mStrSpans); if (mSpans != null) { - handleSpans(mSpans); + isSuccess &= handleSpans(mIds, mSpans); } } + isSuccess &= arrangeWidgets(); + + mConstraintSet.applyTo(mContainer); - arrangeWidgets(); + return isSuccess || !mValidateInputs; } /** @@ -288,15 +275,42 @@ private void initVariables() { mVerticalGuideLines = new Guideline[mColumns + 1]; } + /** + * 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; + } + /** * 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[] rowWeights = parseWeights(rows, mStrRowWeights); + float[] columnWeights = parseWeights(columns, mStrColumnWeights); - float[] horizontalPositions = getLinspace(0, 1, rows + 1); - float[] verticalPositions = getLinspace(0, 1, columns + 1); + float[] horizontalPositions = getLinePositions(0, 1, + rows + 1, rowWeights); + float[] verticalPositions = getLinePositions(0, 1, + columns + 1, columnWeights); for (int i = 0; i < mHorizontalGuideLines.length; i++) { mHorizontalGuideLines[i] = getNewGuideline(myContext, @@ -335,10 +349,9 @@ private Guideline getNewGuideline(Context context, int orientation, float positi * @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) { + int horizontalGaps, int verticalGaps) { // @TODO handle RTL // connect Start of the view @@ -358,13 +371,6 @@ private void connectView(int viewId, int row, int column, int rowSpan, int colum mConstraintSet.connect(viewId, ConstraintSet.BOTTOM, mHorizontalGuideLines[row + rowSpan].getId(), ConstraintSet.TOP, verticalGaps); - - // handle gravity - if (!gravity.trim().equals("")) { - handleGravity(viewId, gravity); - } - - mConstraintSet.applyTo(mContainer); } /** @@ -387,7 +393,7 @@ private boolean arrangeWidgets() { return false; } connectView(mIds[i], position.first, position.second, - 1, 1, mHorizontalGaps, mVerticalGaps, ""); + 1, 1, mHorizontalGaps, mVerticalGaps); } return true; } @@ -437,136 +443,33 @@ private Pair getNextPosition() { 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 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 + * @param str skips in string format * @return true if it is valid else false */ - private boolean isSkipsValid(String mStrSkips) { + private boolean isSpansValid(String str) { // TODO: check string has a valid format. return true; } /** - * parse the skips in the string format into a HashMap> + * parse the skips/spans in the string format into a HashMap> * 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 + * @param str string format of skips or spans * @return a hashmap that contains skip information. */ - private HashMap> parseSkips(String strSkips) { - // TODO: check string has a valid format. - if (!isSkipsValid(strSkips)) { + private HashMap> parseSpans(String str) { + if (!isSpansValid(str)) { return null; } HashMap> skipMap = new HashMap<>(); - String[] skips = strSkips.split(","); + String[] skips = str.split(","); String[] indexAndSpan; String[] rowAndCol; for (String skip: skips) { @@ -578,6 +481,29 @@ private HashMap> parseSkips(String strSkips) { return skipMap; } + /** + * Handle the span use cases + * @param spansMap a hashmap that contains span information + * @return true if the input spans is valid else false + */ + private boolean handleSpans(int[] mId, HashMap> spansMap) { + int mIdIndex = 0; + Pair startPosition; + for (Map.Entry> entry : spansMap.entrySet()) { + startPosition = getPositionByIndex(entry.getKey()); + if (!invalidatePositions(startPosition.first, startPosition.second, + entry.getValue().first, entry.getValue().second)) { + return false; + } + connectView(mId[mIdIndex], startPosition.first, startPosition.second, + entry.getValue().first, entry.getValue().second, + mHorizontalGaps, mVerticalGaps); + mSpanIds.add(mId[mIdIndex]); + mIdIndex++; + } + return true; + } + /** * Make positions in the grid unavailable based on the skips attr * @param skipsMap a hashmap that contains skip information @@ -618,69 +544,76 @@ private boolean invalidatePositions(int startRow, int startColumn, 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 + * Generate line positions (for the Guideline positioning) + * @param min min value of the linear spaced positions + * * @param max max value of the linear spaced positions + * @param numPositions number of positions is required + * @param weights a float array for space weights + * @return a float array of the corresponding positions */ - private int findId(ConstraintLayout container, String idString) { - if (idString == null || container == null) { - return 0; - } - Resources resources = myContext.getResources(); - if (resources == null) { - return 0; + private float[] getLinePositions(float min, float max, int numPositions, float[] weights) { + if (weights != null && numPositions - 1 != weights.length) { + return null; } - 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(); - } - } + + float[] positions = new float[numPositions]; + int weightSum = 0; + for (int i = 0; i < numPositions - 1; i++) { + weightSum += weights != null ? weights[i] : 1; } - 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); + float availableSpace = max - min; + float baseWeight = availableSpace / weightSum; + positions[0] = min; + for (int i = 0; i < numPositions - 1; i++) { + float w = weights != null ? weights[i] : 1; + positions[i + 1] = positions[i] + w * baseWeight; } - return d; + return positions; } /** * get the string value of spans * @return the string value of spans */ - public String getStrSpans() { + public String getSpans() { return mStrSpans; } + /** + * set new spans value and also invoke requestLayout + * @param spans new spans value + * @return true if it succeeds otherwise false + */ + public Boolean setSpans(String spans) { + if (!isSpansValid(spans)) { + return false; + } + mStrSpans = spans; + requestLayout(); + return true; + } + /** * get the string value of skips * @return the string value of skips */ - public String getStrSkips() { + public String getSkips() { return mStrSkips; } + + /** + * set new skips value and also invoke requestLayout + * @param skips new spans value + * @return true if it succeeds otherwise false + */ + public Boolean setSkips(String skips) { + if (!isSpansValid(skips)) { + return false; + } + mStrSkips = skips; + requestLayout(); + return true; + } } diff --git a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml index 4bd9d4004..9f2c814d3 100644 --- a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml +++ b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml @@ -308,6 +308,8 @@ + + diff --git a/projects/GridExperiments/app/src/main/res/layout/activity_main.xml b/projects/GridExperiments/app/src/main/res/layout/activity_main.xml index 6a102bb5e..0db1b3079 100644 --- a/projects/GridExperiments/app/src/main/res/layout/activity_main.xml +++ b/projects/GridExperiments/app/src/main/res/layout/activity_main.xml @@ -12,6 +12,7 @@ app:constraint_referenced_ids="btn0,btn1,btn2,btn3" app:grid_columns="3" app:grid_rows="3" + app:grid_columnWeights="3,1,2" app:grid_horizontalGaps="10" app:grid_verticalGaps="10" app:grid_orientation="horizontal" diff --git a/projects/GridExperiments/app/src/main/res/layout/calculator.xml b/projects/GridExperiments/app/src/main/res/layout/calculator.xml index de38729d2..57942ec9f 100644 --- a/projects/GridExperiments/app/src/main/res/layout/calculator.xml +++ b/projects/GridExperiments/app/src/main/res/layout/calculator.xml @@ -9,13 +9,13 @@ android:id="@+id/grid" android:layout_width="match_parent" android:layout_height="match_parent" - app:constraint_referenced_ids="clear,neg,percent,div,btn7,btn8,btn9,mult,btn4,btn5,btn6,sub,btn1,btn2,btn3,plus,btn0,dot,equal" + app:constraint_referenced_ids="calculatorText,btn0,clear,neg,percent,div,btn7,btn8,btn9,mult,btn4,btn5,btn6,sub,btn1,btn2,btn3,plus,btn0,dot,equal" app:grid_columns="4" app:grid_rows="7" app:grid_horizontalGaps="5" app:grid_verticalGaps="5" app:grid_orientation="horizontal" - app:grid_spans="calculatorText:0#2x4,btn0:24#1x2" /> + app:grid_spans="0:2x4,24:1x2" />