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