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 package com.android.launcher3.celllayout; 17 18 import android.graphics.Point; 19 import android.graphics.Rect; 20 21 import java.util.ArrayDeque; 22 import java.util.ArrayList; 23 import java.util.HashMap; 24 import java.util.HashSet; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.Queue; 28 import java.util.Set; 29 import java.util.stream.Collectors; 30 31 32 public class CellLayoutBoard implements Comparable<CellLayoutBoard> { 33 intersects(Rect r1, Rect r2)34 private boolean intersects(Rect r1, Rect r2) { 35 // If one rectangle is on left side of other 36 if (r1.left > r2.right || r2.left > r1.right) { 37 return false; 38 } 39 40 // If one rectangle is above other 41 if (r1.bottom > r2.top || r2.bottom > r1.top) { 42 return false; 43 } 44 45 return true; 46 } 47 overlapsWithIgnored(Set<Rect> ignoredRectangles, Rect rect)48 private boolean overlapsWithIgnored(Set<Rect> ignoredRectangles, Rect rect) { 49 for (Rect ignoredRect : ignoredRectangles) { 50 // Using the built in intersects doesn't work because it doesn't account for area 0 51 if (intersects(ignoredRect, rect)) { 52 return true; 53 } 54 } 55 return false; 56 } 57 58 @Override compareTo(CellLayoutBoard cellLayoutBoard)59 public int compareTo(CellLayoutBoard cellLayoutBoard) { 60 // to be equal they need to have the same number of widgets and the same dimensions 61 // their order can be different 62 Set<Rect> widgetsSet = new HashSet<>(); 63 Set<Rect> ignoredRectangles = new HashSet<>(); 64 for (WidgetRect rect : mWidgetsRects) { 65 if (rect.shouldIgnore()) { 66 ignoredRectangles.add(rect.mBounds); 67 } else { 68 widgetsSet.add(rect.mBounds); 69 } 70 } 71 for (WidgetRect rect : cellLayoutBoard.mWidgetsRects) { 72 // ignore rectangles overlapping with the area marked by x 73 if (overlapsWithIgnored(ignoredRectangles, rect.mBounds)) { 74 continue; 75 } 76 if (!widgetsSet.contains(rect.mBounds)) { 77 return -1; 78 } 79 widgetsSet.remove(rect.mBounds); 80 } 81 if (!widgetsSet.isEmpty()) { 82 return 1; 83 } 84 85 // to be equal they need to have the same number of icons their order can be different 86 Set<Point> iconsSet = new HashSet<>(); 87 mIconPoints.forEach(icon -> iconsSet.add(icon.getCoord())); 88 for (IconPoint icon : cellLayoutBoard.mIconPoints) { 89 if (!iconsSet.contains(icon.getCoord())) { 90 return -1; 91 } 92 iconsSet.remove(icon.getCoord()); 93 } 94 if (!iconsSet.isEmpty()) { 95 return 1; 96 } 97 return 0; 98 } 99 100 public static class CellType { 101 // The cells marked by this will be filled by 1x1 widgets and will be ignored when 102 // validating 103 public static final char IGNORE = 'x'; 104 // The cells marked by this will be filled by app icons 105 public static final char ICON = 'i'; 106 // Empty space 107 public static final char EMPTY = '-'; 108 // Widget that will be saved as "main widget" for easier retrieval 109 public static final char MAIN_WIDGET = 'm'; 110 // Everything else will be consider a widget 111 } 112 113 public static class WidgetRect { 114 public char mType; 115 public Rect mBounds; 116 WidgetRect(char type, Rect bounds)117 WidgetRect(char type, Rect bounds) { 118 this.mType = type; 119 this.mBounds = bounds; 120 } 121 getSpanX()122 int getSpanX() { 123 return mBounds.right - mBounds.left + 1; 124 } 125 getSpanY()126 int getSpanY() { 127 return mBounds.top - mBounds.bottom + 1; 128 } 129 getCellX()130 int getCellX() { 131 return mBounds.left; 132 } 133 getCellY()134 int getCellY() { 135 return mBounds.bottom; 136 } 137 shouldIgnore()138 boolean shouldIgnore() { 139 return this.mType == CellType.IGNORE; 140 } 141 142 @Override toString()143 public String toString() { 144 return "WidgetRect type = " + mType + " bounds = " + mBounds.toString(); 145 } 146 } 147 148 public static class IconPoint { 149 public Point coord; 150 public char mType; 151 IconPoint(Point coord, char type)152 public IconPoint(Point coord, char type) { 153 this.coord = coord; 154 mType = type; 155 } 156 getType()157 public char getType() { 158 return mType; 159 } 160 setType(char type)161 public void setType(char type) { 162 mType = type; 163 } 164 getCoord()165 public Point getCoord() { 166 return coord; 167 } 168 setCoord(Point coord)169 public void setCoord(Point coord) { 170 this.coord = coord; 171 } 172 } 173 174 static final int INFINITE = 99999; 175 176 char[][] mWidget = new char[30][30]; 177 178 List<WidgetRect> mWidgetsRects = new ArrayList<>(); 179 Map<Character, WidgetRect> mWidgetsMap = new HashMap<>(); 180 181 List<IconPoint> mIconPoints = new ArrayList<>(); 182 Map<Character, IconPoint> mIconsMap = new HashMap<>(); 183 184 WidgetRect mMain = null; 185 CellLayoutBoard()186 CellLayoutBoard() { 187 for (int x = 0; x < mWidget.length; x++) { 188 for (int y = 0; y < mWidget[0].length; y++) { 189 mWidget[x][y] = CellType.EMPTY; 190 } 191 } 192 } 193 getWidgets()194 public List<WidgetRect> getWidgets() { 195 return mWidgetsRects; 196 } 197 getIcons()198 public List<IconPoint> getIcons() { 199 return mIconPoints; 200 } 201 getMain()202 public WidgetRect getMain() { 203 return mMain; 204 } 205 getWidgetRect(char c)206 public WidgetRect getWidgetRect(char c) { 207 return mWidgetsMap.get(c); 208 } 209 removeWidgetFromBoard(WidgetRect widget)210 private void removeWidgetFromBoard(WidgetRect widget) { 211 for (int xi = widget.mBounds.left; xi < widget.mBounds.right; xi++) { 212 for (int yi = widget.mBounds.top; yi < widget.mBounds.bottom; yi++) { 213 mWidget[xi][yi] = '-'; 214 } 215 } 216 } 217 removeOverlappingItems(Rect rect)218 private void removeOverlappingItems(Rect rect) { 219 // Remove overlapping widgets and remove them from the board 220 mWidgetsRects = mWidgetsRects.stream().filter(widget -> { 221 if (rect.intersect(widget.mBounds)) { 222 removeWidgetFromBoard(widget); 223 return false; 224 } 225 return true; 226 }).collect(Collectors.toList()); 227 // Remove overlapping icons and remove them from the board 228 mIconPoints = mIconPoints.stream().filter(iconPoint -> { 229 int x = iconPoint.coord.x; 230 int y = iconPoint.coord.y; 231 if (rect.contains(x, y)) { 232 mWidget[x][y] = '-'; 233 return false; 234 } 235 return true; 236 }).collect(Collectors.toList()); 237 } 238 removeOverlappingItems(Point p)239 private void removeOverlappingItems(Point p) { 240 // Remove overlapping widgets and remove them from the board 241 mWidgetsRects = mWidgetsRects.stream().filter(widget -> { 242 if (widget.mBounds.contains(p.x, p.y)) { 243 removeWidgetFromBoard(widget); 244 return false; 245 } 246 return true; 247 }).collect(Collectors.toList()); 248 // Remove overlapping icons and remove them from the board 249 mIconPoints = mIconPoints.stream().filter(iconPoint -> { 250 int x = iconPoint.coord.x; 251 int y = iconPoint.coord.y; 252 if (p.x == x && p.y == y) { 253 mWidget[x][y] = '-'; 254 return false; 255 } 256 return true; 257 }).collect(Collectors.toList()); 258 } 259 addWidget(int x, int y, int spanX, int spanY, char type)260 public void addWidget(int x, int y, int spanX, int spanY, char type) { 261 Rect rect = new Rect(x, y + spanY - 1, x + spanX - 1, y); 262 removeOverlappingItems(rect); 263 WidgetRect widgetRect = new WidgetRect(type, rect); 264 mWidgetsRects.add(widgetRect); 265 for (int xi = rect.left; xi < rect.right + 1; xi++) { 266 for (int yi = rect.bottom; yi < rect.top + 1; yi++) { 267 mWidget[xi][yi] = type; 268 } 269 } 270 } 271 addIcon(int x, int y)272 public void addIcon(int x, int y) { 273 removeOverlappingItems(new Point(x, y)); 274 mWidget[x][y] = 'i'; 275 } 276 getWidgetRect(int x, int y, Set<Point> used, char[][] board)277 public static WidgetRect getWidgetRect(int x, int y, Set<Point> used, char[][] board) { 278 char type = board[x][y]; 279 Queue<Point> search = new ArrayDeque<Point>(); 280 Point current = new Point(x, y); 281 search.add(current); 282 used.add(current); 283 List<Point> neighbors = new ArrayList<>(List.of( 284 new Point(-1, 0), 285 new Point(0, -1), 286 new Point(1, 0), 287 new Point(0, 1)) 288 ); 289 Rect widgetRect = new Rect(INFINITE, -INFINITE, -INFINITE, INFINITE); 290 while (!search.isEmpty()) { 291 current = search.poll(); 292 widgetRect.top = Math.max(widgetRect.top, current.y); 293 widgetRect.right = Math.max(widgetRect.right, current.x); 294 widgetRect.bottom = Math.min(widgetRect.bottom, current.y); 295 widgetRect.left = Math.min(widgetRect.left, current.x); 296 for (Point p : neighbors) { 297 Point next = new Point(current.x + p.x, current.y + p.y); 298 if (next.x < 0 || next.x >= board.length) continue; 299 if (next.y < 0 || next.y >= board[0].length) continue; 300 if (board[next.x][next.y] == type && !used.contains(next)) { 301 used.add(next); 302 search.add(next); 303 } 304 } 305 } 306 return new WidgetRect(type, widgetRect); 307 } 308 isWidget(char type)309 public static boolean isWidget(char type) { 310 return type != CellType.ICON && type != CellType.EMPTY; 311 } 312 isIcon(char type)313 public static boolean isIcon(char type) { 314 return type == CellType.ICON; 315 } 316 getRects(char[][] board)317 private static List<WidgetRect> getRects(char[][] board) { 318 Set<Point> used = new HashSet<>(); 319 List<WidgetRect> widgetsRects = new ArrayList<>(); 320 for (int x = 0; x < board.length; x++) { 321 for (int y = 0; y < board[0].length; y++) { 322 if (!used.contains(new Point(x, y)) && isWidget(board[x][y])) { 323 widgetsRects.add(getWidgetRect(x, y, used, board)); 324 } 325 } 326 } 327 return widgetsRects; 328 } 329 getIconPoints(char[][] board)330 private static List<IconPoint> getIconPoints(char[][] board) { 331 List<IconPoint> iconPoints = new ArrayList<>(); 332 for (int x = 0; x < board.length; x++) { 333 for (int y = 0; y < board[0].length; y++) { 334 if (isIcon(board[x][y])) { 335 iconPoints.add(new IconPoint(new Point(x, y), board[x][y])); 336 } 337 } 338 } 339 return iconPoints; 340 } 341 getMainFromList(List<CellLayoutBoard> boards)342 public static WidgetRect getMainFromList(List<CellLayoutBoard> boards) { 343 for (CellLayoutBoard board : boards) { 344 WidgetRect main = board.getMain(); 345 if (main != null) { 346 return main; 347 } 348 } 349 return null; 350 } 351 boardFromString(String boardStr)352 public static CellLayoutBoard boardFromString(String boardStr) { 353 String[] lines = boardStr.split("\n"); 354 CellLayoutBoard board = new CellLayoutBoard(); 355 356 for (int y = 0; y < lines.length; y++) { 357 String line = lines[y]; 358 for (int x = 0; x < line.length(); x++) { 359 char c = line.charAt(x); 360 if (c != CellType.EMPTY) { 361 board.mWidget[x][y] = line.charAt(x); 362 } 363 } 364 } 365 board.mWidgetsRects = getRects(board.mWidget); 366 board.mWidgetsRects.forEach(widgetRect -> { 367 if (widgetRect.mType == CellType.MAIN_WIDGET) { 368 board.mMain = widgetRect; 369 } 370 board.mWidgetsMap.put(widgetRect.mType, widgetRect); 371 }); 372 board.mIconPoints = getIconPoints(board.mWidget); 373 return board; 374 } 375 toString(int maxX, int maxY)376 public String toString(int maxX, int maxY) { 377 StringBuilder s = new StringBuilder(); 378 maxX = Math.min(maxX, mWidget.length); 379 maxY = Math.min(maxY, mWidget[0].length); 380 for (int y = 0; y < maxY; y++) { 381 for (int x = 0; x < maxX; x++) { 382 s.append(mWidget[x][y]); 383 } 384 s.append('\n'); 385 } 386 return s.toString(); 387 } 388 boardListFromString(String boardsStr)389 public static List<CellLayoutBoard> boardListFromString(String boardsStr) { 390 String[] lines = boardsStr.split("\n"); 391 ArrayList<String> individualBoards = new ArrayList<>(); 392 ArrayList<CellLayoutBoard> boards = new ArrayList<>(); 393 for (String line : lines) { 394 String[] boardSegment = line.split("\\|"); 395 for (int i = 0; i < boardSegment.length; i++) { 396 if (i >= individualBoards.size()) { 397 individualBoards.add(boardSegment[i]); 398 } else { 399 individualBoards.set(i, individualBoards.get(i) + "\n" + boardSegment[i]); 400 } 401 } 402 } 403 for (String board : individualBoards) { 404 boards.add(CellLayoutBoard.boardFromString(board)); 405 } 406 return boards; 407 } 408 } 409