From 9cab1c3ece40d1b4ddadd085f870d4639973d91c Mon Sep 17 00:00:00 2001 From: Sebastian Franco Date: Tue, 25 Oct 2022 17:28:54 -0700 Subject: [PATCH] No code changes, this cl only reorganizes the functions This is done such that future cls are easier to visualize. Bug: 188081026 Test: atest ReorderWidgets Change-Id: I749d21b517dada97887d5b5e7cb5e2ac644d0030 --- src/com/android/launcher3/CellLayout.java | 1443 ++++++++++----------- 1 file changed, 721 insertions(+), 722 deletions(-) diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 19afa7830f..9d059bbb0e 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -1396,649 +1396,6 @@ public class CellLayout extends ViewGroup { return bestXY; } - /** - * Find a vacant area that will fit the given bounds nearest the requested - * cell location, and will also weigh in a suggested direction vector of the - * desired location. This method computers distance based on unit grid distances, - * not pixel distances. - * - * @param cellX The X cell nearest to which you want to search for a vacant area. - * @param cellY The Y cell nearest which you want to search for a vacant area. - * @param spanX Horizontal span of the object. - * @param spanY Vertical span of the object. - * @param direction The favored direction in which the views should move from x, y - * @param occupied The array which represents which cells in the CellLayout are occupied - * @param blockOccupied The array which represents which cells in the specified block (cellX, - * cellY, spanX, spanY) are occupied. This is used when try to move a group of views. - * @param result Array in which to place the result, or null (in which case a new array will - * be allocated) - * @return The X, Y cell of a vacant area that can contain this object, - * nearest the requested location. - */ - private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction, - boolean[][] occupied, boolean blockOccupied[][], int[] result) { - // Keep track of best-scoring drop area - final int[] bestXY = result != null ? result : new int[2]; - float bestDistance = Float.MAX_VALUE; - int bestDirectionScore = Integer.MIN_VALUE; - - final int countX = mCountX; - final int countY = mCountY; - - for (int y = 0; y < countY - (spanY - 1); y++) { - inner: - for (int x = 0; x < countX - (spanX - 1); x++) { - // First, let's see if this thing fits anywhere - for (int i = 0; i < spanX; i++) { - for (int j = 0; j < spanY; j++) { - if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) { - continue inner; - } - } - } - - float distance = (float) Math.hypot(x - cellX, y - cellY); - int[] curDirection = mTmpPoint; - computeDirectionVector(x - cellX, y - cellY, curDirection); - // The direction score is just the dot product of the two candidate direction - // and that passed in. - int curDirectionScore = direction[0] * curDirection[0] + - direction[1] * curDirection[1]; - if (Float.compare(distance, bestDistance) < 0 || - (Float.compare(distance, bestDistance) == 0 - && curDirectionScore > bestDirectionScore)) { - bestDistance = distance; - bestDirectionScore = curDirectionScore; - bestXY[0] = x; - bestXY[1] = y; - } - } - } - - // Return -1, -1 if no suitable location found - if (bestDistance == Float.MAX_VALUE) { - bestXY[0] = -1; - bestXY[1] = -1; - } - return bestXY; - } - - private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop, - int[] direction, ItemConfiguration currentState) { - CellAndSpan c = currentState.map.get(v); - boolean success = false; - mTmpOccupied.markCells(c, false); - mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true); - - findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction, - mTmpOccupied.cells, null, mTempLocation); - - if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { - c.cellX = mTempLocation[0]; - c.cellY = mTempLocation[1]; - success = true; - } - mTmpOccupied.markCells(c, true); - return success; - } - - /** - * This helper class defines a cluster of views. It helps with defining complex edges - * of the cluster and determining how those edges interact with other views. The edges - * essentially define a fine-grained boundary around the cluster of views -- like a more - * precise version of a bounding box. - */ - private class ViewCluster { - final static int LEFT = 1 << 0; - final static int TOP = 1 << 1; - final static int RIGHT = 1 << 2; - final static int BOTTOM = 1 << 3; - - final ArrayList views; - final ItemConfiguration config; - final Rect boundingRect = new Rect(); - - final int[] leftEdge = new int[mCountY]; - final int[] rightEdge = new int[mCountY]; - final int[] topEdge = new int[mCountX]; - final int[] bottomEdge = new int[mCountX]; - int dirtyEdges; - boolean boundingRectDirty; - - @SuppressWarnings("unchecked") - public ViewCluster(ArrayList views, ItemConfiguration config) { - this.views = (ArrayList) views.clone(); - this.config = config; - resetEdges(); - } - - void resetEdges() { - for (int i = 0; i < mCountX; i++) { - topEdge[i] = -1; - bottomEdge[i] = -1; - } - for (int i = 0; i < mCountY; i++) { - leftEdge[i] = -1; - rightEdge[i] = -1; - } - dirtyEdges = LEFT | TOP | RIGHT | BOTTOM; - boundingRectDirty = true; - } - - void computeEdge(int which) { - int count = views.size(); - for (int i = 0; i < count; i++) { - CellAndSpan cs = config.map.get(views.get(i)); - switch (which) { - case LEFT: - int left = cs.cellX; - for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) { - if (left < leftEdge[j] || leftEdge[j] < 0) { - leftEdge[j] = left; - } - } - break; - case RIGHT: - int right = cs.cellX + cs.spanX; - for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) { - if (right > rightEdge[j]) { - rightEdge[j] = right; - } - } - break; - case TOP: - int top = cs.cellY; - for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) { - if (top < topEdge[j] || topEdge[j] < 0) { - topEdge[j] = top; - } - } - break; - case BOTTOM: - int bottom = cs.cellY + cs.spanY; - for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) { - if (bottom > bottomEdge[j]) { - bottomEdge[j] = bottom; - } - } - break; - } - } - } - - boolean isViewTouchingEdge(View v, int whichEdge) { - CellAndSpan cs = config.map.get(v); - - if ((dirtyEdges & whichEdge) == whichEdge) { - computeEdge(whichEdge); - dirtyEdges &= ~whichEdge; - } - - switch (whichEdge) { - case LEFT: - for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) { - if (leftEdge[i] == cs.cellX + cs.spanX) { - return true; - } - } - break; - case RIGHT: - for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) { - if (rightEdge[i] == cs.cellX) { - return true; - } - } - break; - case TOP: - for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) { - if (topEdge[i] == cs.cellY + cs.spanY) { - return true; - } - } - break; - case BOTTOM: - for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) { - if (bottomEdge[i] == cs.cellY) { - return true; - } - } - break; - } - return false; - } - - void shift(int whichEdge, int delta) { - for (View v: views) { - CellAndSpan c = config.map.get(v); - switch (whichEdge) { - case LEFT: - c.cellX -= delta; - break; - case RIGHT: - c.cellX += delta; - break; - case TOP: - c.cellY -= delta; - break; - case BOTTOM: - default: - c.cellY += delta; - break; - } - } - resetEdges(); - } - - public void addView(View v) { - views.add(v); - resetEdges(); - } - - public Rect getBoundingRect() { - if (boundingRectDirty) { - config.getBoundingRectForViews(views, boundingRect); - } - return boundingRect; - } - - final PositionComparator comparator = new PositionComparator(); - class PositionComparator implements Comparator { - int whichEdge = 0; - public int compare(View left, View right) { - CellAndSpan l = config.map.get(left); - CellAndSpan r = config.map.get(right); - switch (whichEdge) { - case LEFT: - return (r.cellX + r.spanX) - (l.cellX + l.spanX); - case RIGHT: - return l.cellX - r.cellX; - case TOP: - return (r.cellY + r.spanY) - (l.cellY + l.spanY); - case BOTTOM: - default: - return l.cellY - r.cellY; - } - } - } - - public void sortConfigurationForEdgePush(int edge) { - comparator.whichEdge = edge; - Collections.sort(config.sortedViews, comparator); - } - } - - private boolean pushViewsToTempLocation(ArrayList views, Rect rectOccupiedByPotentialDrop, - int[] direction, View dragView, ItemConfiguration currentState) { - - ViewCluster cluster = new ViewCluster(views, currentState); - Rect clusterRect = cluster.getBoundingRect(); - int whichEdge; - int pushDistance; - boolean fail = false; - - // Determine the edge of the cluster that will be leading the push and how far - // the cluster must be shifted. - if (direction[0] < 0) { - whichEdge = ViewCluster.LEFT; - pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left; - } else if (direction[0] > 0) { - whichEdge = ViewCluster.RIGHT; - pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left; - } else if (direction[1] < 0) { - whichEdge = ViewCluster.TOP; - pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top; - } else { - whichEdge = ViewCluster.BOTTOM; - pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top; - } - - // Break early for invalid push distance. - if (pushDistance <= 0) { - return false; - } - - // Mark the occupied state as false for the group of views we want to move. - for (View v: views) { - CellAndSpan c = currentState.map.get(v); - mTmpOccupied.markCells(c, false); - } - - // We save the current configuration -- if we fail to find a solution we will revert - // to the initial state. The process of finding a solution modifies the configuration - // in place, hence the need for revert in the failure case. - currentState.save(); - - // The pushing algorithm is simplified by considering the views in the order in which - // they would be pushed by the cluster. For example, if the cluster is leading with its - // left edge, we consider sort the views by their right edge, from right to left. - cluster.sortConfigurationForEdgePush(whichEdge); - - while (pushDistance > 0 && !fail) { - for (View v: currentState.sortedViews) { - // For each view that isn't in the cluster, we see if the leading edge of the - // cluster is contacting the edge of that view. If so, we add that view to the - // cluster. - if (!cluster.views.contains(v) && v != dragView) { - if (cluster.isViewTouchingEdge(v, whichEdge)) { - CellLayoutLayoutParams lp = (CellLayoutLayoutParams) v.getLayoutParams(); - if (!lp.canReorder) { - // The push solution includes the all apps button, this is not viable. - fail = true; - break; - } - cluster.addView(v); - CellAndSpan c = currentState.map.get(v); - - // Adding view to cluster, mark it as not occupied. - mTmpOccupied.markCells(c, false); - } - } - } - pushDistance--; - - // The cluster has been completed, now we move the whole thing over in the appropriate - // direction. - cluster.shift(whichEdge, 1); - } - - boolean foundSolution = false; - clusterRect = cluster.getBoundingRect(); - - // Due to the nature of the algorithm, the only check required to verify a valid solution - // is to ensure that completed shifted cluster lies completely within the cell layout. - if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 && - clusterRect.bottom <= mCountY) { - foundSolution = true; - } else { - currentState.restore(); - } - - // In either case, we set the occupied array as marked for the location of the views - for (View v: cluster.views) { - CellAndSpan c = currentState.map.get(v); - mTmpOccupied.markCells(c, true); - } - - return foundSolution; - } - - private boolean addViewsToTempLocation(ArrayList views, Rect rectOccupiedByPotentialDrop, - int[] direction, View dragView, ItemConfiguration currentState) { - if (views.size() == 0) return true; - - boolean success = false; - Rect boundingRect = new Rect(); - // We construct a rect which represents the entire group of views passed in - currentState.getBoundingRectForViews(views, boundingRect); - - // Mark the occupied state as false for the group of views we want to move. - for (View v: views) { - CellAndSpan c = currentState.map.get(v); - mTmpOccupied.markCells(c, false); - } - - GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(), boundingRect.height()); - int top = boundingRect.top; - int left = boundingRect.left; - // We mark more precisely which parts of the bounding rect are truly occupied, allowing - // for interlocking. - for (View v: views) { - CellAndSpan c = currentState.map.get(v); - blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true); - } - - mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true); - - findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(), - boundingRect.height(), direction, - mTmpOccupied.cells, blockOccupied.cells, mTempLocation); - - // If we successfuly found a location by pushing the block of views, we commit it - if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { - int deltaX = mTempLocation[0] - boundingRect.left; - int deltaY = mTempLocation[1] - boundingRect.top; - for (View v: views) { - CellAndSpan c = currentState.map.get(v); - c.cellX += deltaX; - c.cellY += deltaY; - } - success = true; - } - - // In either case, we set the occupied array as marked for the location of the views - for (View v: views) { - CellAndSpan c = currentState.map.get(v); - mTmpOccupied.markCells(c, true); - } - return success; - } - - // This method tries to find a reordering solution which satisfies the push mechanic by trying - // to push items in each of the cardinal directions, in an order based on the direction vector - // passed. - private boolean attemptPushInDirection(ArrayList intersectingViews, Rect occupied, - int[] direction, View ignoreView, ItemConfiguration solution) { - if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) { - // If the direction vector has two non-zero components, we try pushing - // separately in each of the components. - int temp = direction[1]; - direction[1] = 0; - - if (pushViewsToTempLocation(intersectingViews, occupied, direction, - ignoreView, solution)) { - return true; - } - direction[1] = temp; - temp = direction[0]; - direction[0] = 0; - - if (pushViewsToTempLocation(intersectingViews, occupied, direction, - ignoreView, solution)) { - return true; - } - // Revert the direction - direction[0] = temp; - - // Now we try pushing in each component of the opposite direction - direction[0] *= -1; - direction[1] *= -1; - temp = direction[1]; - direction[1] = 0; - if (pushViewsToTempLocation(intersectingViews, occupied, direction, - ignoreView, solution)) { - return true; - } - - direction[1] = temp; - temp = direction[0]; - direction[0] = 0; - if (pushViewsToTempLocation(intersectingViews, occupied, direction, - ignoreView, solution)) { - return true; - } - // revert the direction - direction[0] = temp; - direction[0] *= -1; - direction[1] *= -1; - - } else { - // If the direction vector has a single non-zero component, we push first in the - // direction of the vector - if (pushViewsToTempLocation(intersectingViews, occupied, direction, - ignoreView, solution)) { - return true; - } - // Then we try the opposite direction - direction[0] *= -1; - direction[1] *= -1; - if (pushViewsToTempLocation(intersectingViews, occupied, direction, - ignoreView, solution)) { - return true; - } - // Switch the direction back - direction[0] *= -1; - direction[1] *= -1; - - // If we have failed to find a push solution with the above, then we try - // to find a solution by pushing along the perpendicular axis. - - // Swap the components - int temp = direction[1]; - direction[1] = direction[0]; - direction[0] = temp; - if (pushViewsToTempLocation(intersectingViews, occupied, direction, - ignoreView, solution)) { - return true; - } - - // Then we try the opposite direction - direction[0] *= -1; - direction[1] *= -1; - if (pushViewsToTempLocation(intersectingViews, occupied, direction, - ignoreView, solution)) { - return true; - } - // Switch the direction back - direction[0] *= -1; - direction[1] *= -1; - - // Swap the components back - temp = direction[1]; - direction[1] = direction[0]; - direction[0] = temp; - } - return false; - } - - private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction, - View ignoreView, ItemConfiguration solution) { - // Return early if get invalid cell positions - if (cellX < 0 || cellY < 0) return false; - - mIntersectingViews.clear(); - mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY); - - // Mark the desired location of the view currently being dragged. - if (ignoreView != null) { - CellAndSpan c = solution.map.get(ignoreView); - if (c != null) { - c.cellX = cellX; - c.cellY = cellY; - } - } - Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY); - Rect r1 = new Rect(); - for (View child: solution.map.keySet()) { - if (child == ignoreView) continue; - CellAndSpan c = solution.map.get(child); - CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); - r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY); - if (Rect.intersects(r0, r1)) { - if (!lp.canReorder) { - return false; - } - mIntersectingViews.add(child); - } - } - - solution.intersectingViews = new ArrayList<>(mIntersectingViews); - - // First we try to find a solution which respects the push mechanic. That is, - // we try to find a solution such that no displaced item travels through another item - // without also displacing that item. - if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView, - solution)) { - return true; - } - - // Next we try moving the views as a block, but without requiring the push mechanic. - if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView, - solution)) { - return true; - } - - // Ok, they couldn't move as a block, let's move them individually - for (View v : mIntersectingViews) { - if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) { - return false; - } - } - return true; - } - - /* - * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between - * the provided point and the provided cell - */ - private void computeDirectionVector(float deltaX, float deltaY, int[] result) { - double angle = Math.atan(deltaY / deltaX); - - result[0] = 0; - result[1] = 0; - if (Math.abs(Math.cos(angle)) > 0.5f) { - result[0] = (int) Math.signum(deltaX); - } - if (Math.abs(Math.sin(angle)) > 0.5f) { - result[1] = (int) Math.signum(deltaY); - } - } - - private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, - int spanX, int spanY, int[] direction, View dragView, boolean decX, - ItemConfiguration solution) { - // Copy the current state into the solution. This solution will be manipulated as necessary. - copyCurrentStateToSolution(solution, false); - // Copy the current occupied array into the temporary occupied array. This array will be - // manipulated as necessary to find a solution. - mOccupied.copyTo(mTmpOccupied); - - // We find the nearest cell into which we would place the dragged item, assuming there's - // nothing in its way. - int result[] = new int[2]; - result = findNearestArea(pixelX, pixelY, spanX, spanY, result); - - boolean success; - // First we try the exact nearest position of the item being dragged, - // we will then want to try to move this around to other neighbouring positions - success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView, - solution); - - if (!success) { - // We try shrinking the widget down to size in an alternating pattern, shrink 1 in - // x, then 1 in y etc. - if (spanX > minSpanX && (minSpanY == spanY || decX)) { - return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY, - direction, dragView, false, solution); - } else if (spanY > minSpanY) { - return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1, - direction, dragView, true, solution); - } - solution.isSolution = false; - } else { - solution.isSolution = true; - solution.cellX = result[0]; - solution.cellY = result[1]; - solution.spanX = spanX; - solution.spanY = spanY; - } - return solution; - } - - private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) { - int childCount = mShortcutsAndWidgets.getChildCount(); - for (int i = 0; i < childCount; i++) { - View child = mShortcutsAndWidgets.getChildAt(i); - CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); - CellAndSpan c; - if (temp) { - c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan); - } else { - c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan); - } - solution.add(child, c); - } - } - private void copySolutionToTempState(ItemConfiguration solution, View dragView) { mTmpOccupied.clear(); @@ -2335,54 +1692,6 @@ public class CellLayout extends ViewGroup { return solution; } - /* This seems like it should be obvious and straight-forward, but when the direction vector - needs to match with the notion of the dragView pushing other views, we have to employ - a slightly more subtle notion of the direction vector. The question is what two points is - the vector between? The center of the dragView and its desired destination? Not quite, as - this doesn't necessarily coincide with the interaction of the dragView and items occupying - those cells. Instead we use some heuristics to often lock the vector to up, down, left - or right, which helps make pushing feel right. - */ - private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX, - int spanY, View dragView, int[] resultDirection) { - - //TODO(adamcohen) b/151776141 use the items visual center for the direction vector - int[] targetDestination = new int[2]; - - findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination); - Rect dragRect = new Rect(); - cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect); - dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY()); - - Rect dropRegionRect = new Rect(); - getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY, - dragView, dropRegionRect, mIntersectingViews); - - int dropRegionSpanX = dropRegionRect.width(); - int dropRegionSpanY = dropRegionRect.height(); - - cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(), - dropRegionRect.height(), dropRegionRect); - - int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX; - int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY; - - if (dropRegionSpanX == mCountX || spanX == mCountX) { - deltaX = 0; - } - if (dropRegionSpanY == mCountY || spanY == mCountY) { - deltaY = 0; - } - - if (deltaX == 0 && deltaY == 0) { - // No idea what to do, give a random direction. - resultDirection[0] = 1; - resultDirection[1] = 0; - } else { - computeDirectionVector(deltaX, deltaY, resultDirection); - } - } - // For a given cell and span, fetch the set of views intersecting the region. private void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY, View dragView, Rect boundingRect, ArrayList intersectingViews) { @@ -2466,42 +1775,695 @@ public class CellLayout extends ViewGroup { return swapSolution.isSolution; } - int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, - View dragView, int[] result, int[] resultSpan, int mode) { - if (resultSpan == null) { - resultSpan = new int[]{-1, -1}; - } - if (result == null) { - result = new int[]{-1, -1}; - } + /** + * Find a vacant area that will fit the given bounds nearest the requested + * cell location, and will also weigh in a suggested direction vector of the + * desired location. This method computers distance based on unit grid distances, + * not pixel distances. + * + * @param cellX The X cell nearest to which you want to search for a vacant area. + * @param cellY The Y cell nearest which you want to search for a vacant area. + * @param spanX Horizontal span of the object. + * @param spanY Vertical span of the object. + * @param direction The favored direction in which the views should move from x, y + * @param occupied The array which represents which cells in the CellLayout are occupied + * @param blockOccupied The array which represents which cells in the specified block (cellX, + * cellY, spanX, spanY) are occupied. This is used when try to move a group of views. + * @param result Array in which to place the result, or null (in which case a new array will + * be allocated) + * @return The X, Y cell of a vacant area that can contain this object, + * nearest the requested location. + */ + private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction, + boolean[][] occupied, boolean blockOccupied[][], int[] result) { + // Keep track of best-scoring drop area + final int[] bestXY = result != null ? result : new int[2]; + float bestDistance = Float.MAX_VALUE; + int bestDirectionScore = Integer.MIN_VALUE; - ItemConfiguration finalSolution; - // When we are checking drop validity or actually dropping, we don't recompute the - // direction vector, since we want the solution to match the preview, and it's possible - // that the exact position of the item has changed to result in a new reordering outcome. - if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP) - && mPreviousSolution != null) { - finalSolution = mPreviousSolution; - // We reset this vector after drop - if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { - mPreviousSolution = null; + final int countX = mCountX; + final int countY = mCountY; + + for (int y = 0; y < countY - (spanY - 1); y++) { + inner: + for (int x = 0; x < countX - (spanX - 1); x++) { + // First, let's see if this thing fits anywhere + for (int i = 0; i < spanX; i++) { + for (int j = 0; j < spanY; j++) { + if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) { + continue inner; + } + } + } + + float distance = (float) Math.hypot(x - cellX, y - cellY); + int[] curDirection = mTmpPoint; + computeDirectionVector(x - cellX, y - cellY, curDirection); + // The direction score is just the dot product of the two candidate direction + // and that passed in. + int curDirectionScore = direction[0] * curDirection[0] + + direction[1] * curDirection[1]; + if (Float.compare(distance, bestDistance) < 0 || + (Float.compare(distance, bestDistance) == 0 + && curDirectionScore > bestDirectionScore)) { + bestDistance = distance; + bestDirectionScore = curDirectionScore; + bestXY[0] = x; + bestXY[1] = y; + } } - } else { - finalSolution = calculateReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, - dragView); - mPreviousSolution = finalSolution; } - if (finalSolution == null || !finalSolution.isSolution) { - result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1; - } else { - result[0] = finalSolution.cellX; - result[1] = finalSolution.cellY; - resultSpan[0] = finalSolution.spanX; - resultSpan[1] = finalSolution.spanY; + // Return -1, -1 if no suitable location found + if (bestDistance == Float.MAX_VALUE) { + bestXY[0] = -1; + bestXY[1] = -1; + } + return bestXY; + } + + private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop, + int[] direction, ItemConfiguration currentState) { + CellAndSpan c = currentState.map.get(v); + boolean success = false; + mTmpOccupied.markCells(c, false); + mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true); + + findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction, + mTmpOccupied.cells, null, mTempLocation); + + if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { + c.cellX = mTempLocation[0]; + c.cellY = mTempLocation[1]; + success = true; + } + mTmpOccupied.markCells(c, true); + return success; + } + + private boolean pushViewsToTempLocation(ArrayList views, Rect rectOccupiedByPotentialDrop, + int[] direction, View dragView, ItemConfiguration currentState) { + + ViewCluster cluster = new ViewCluster(views, currentState); + Rect clusterRect = cluster.getBoundingRect(); + int whichEdge; + int pushDistance; + boolean fail = false; + + // Determine the edge of the cluster that will be leading the push and how far + // the cluster must be shifted. + if (direction[0] < 0) { + whichEdge = ViewCluster.LEFT; + pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left; + } else if (direction[0] > 0) { + whichEdge = ViewCluster.RIGHT; + pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left; + } else if (direction[1] < 0) { + whichEdge = ViewCluster.TOP; + pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top; + } else { + whichEdge = ViewCluster.BOTTOM; + pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top; + } + + // Break early for invalid push distance. + if (pushDistance <= 0) { + return false; + } + + // Mark the occupied state as false for the group of views we want to move. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + mTmpOccupied.markCells(c, false); + } + + // We save the current configuration -- if we fail to find a solution we will revert + // to the initial state. The process of finding a solution modifies the configuration + // in place, hence the need for revert in the failure case. + currentState.save(); + + // The pushing algorithm is simplified by considering the views in the order in which + // they would be pushed by the cluster. For example, if the cluster is leading with its + // left edge, we consider sort the views by their right edge, from right to left. + cluster.sortConfigurationForEdgePush(whichEdge); + + while (pushDistance > 0 && !fail) { + for (View v: currentState.sortedViews) { + // For each view that isn't in the cluster, we see if the leading edge of the + // cluster is contacting the edge of that view. If so, we add that view to the + // cluster. + if (!cluster.views.contains(v) && v != dragView) { + if (cluster.isViewTouchingEdge(v, whichEdge)) { + CellLayoutLayoutParams lp = (CellLayoutLayoutParams) v.getLayoutParams(); + if (!lp.canReorder) { + // The push solution includes the all apps button, this is not viable. + fail = true; + break; + } + cluster.addView(v); + CellAndSpan c = currentState.map.get(v); + + // Adding view to cluster, mark it as not occupied. + mTmpOccupied.markCells(c, false); + } + } + } + pushDistance--; + + // The cluster has been completed, now we move the whole thing over in the appropriate + // direction. + cluster.shift(whichEdge, 1); + } + + boolean foundSolution = false; + clusterRect = cluster.getBoundingRect(); + + // Due to the nature of the algorithm, the only check required to verify a valid solution + // is to ensure that completed shifted cluster lies completely within the cell layout. + if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 && + clusterRect.bottom <= mCountY) { + foundSolution = true; + } else { + currentState.restore(); + } + + // In either case, we set the occupied array as marked for the location of the views + for (View v: cluster.views) { + CellAndSpan c = currentState.map.get(v); + mTmpOccupied.markCells(c, true); + } + + return foundSolution; + } + + /** + * This helper class defines a cluster of views. It helps with defining complex edges + * of the cluster and determining how those edges interact with other views. The edges + * essentially define a fine-grained boundary around the cluster of views -- like a more + * precise version of a bounding box. + */ + private class ViewCluster { + final static int LEFT = 1 << 0; + final static int TOP = 1 << 1; + final static int RIGHT = 1 << 2; + final static int BOTTOM = 1 << 3; + + final ArrayList views; + final ItemConfiguration config; + final Rect boundingRect = new Rect(); + + final int[] leftEdge = new int[mCountY]; + final int[] rightEdge = new int[mCountY]; + final int[] topEdge = new int[mCountX]; + final int[] bottomEdge = new int[mCountX]; + int dirtyEdges; + boolean boundingRectDirty; + + @SuppressWarnings("unchecked") + public ViewCluster(ArrayList views, ItemConfiguration config) { + this.views = (ArrayList) views.clone(); + this.config = config; + resetEdges(); + } + + void resetEdges() { + for (int i = 0; i < mCountX; i++) { + topEdge[i] = -1; + bottomEdge[i] = -1; + } + for (int i = 0; i < mCountY; i++) { + leftEdge[i] = -1; + rightEdge[i] = -1; + } + dirtyEdges = LEFT | TOP | RIGHT | BOTTOM; + boundingRectDirty = true; + } + + void computeEdge(int which) { + int count = views.size(); + for (int i = 0; i < count; i++) { + CellAndSpan cs = config.map.get(views.get(i)); + switch (which) { + case LEFT: + int left = cs.cellX; + for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) { + if (left < leftEdge[j] || leftEdge[j] < 0) { + leftEdge[j] = left; + } + } + break; + case RIGHT: + int right = cs.cellX + cs.spanX; + for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) { + if (right > rightEdge[j]) { + rightEdge[j] = right; + } + } + break; + case TOP: + int top = cs.cellY; + for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) { + if (top < topEdge[j] || topEdge[j] < 0) { + topEdge[j] = top; + } + } + break; + case BOTTOM: + int bottom = cs.cellY + cs.spanY; + for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) { + if (bottom > bottomEdge[j]) { + bottomEdge[j] = bottom; + } + } + break; + } + } + } + + boolean isViewTouchingEdge(View v, int whichEdge) { + CellAndSpan cs = config.map.get(v); + + if ((dirtyEdges & whichEdge) == whichEdge) { + computeEdge(whichEdge); + dirtyEdges &= ~whichEdge; + } + + switch (whichEdge) { + case LEFT: + for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) { + if (leftEdge[i] == cs.cellX + cs.spanX) { + return true; + } + } + break; + case RIGHT: + for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) { + if (rightEdge[i] == cs.cellX) { + return true; + } + } + break; + case TOP: + for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) { + if (topEdge[i] == cs.cellY + cs.spanY) { + return true; + } + } + break; + case BOTTOM: + for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) { + if (bottomEdge[i] == cs.cellY) { + return true; + } + } + break; + } + return false; + } + + void shift(int whichEdge, int delta) { + for (View v: views) { + CellAndSpan c = config.map.get(v); + switch (whichEdge) { + case LEFT: + c.cellX -= delta; + break; + case RIGHT: + c.cellX += delta; + break; + case TOP: + c.cellY -= delta; + break; + case BOTTOM: + default: + c.cellY += delta; + break; + } + } + resetEdges(); + } + + public void addView(View v) { + views.add(v); + resetEdges(); + } + + public Rect getBoundingRect() { + if (boundingRectDirty) { + config.getBoundingRectForViews(views, boundingRect); + } + return boundingRect; + } + + final PositionComparator comparator = new PositionComparator(); + class PositionComparator implements Comparator { + int whichEdge = 0; + public int compare(View left, View right) { + CellAndSpan l = config.map.get(left); + CellAndSpan r = config.map.get(right); + switch (whichEdge) { + case LEFT: + return (r.cellX + r.spanX) - (l.cellX + l.spanX); + case RIGHT: + return l.cellX - r.cellX; + case TOP: + return (r.cellY + r.spanY) - (l.cellY + l.spanY); + case BOTTOM: + default: + return l.cellY - r.cellY; + } + } + } + + public void sortConfigurationForEdgePush(int edge) { + comparator.whichEdge = edge; + Collections.sort(config.sortedViews, comparator); + } + } + + // This method tries to find a reordering solution which satisfies the push mechanic by trying + // to push items in each of the cardinal directions, in an order based on the direction vector + // passed. + private boolean attemptPushInDirection(ArrayList intersectingViews, Rect occupied, + int[] direction, View ignoreView, ItemConfiguration solution) { + if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) { + // If the direction vector has two non-zero components, we try pushing + // separately in each of the components. + int temp = direction[1]; + direction[1] = 0; + + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + direction[1] = temp; + temp = direction[0]; + direction[0] = 0; + + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Revert the direction + direction[0] = temp; + + // Now we try pushing in each component of the opposite direction + direction[0] *= -1; + direction[1] *= -1; + temp = direction[1]; + direction[1] = 0; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + + direction[1] = temp; + temp = direction[0]; + direction[0] = 0; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // revert the direction + direction[0] = temp; + direction[0] *= -1; + direction[1] *= -1; + + } else { + // If the direction vector has a single non-zero component, we push first in the + // direction of the vector + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Then we try the opposite direction + direction[0] *= -1; + direction[1] *= -1; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Switch the direction back + direction[0] *= -1; + direction[1] *= -1; + + // If we have failed to find a push solution with the above, then we try + // to find a solution by pushing along the perpendicular axis. + + // Swap the components + int temp = direction[1]; + direction[1] = direction[0]; + direction[0] = temp; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + + // Then we try the opposite direction + direction[0] *= -1; + direction[1] *= -1; + if (pushViewsToTempLocation(intersectingViews, occupied, direction, + ignoreView, solution)) { + return true; + } + // Switch the direction back + direction[0] *= -1; + direction[1] *= -1; + + // Swap the components back + temp = direction[1]; + direction[1] = direction[0]; + direction[0] = temp; + } + return false; + } + + /* + * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between + * the provided point and the provided cell + */ + private void computeDirectionVector(float deltaX, float deltaY, int[] result) { + double angle = Math.atan(deltaY / deltaX); + + result[0] = 0; + result[1] = 0; + if (Math.abs(Math.cos(angle)) > 0.5f) { + result[0] = (int) Math.signum(deltaX); + } + if (Math.abs(Math.sin(angle)) > 0.5f) { + result[1] = (int) Math.signum(deltaY); + } + } + + /* This seems like it should be obvious and straight-forward, but when the direction vector + needs to match with the notion of the dragView pushing other views, we have to employ + a slightly more subtle notion of the direction vector. The question is what two points is + the vector between? The center of the dragView and its desired destination? Not quite, as + this doesn't necessarily coincide with the interaction of the dragView and items occupying + those cells. Instead we use some heuristics to often lock the vector to up, down, left + or right, which helps make pushing feel right. + */ + private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX, + int spanY, View dragView, int[] resultDirection) { + + //TODO(adamcohen) b/151776141 use the items visual center for the direction vector + int[] targetDestination = new int[2]; + + findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination); + Rect dragRect = new Rect(); + cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect); + dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY()); + + Rect dropRegionRect = new Rect(); + getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY, + dragView, dropRegionRect, mIntersectingViews); + + int dropRegionSpanX = dropRegionRect.width(); + int dropRegionSpanY = dropRegionRect.height(); + + cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(), + dropRegionRect.height(), dropRegionRect); + + int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX; + int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY; + + if (dropRegionSpanX == mCountX || spanX == mCountX) { + deltaX = 0; + } + if (dropRegionSpanY == mCountY || spanY == mCountY) { + deltaY = 0; + } + + if (deltaX == 0 && deltaY == 0) { + // No idea what to do, give a random direction. + resultDirection[0] = 1; + resultDirection[1] = 0; + } else { + computeDirectionVector(deltaX, deltaY, resultDirection); + } + } + + private boolean addViewsToTempLocation(ArrayList views, Rect rectOccupiedByPotentialDrop, + int[] direction, View dragView, ItemConfiguration currentState) { + if (views.size() == 0) return true; + + boolean success = false; + Rect boundingRect = new Rect(); + // We construct a rect which represents the entire group of views passed in + currentState.getBoundingRectForViews(views, boundingRect); + + // Mark the occupied state as false for the group of views we want to move. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + mTmpOccupied.markCells(c, false); + } + + GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(), boundingRect.height()); + int top = boundingRect.top; + int left = boundingRect.left; + // We mark more precisely which parts of the bounding rect are truly occupied, allowing + // for interlocking. + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true); + } + + mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true); + + findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(), + boundingRect.height(), direction, + mTmpOccupied.cells, blockOccupied.cells, mTempLocation); + + // If we successfully found a location by pushing the block of views, we commit it + if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { + int deltaX = mTempLocation[0] - boundingRect.left; + int deltaY = mTempLocation[1] - boundingRect.top; + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + c.cellX += deltaX; + c.cellY += deltaY; + } + success = true; + } + + // In either case, we set the occupied array as marked for the location of the views + for (View v: views) { + CellAndSpan c = currentState.map.get(v); + mTmpOccupied.markCells(c, true); + } + return success; + } + + private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction, + View ignoreView, ItemConfiguration solution) { + // Return early if get invalid cell positions + if (cellX < 0 || cellY < 0) return false; + + mIntersectingViews.clear(); + mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY); + + // Mark the desired location of the view currently being dragged. + if (ignoreView != null) { + CellAndSpan c = solution.map.get(ignoreView); + if (c != null) { + c.cellX = cellX; + c.cellY = cellY; + } + } + Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY); + Rect r1 = new Rect(); + for (View child: solution.map.keySet()) { + if (child == ignoreView) continue; + CellAndSpan c = solution.map.get(child); + CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); + r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY); + if (Rect.intersects(r0, r1)) { + if (!lp.canReorder) { + return false; + } + mIntersectingViews.add(child); + } + } + + solution.intersectingViews = new ArrayList<>(mIntersectingViews); + + // First we try to find a solution which respects the push mechanic. That is, + // we try to find a solution such that no displaced item travels through another item + // without also displacing that item. + if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView, + solution)) { + return true; + } + + // Next we try moving the views as a block, but without requiring the push mechanic. + if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView, + solution)) { + return true; + } + + // Ok, they couldn't move as a block, let's move them individually + for (View v : mIntersectingViews) { + if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) { + return false; + } + } + return true; + } + + private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, + int spanX, int spanY, int[] direction, View dragView, boolean decX, + ItemConfiguration solution) { + // Copy the current state into the solution. This solution will be manipulated as necessary. + copyCurrentStateToSolution(solution, false); + // Copy the current occupied array into the temporary occupied array. This array will be + // manipulated as necessary to find a solution. + mOccupied.copyTo(mTmpOccupied); + + // We find the nearest cell into which we would place the dragged item, assuming there's + // nothing in its way. + int result[] = new int[2]; + result = findNearestArea(pixelX, pixelY, spanX, spanY, result); + + boolean success; + // First we try the exact nearest position of the item being dragged, + // we will then want to try to move this around to other neighbouring positions + success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView, + solution); + + if (!success) { + // We try shrinking the widget down to size in an alternating pattern, shrink 1 in + // x, then 1 in y etc. + if (spanX > minSpanX && (minSpanY == spanY || decX)) { + return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY, + direction, dragView, false, solution); + } else if (spanY > minSpanY) { + return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1, + direction, dragView, true, solution); + } + solution.isSolution = false; + } else { + solution.isSolution = true; + solution.cellX = result[0]; + solution.cellY = result[1]; + solution.spanX = spanX; + solution.spanY = spanY; + } + return solution; + } + + private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) { + int childCount = mShortcutsAndWidgets.getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = mShortcutsAndWidgets.getChildAt(i); + CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); + CellAndSpan c; + if (temp) { + c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan); + } else { + c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan); + } + solution.add(child, c); } - performReorder(finalSolution, dragView, mode); - return result; } /** @@ -2573,6 +2535,43 @@ public class CellLayout extends ViewGroup { return null; } + int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, + View dragView, int[] result, int[] resultSpan, int mode) { + if (resultSpan == null) { + resultSpan = new int[]{-1, -1}; + } + if (result == null) { + result = new int[]{-1, -1}; + } + ItemConfiguration finalSolution; + // When we are checking drop validity or actually dropping, we don't recompute the + // direction vector, since we want the solution to match the preview, and it's possible + // that the exact position of the item has changed to result in a new reordering outcome. + if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP) + && mPreviousSolution != null) { + finalSolution = mPreviousSolution; + // We reset this vector after drop + if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { + mPreviousSolution = null; + } + } else { + finalSolution = calculateReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, + dragView); + mPreviousSolution = finalSolution; + } + + if (finalSolution == null || !finalSolution.isSolution) { + result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1; + } else { + result[0] = finalSolution.cellX; + result[1] = finalSolution.cellY; + resultSpan[0] = finalSolution.spanX; + resultSpan[1] = finalSolution.spanY; + } + performReorder(finalSolution, dragView, mode); + return result; + } + /** * Animates and submits in the DB the given ItemConfiguration depending of the mode. *