1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.constraintlayout.core.utils;
18 
19 import java.util.Arrays;
20 
21 /**
22  * GridEngine class contains the main logic of the Grid Helper
23  */
24 public class GridEngine {
25 
26     public static final int VERTICAL = 1;
27     public static final int HORIZONTAL = 0;
28     private static final int MAX_ROWS = 50; // maximum number of rows can be specified.
29     private static final int MAX_COLUMNS = 50; // maximum number of columns can be specified.
30     private static final int DEFAULT_SIZE = 3; // default rows and columns.
31 
32     /**
33      * number of rows of the grid
34      */
35     private int mRows;
36 
37     /**
38      * number of rows set by the XML or API
39      */
40     private int mRowsSet;
41 
42     /**
43      * How many widgets need to be placed in the Grid
44      */
45     private int mNumWidgets;
46 
47     /**
48      * number of columns of the grid
49      */
50     private int mColumns;
51 
52     /**
53      * number of columns set by the XML or API
54      */
55     private int mColumnsSet;
56 
57     /**
58      * string format of the input Spans
59      */
60     private String mStrSpans;
61 
62     /**
63      * string format of the input Skips
64      */
65     private String mStrSkips;
66 
67     /**
68      * orientation of the view arrangement - vertical or horizontal
69      */
70     private int mOrientation;
71 
72     /**
73      * Indicates what is the next available position to place an widget
74      */
75     private int mNextAvailableIndex = 0;
76 
77     /**
78      * A boolean matrix that tracks the positions that are occupied by skips and spans
79      * true: available position
80      * false: non-available position
81      */
82     private boolean[][] mPositionMatrix;
83 
84     /**
85      * A int matrix that contains the positions where a widget would constraint to at each direction
86      * Each row contains 4 values that indicate the position to constraint of a widget.
87      * Example row: [left, top, right, bottom]
88      */
89     private int[][] mConstraintMatrix;
90 
GridEngine()91     public GridEngine() {}
92 
GridEngine(int rows, int columns)93     public GridEngine(int rows, int columns) {
94         mRowsSet = rows;
95         mColumnsSet = columns;
96         if (rows > MAX_ROWS) {
97             mRowsSet = DEFAULT_SIZE;
98         }
99 
100         if (columns > MAX_COLUMNS) {
101             mColumnsSet = DEFAULT_SIZE;
102         }
103 
104         updateActualRowsAndColumns();
105         initVariables();
106     }
107 
GridEngine(int rows, int columns, int numWidgets)108     public GridEngine(int rows, int columns, int numWidgets) {
109         mRowsSet = rows;
110         mColumnsSet = columns;
111         mNumWidgets = numWidgets;
112 
113         if (rows > MAX_ROWS) {
114             mRowsSet = DEFAULT_SIZE;
115         }
116 
117         if (columns > MAX_COLUMNS) {
118             mColumnsSet = DEFAULT_SIZE;
119         }
120 
121         updateActualRowsAndColumns();
122 
123         if (numWidgets > mRows * mColumns || numWidgets < 1) {
124             mNumWidgets = mRows * mColumns;
125         }
126 
127         initVariables();
128         fillConstraintMatrix(false);
129     }
130 
131     /**
132      * Initialize the relevant variables
133      */
initVariables()134     private void initVariables() {
135         mPositionMatrix = new boolean[mRows][mColumns];
136         for (boolean[] row : mPositionMatrix) {
137             Arrays.fill(row, true);
138         }
139 
140         if (mNumWidgets > 0) {
141             mConstraintMatrix = new int[mNumWidgets][4];
142             for (int[] row : mConstraintMatrix) {
143                 Arrays.fill(row, -1);
144             }
145         }
146     }
147 
148     /**
149      * Convert a 1D index to a 2D index that has index for row and index for column
150      *
151      * @param index index in 1D
152      * @return row as its values.
153      */
getRowByIndex(int index)154     private int getRowByIndex(int index) {
155         if (mOrientation == 1) {
156             return index % mRows;
157 
158         } else {
159             return index / mColumns;
160         }
161     }
162 
163     /**
164      * Convert a 1D index to a 2D index that has index for row and index for column
165      *
166      * @param index index in 1D
167      * @return column as its values.
168      */
getColByIndex(int index)169     private int getColByIndex(int index) {
170         if (mOrientation == 1) {
171             return index / mRows;
172         } else {
173             return index % mColumns;
174         }
175     }
176 
177     /**
178      * Check if the value of the spans/skips is valid
179      *
180      * @param str spans/skips in string format
181      * @return true if it is valid else false
182      */
isSpansValid(CharSequence str)183     private boolean isSpansValid(CharSequence str) {
184         if (str == null) {
185             return false;
186         }
187         return true;
188     }
189 
190     /**
191      * parse the skips/spans in the string format into a int matrix
192      * that each row has the information - [index, row_span, col_span]
193      * the format of the input string is index:row_spanxcol_span.
194      * index - the index of the starting position
195      * row_span - the number of rows to span
196      * col_span- the number of columns to span
197      *
198      * @param str string format of skips or spans
199      * @return a int matrix that contains skip information.
200      */
parseSpans(String str)201     private int[][] parseSpans(String str) {
202         if (!isSpansValid(str)) {
203             return null;
204         }
205 
206         String[] spans = str.split(",");
207         int[][] spanMatrix = new int[spans.length][3];
208 
209         String[] indexAndSpan;
210         String[] rowAndCol;
211         for (int i = 0; i < spans.length; i++) {
212             indexAndSpan = spans[i].trim().split(":");
213             rowAndCol = indexAndSpan[1].split("x");
214             spanMatrix[i][0] = Integer.parseInt(indexAndSpan[0]);
215             spanMatrix[i][1] = Integer.parseInt(rowAndCol[0]);
216             spanMatrix[i][2] = Integer.parseInt(rowAndCol[1]);
217         }
218         return spanMatrix;
219     }
220 
221     /**
222      * fill the constraintMatrix based on the input attributes
223      *
224      * @param isUpdate whether to update the existing grid (true) or create a new one (false)
225      */
fillConstraintMatrix(boolean isUpdate)226     private void fillConstraintMatrix(boolean isUpdate) {
227         if (isUpdate) {
228             for (int i = 0; i < mPositionMatrix.length; i++) {
229                 for (int j = 0; j < mPositionMatrix[0].length; j++) {
230                     mPositionMatrix[i][j] = true;
231                 }
232             }
233 
234             for (int i = 0; i < mConstraintMatrix.length; i++) {
235                 for (int j = 0; j < mConstraintMatrix[0].length; j++) {
236                     mConstraintMatrix[i][j] = -1;
237                 }
238             }
239         }
240 
241         mNextAvailableIndex = 0;
242 
243         if (mStrSkips != null && !mStrSkips.trim().isEmpty()) {
244             int[][] mSkips = parseSpans(mStrSkips);
245             if (mSkips != null) {
246                 handleSkips(mSkips);
247             }
248         }
249 
250         if (mStrSpans != null && !mStrSpans.trim().isEmpty()) {
251             int[][] mSpans = parseSpans(mStrSpans);
252             if (mSpans != null) {
253                 handleSpans(mSpans);
254             }
255         }
256 
257         addAllConstraintPositions();
258     }
259 
260     /**
261      * Get the next available position for widget arrangement.
262      * @return int[] -> [row, column]
263      */
getNextPosition()264     private int getNextPosition() {
265         int position = 0;
266         boolean positionFound = false;
267 
268         while (!positionFound) {
269             if (mNextAvailableIndex >= mRows * mColumns) {
270                 return -1;
271             }
272 
273             position = mNextAvailableIndex;
274             int row = getRowByIndex(mNextAvailableIndex);
275             int col = getColByIndex(mNextAvailableIndex);
276             if (mPositionMatrix[row][col]) {
277                 mPositionMatrix[row][col] = false;
278                 positionFound = true;
279             }
280 
281             mNextAvailableIndex++;
282         }
283         return position;
284     }
285 
286     /**
287      * add the constraint position info of a widget based on the input params
288      *
289      * @param widgetId the Id of the widget
290      * @param row row position to place the view
291      * @param column column position to place the view
292      */
addConstraintPosition(int widgetId, int row, int column, int rowSpan, int columnSpan)293     private void addConstraintPosition(int widgetId, int row, int column,
294                                        int rowSpan, int columnSpan) {
295 
296         mConstraintMatrix[widgetId][0] = column;
297         mConstraintMatrix[widgetId][1] = row;
298         mConstraintMatrix[widgetId][2] = column + columnSpan - 1;
299         mConstraintMatrix[widgetId][3] = row + rowSpan - 1;
300     }
301 
302     /**
303      * Handle the span use cases
304      *
305      * @param spansMatrix a int matrix that contains span information
306      */
handleSpans(int[][] spansMatrix)307     private void handleSpans(int[][] spansMatrix) {
308         for (int i = 0; i < spansMatrix.length; i++) {
309             int row = getRowByIndex(spansMatrix[i][0]);
310             int col = getColByIndex(spansMatrix[i][0]);
311             if (!invalidatePositions(row, col,
312                     spansMatrix[i][1], spansMatrix[i][2])) {
313                 return;
314             }
315             addConstraintPosition(i, row, col,
316                     spansMatrix[i][1], spansMatrix[i][2]);
317         }
318     }
319 
320     /**
321      * Make positions in the grid unavailable based on the skips attr
322      *
323      * @param skipsMatrix a int matrix that contains skip information
324      */
handleSkips(int[][] skipsMatrix)325     private void handleSkips(int[][] skipsMatrix) {
326         for (int i = 0; i < skipsMatrix.length; i++) {
327             int row = getRowByIndex(skipsMatrix[i][0]);
328             int col = getColByIndex(skipsMatrix[i][0]);
329             if (!invalidatePositions(row, col,
330                     skipsMatrix[i][1], skipsMatrix[i][2])) {
331                 return;
332             }
333         }
334     }
335 
336     /**
337      * Make the specified positions in the grid unavailable.
338      *
339      * @param startRow the row of the staring position
340      * @param startColumn the column of the staring position
341      * @param rowSpan how many rows to span
342      * @param columnSpan how many columns to span
343      * @return true if we could properly invalidate the positions else false
344      */
invalidatePositions(int startRow, int startColumn, int rowSpan, int columnSpan)345     private boolean invalidatePositions(int startRow, int startColumn,
346                                         int rowSpan, int columnSpan) {
347         for (int i = startRow; i < startRow + rowSpan; i++) {
348             for (int j = startColumn; j < startColumn + columnSpan; j++) {
349                 if (i >= mPositionMatrix.length || j >= mPositionMatrix[0].length
350                         || !mPositionMatrix[i][j]) {
351                     // the position is already occupied.
352                     return false;
353                 }
354                 mPositionMatrix[i][j] = false;
355             }
356         }
357         return true;
358     }
359 
360     /**
361      * Arrange the views in the constraint_referenced_ids
362      */
addAllConstraintPositions()363     private void addAllConstraintPositions() {
364         int position;
365 
366         for (int i = 0; i < mNumWidgets; i++) {
367 
368             // Already added ConstraintPosition
369             if (leftOfWidget(i) != -1) {
370                 continue;
371             }
372 
373             position = getNextPosition();
374             int row = getRowByIndex(position);
375             int col = getColByIndex(position);
376             if (position == -1) {
377                 // no more available position.
378                 return;
379             }
380             addConstraintPosition(i, row, col, 1, 1);
381         }
382     }
383 
384     /**
385      * Compute the actual rows and columns given what was set
386      * if 0,0 find the most square rows and columns that fits
387      * if 0,n or n,0 scale to fit
388      */
updateActualRowsAndColumns()389     private void updateActualRowsAndColumns() {
390         if (mRowsSet == 0 || mColumnsSet == 0) {
391             if (mColumnsSet > 0) {
392                 mColumns = mColumnsSet;
393                 mRows = (mNumWidgets + mColumns - 1) / mColumnsSet; // round up
394             } else  if (mRowsSet > 0) {
395                 mRows = mRowsSet;
396                 mColumns = (mNumWidgets + mRowsSet - 1) / mRowsSet; // round up
397             } else { // as close to square as possible favoring more rows
398                 mRows = (int)  (1.5 + Math.sqrt(mNumWidgets));
399                 mColumns = (mNumWidgets + mRows - 1) / mRows;
400             }
401         } else {
402             mRows = mRowsSet;
403             mColumns = mColumnsSet;
404         }
405     }
406 
407     /**
408      * Set up the Grid engine.
409      */
setup()410     public void setup() {
411         boolean isUpdate = true;
412 
413         if (mConstraintMatrix == null
414                 || mConstraintMatrix.length != mNumWidgets
415                 || mPositionMatrix == null
416                 || mPositionMatrix.length != mRows
417                 || mPositionMatrix[0].length != mColumns) {
418             isUpdate = false;
419         }
420 
421         if (!isUpdate) {
422             initVariables();
423         }
424 
425         fillConstraintMatrix(isUpdate);
426     }
427 
428     /**
429      * set new spans value
430      *
431      * @param spans new spans value
432      */
setSpans(CharSequence spans)433     public void setSpans(CharSequence spans) {
434         if (mStrSpans != null && mStrSpans.equals(spans.toString())) {
435             return;
436         }
437 
438         mStrSpans = spans.toString();
439     }
440 
441     /**
442      * set new skips value
443      *
444      * @param skips new spans value
445      */
setSkips(String skips)446     public void setSkips(String skips) {
447         if (mStrSkips != null && mStrSkips.equals(skips)) {
448             return;
449         }
450 
451         mStrSkips = skips;
452 
453     }
454 
455     /**
456      * set new orientation value
457      *
458      * @param orientation new orientation value
459      */
setOrientation(int orientation)460     public void setOrientation(int orientation) {
461         if (!(orientation == HORIZONTAL || orientation == VERTICAL)) {
462             return;
463         }
464 
465         if (mOrientation == orientation) {
466             return;
467         }
468 
469         mOrientation = orientation;
470     }
471 
472     /**
473      * Set new NumWidgets value
474      * @param num how many widgets to be arranged in Grid
475      */
setNumWidgets(int num)476     public void setNumWidgets(int num) {
477         if (num > mRows * mColumns) {
478             return;
479         }
480 
481         mNumWidgets = num;
482     }
483 
484     /**
485      * set new rows value
486      *
487      * @param rows new rows value
488      */
setRows(int rows)489     public void setRows(int rows) {
490         if (rows > MAX_ROWS) {
491             return;
492         }
493 
494         if (mRowsSet == rows) {
495             return;
496         }
497 
498         mRowsSet = rows;
499         updateActualRowsAndColumns();
500 
501     }
502 
503     /**
504      * set new columns value
505      *
506      * @param columns new rows value
507      */
setColumns(int columns)508     public void setColumns(int columns) {
509         if (columns > MAX_COLUMNS) {
510             return;
511         }
512 
513         if (mColumnsSet == columns) {
514             return;
515         }
516 
517         mColumnsSet = columns;
518         updateActualRowsAndColumns();
519     }
520 
521     /**
522      * Get the boxView for the widget i to add a constraint on the left
523      *
524      * @param i the widget that has the order as i in the constraint_reference_ids
525      * @return the boxView to add a constraint on the left
526      */
leftOfWidget(int i)527     public int leftOfWidget(int i) {
528         if (mConstraintMatrix == null || i >= mConstraintMatrix.length) {
529             return 0;
530         }
531         return mConstraintMatrix[i][0];
532     }
533 
534     /**
535      * Get the boxView for the widget i to add a constraint on the top
536      *
537      * @param i the widget that has the order as i in the constraint_reference_ids
538      * @return the boxView to add a constraint on the top
539      */
topOfWidget(int i)540     public int topOfWidget(int i) {
541         if (mConstraintMatrix == null || i >= mConstraintMatrix.length) {
542             return 0;
543         }
544         return mConstraintMatrix[i][1];
545     }
546 
547     /**
548      * Get the boxView for the widget i to add a constraint on the right
549      *
550      * @param i the widget that has the order as i in the constraint_reference_ids
551      * @return the boxView to add a constraint on the right
552      */
rightOfWidget(int i)553     public int rightOfWidget(int i) {
554         if (mConstraintMatrix == null || i >= mConstraintMatrix.length) {
555             return 0;
556         }
557         return mConstraintMatrix[i][2];
558     }
559 
560     /**
561      * Get the boxView for the widget i to add a constraint on the bottom
562      *
563      * @param i the widget that has the order as i in the constraint_reference_ids
564      * @return the boxView to add a constraint on the bottom
565      */
bottomOfWidget(int i)566     public int bottomOfWidget(int i) {
567         if (mConstraintMatrix == null || i >= mConstraintMatrix.length) {
568             return 0;
569         }
570         return mConstraintMatrix[i][3];
571     }
572 }
573