• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.Rect;
19 import android.view.View;
20 
21 import com.android.launcher3.CellLayout;
22 import com.android.launcher3.util.CellAndSpan;
23 import com.android.launcher3.util.GridOccupancy;
24 
25 import java.util.ArrayList;
26 import java.util.Comparator;
27 import java.util.List;
28 import java.util.Map.Entry;
29 import java.util.stream.Collectors;
30 
31 /**
32  * Contains the logic of a reorder.
33  *
34  * The content of this class was extracted from {@link CellLayout} and should mimic the exact
35  * same behaviour.
36  */
37 public class ReorderAlgorithm {
38 
39     CellLayout mCellLayout;
40 
ReorderAlgorithm(CellLayout cellLayout)41     public ReorderAlgorithm(CellLayout cellLayout) {
42         mCellLayout = cellLayout;
43     }
44 
45     /**
46      * This method differs from closestEmptySpaceReorder and dropInPlaceSolution because this method
47      * will move items around and will change the shape of the item if possible to try to find a
48      * solution.
49      * <p>
50      * When changing the size of the widget this method will try first subtracting -1 in the x
51      * dimension and then subtracting -1 in the y dimension until finding a possible solution or
52      * until it no longer can reduce the span.
53      * @param decX     whether it will decrease the horizontal or vertical span if it can't find a
54      *                 solution for the current span.
55      * @return the same solution variable
56      */
findReorderSolution(ReorderParameters reorderParameters, boolean decX)57     public ItemConfiguration findReorderSolution(ReorderParameters reorderParameters,
58             boolean decX) {
59         return findReorderSolution(reorderParameters, mCellLayout.mDirectionVector, decX);
60     }
61 
62     /**
63      * This method differs from closestEmptySpaceReorder and dropInPlaceSolution because this method
64      * will move items around and will change the shape of the item if possible to try to find a
65      * solution.
66      * <p>
67      * When changing the size of the widget this method will try first subtracting -1 in the x
68      * dimension and then subtracting -1 in the y dimension until finding a possible solution or
69      * until it no longer can reduce the span.
70      * @param direction Direction to attempt to push items if needed
71      * @param decX     whether it will decrease the horizontal or vertical span if it can't find a
72      *                 solution for the current span.
73      * @return the same solution variable
74      */
findReorderSolution(ReorderParameters reorderParameters, int[] direction, boolean decX)75     public ItemConfiguration findReorderSolution(ReorderParameters reorderParameters,
76             int[] direction, boolean decX) {
77         return findReorderSolutionRecursive(reorderParameters.getPixelX(),
78                 reorderParameters.getPixelY(), reorderParameters.getMinSpanX(),
79                 reorderParameters.getMinSpanY(), reorderParameters.getSpanX(),
80                 reorderParameters.getSpanY(), direction,
81                 reorderParameters.getDragView(), decX, reorderParameters.getSolution());
82     }
83 
findReorderSolutionRecursive(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution)84     private ItemConfiguration findReorderSolutionRecursive(int pixelX, int pixelY, int minSpanX,
85             int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
86             ItemConfiguration solution) {
87         // Copy the current state into the solution. This solution will be manipulated as necessary.
88         mCellLayout.copyCurrentStateToSolution(solution);
89         // Copy the current occupied array into the temporary occupied array. This array will be
90         // manipulated as necessary to find a solution.
91         mCellLayout.getOccupied().copyTo(mCellLayout.mTmpOccupied);
92 
93         // We find the nearest cell into which we would place the dragged item, assuming there's
94         // nothing in its way.
95         int[] result = new int[2];
96         result = mCellLayout.findNearestAreaIgnoreOccupied(pixelX, pixelY, spanX, spanY, result);
97 
98         boolean success;
99         // First we try the exact nearest position of the item being dragged,
100         // we will then want to try to move this around to other neighbouring positions
101         success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView,
102                 solution);
103 
104         if (!success) {
105             // We try shrinking the widget down to size in an alternating pattern, shrink 1 in
106             // x, then 1 in y etc.
107             if (spanX > minSpanX && (minSpanY == spanY || decX)) {
108                 return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX - 1,
109                         spanY, direction, dragView, false, solution);
110             } else if (spanY > minSpanY) {
111                 return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX,
112                         spanY - 1, direction, dragView, true, solution);
113             }
114             solution.isSolution = false;
115         } else {
116             solution.isSolution = true;
117             solution.cellX = result[0];
118             solution.cellY = result[1];
119             solution.spanX = spanX;
120             solution.spanY = spanY;
121         }
122         return solution;
123     }
124 
rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction, View ignoreView, ItemConfiguration solution)125     private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
126             View ignoreView, ItemConfiguration solution) {
127         // Return early if get invalid cell positions
128         if (cellX < 0 || cellY < 0) return false;
129 
130         ArrayList<View> intersectingViews = new ArrayList<>();
131         Rect occupiedRect = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
132 
133         // Mark the desired location of the view currently being dragged.
134         if (ignoreView != null) {
135             CellAndSpan c = solution.map.get(ignoreView);
136             if (c != null) {
137                 c.cellX = cellX;
138                 c.cellY = cellY;
139             }
140         }
141         Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
142         Rect r1 = new Rect();
143         // The views need to be sorted so that the results are deterministic on the views positions
144         // and not by the views hash which is "random".
145         // The views are sorted twice, once for the X position and a second time for the Y position
146         // to ensure same order everytime.
147         Comparator<View> comparator = Comparator.comparing(
148                 (View view) -> ((CellLayoutLayoutParams) view.getLayoutParams()).getCellX()
149         ).thenComparing(
150                 (View view) -> ((CellLayoutLayoutParams) view.getLayoutParams()).getCellY()
151         );
152         List<View> views = solution.map.keySet().stream()
153                 .sorted(comparator)
154                 .collect(Collectors.toList());
155         for (View child : views) {
156             if (child == ignoreView) continue;
157             CellAndSpan c = solution.map.get(child);
158             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
159             r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
160             if (Rect.intersects(r0, r1)) {
161                 if (!lp.canReorder) {
162                     return false;
163                 }
164                 intersectingViews.add(child);
165             }
166         }
167 
168         solution.intersectingViews = intersectingViews;
169 
170         // First we try to find a solution which respects the push mechanic. That is,
171         // we try to find a solution such that no displaced item travels through another item
172         // without also displacing that item.
173         if (attemptPushInDirection(intersectingViews, occupiedRect, direction, ignoreView,
174                 solution)) {
175             return true;
176         }
177 
178         // Next we try moving the views as a block, but without requiring the push mechanic.
179         if (addViewsToTempLocation(intersectingViews, occupiedRect, direction, ignoreView,
180                 solution)) {
181             return true;
182         }
183 
184         // Ok, they couldn't move as a block, let's move them individually
185         for (View v : intersectingViews) {
186             if (!addViewToTempLocation(v, occupiedRect, direction, solution)) {
187                 return false;
188             }
189         }
190         return true;
191     }
192 
addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop, int[] direction, ItemConfiguration currentState)193     private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop, int[] direction,
194             ItemConfiguration currentState) {
195         CellAndSpan c = currentState.map.get(v);
196         boolean success = false;
197         mCellLayout.mTmpOccupied.markCells(c, false);
198         mCellLayout.mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
199 
200         int[] tmpLocation = findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction,
201                 mCellLayout.mTmpOccupied.cells, null, new int[2]);
202 
203         if (tmpLocation[0] >= 0 && tmpLocation[1] >= 0) {
204             c.cellX = tmpLocation[0];
205             c.cellY = tmpLocation[1];
206             success = true;
207         }
208         mCellLayout.mTmpOccupied.markCells(c, true);
209         return success;
210     }
211 
pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop, int[] direction, View dragView, ItemConfiguration currentState)212     private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
213             int[] direction, View dragView, ItemConfiguration currentState) {
214 
215         ViewCluster cluster = new ViewCluster(mCellLayout, views, currentState);
216         Rect clusterRect = cluster.getBoundingRect();
217         int whichEdge;
218         int pushDistance;
219         boolean fail = false;
220 
221         // Determine the edge of the cluster that will be leading the push and how far
222         // the cluster must be shifted.
223         if (direction[0] < 0) {
224             whichEdge = ViewCluster.LEFT;
225             pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left;
226         } else if (direction[0] > 0) {
227             whichEdge = ViewCluster.RIGHT;
228             pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left;
229         } else if (direction[1] < 0) {
230             whichEdge = ViewCluster.TOP;
231             pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top;
232         } else {
233             whichEdge = ViewCluster.BOTTOM;
234             pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top;
235         }
236 
237         // Break early for invalid push distance.
238         if (pushDistance <= 0) {
239             return false;
240         }
241 
242         // Mark the occupied state as false for the group of views we want to move.
243         for (View v : views) {
244             CellAndSpan c = currentState.map.get(v);
245             mCellLayout.mTmpOccupied.markCells(c, false);
246         }
247 
248         // We save the current configuration -- if we fail to find a solution we will revert
249         // to the initial state. The process of finding a solution modifies the configuration
250         // in place, hence the need for revert in the failure case.
251         currentState.save();
252 
253         // The pushing algorithm is simplified by considering the views in the order in which
254         // they would be pushed by the cluster. For example, if the cluster is leading with its
255         // left edge, we consider sort the views by their right edge, from right to left.
256         cluster.sortConfigurationForEdgePush(whichEdge);
257 
258         while (pushDistance > 0 && !fail) {
259             for (View v : currentState.sortedViews) {
260                 // For each view that isn't in the cluster, we see if the leading edge of the
261                 // cluster is contacting the edge of that view. If so, we add that view to the
262                 // cluster.
263                 if (!cluster.views.contains(v) && v != dragView) {
264                     if (cluster.isViewTouchingEdge(v, whichEdge)) {
265                         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) v.getLayoutParams();
266                         if (!lp.canReorder) {
267                             // The push solution includes the all apps button, this is not viable.
268                             fail = true;
269                             break;
270                         }
271                         cluster.addView(v);
272                         CellAndSpan c = currentState.map.get(v);
273 
274                         // Adding view to cluster, mark it as not occupied.
275                         mCellLayout.mTmpOccupied.markCells(c, false);
276                     }
277                 }
278             }
279             pushDistance--;
280 
281             // The cluster has been completed, now we move the whole thing over in the appropriate
282             // direction.
283             cluster.shift(whichEdge, 1);
284         }
285 
286         boolean foundSolution = false;
287         clusterRect = cluster.getBoundingRect();
288 
289         // Due to the nature of the algorithm, the only check required to verify a valid solution
290         // is to ensure that completed shifted cluster lies completely within the cell layout.
291         if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCellLayout.getCountX()
292                 && clusterRect.top >= 0 && clusterRect.bottom <= mCellLayout.getCountY()) {
293             foundSolution = true;
294         } else {
295             currentState.restore();
296         }
297 
298         // In either case, we set the occupied array as marked for the location of the views
299         for (View v : cluster.views) {
300             CellAndSpan c = currentState.map.get(v);
301             mCellLayout.mTmpOccupied.markCells(c, true);
302         }
303 
304         return foundSolution;
305     }
306 
revertDir(int[] direction)307     private void revertDir(int[] direction) {
308         direction[0] *= -1;
309         direction[1] *= -1;
310     }
311 
312     // This method tries to find a reordering solution which satisfies the push mechanic by trying
313     // to push items in each of the cardinal directions, in an order based on the direction vector
314     // passed.
attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied, int[] direction, View ignoreView, ItemConfiguration solution)315     private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied,
316             int[] direction, View ignoreView, ItemConfiguration solution) {
317         if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
318             // If the direction vector has two non-zero components, we try pushing
319             // separately in each of the components.
320             int temp;
321             for (int j = 0; j < 2; j++) {
322                 for (int i = 1; i >= 0; i--) {
323                     temp = direction[i];
324                     direction[i] = 0;
325                     if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
326                             solution)) {
327                         return true;
328                     }
329                     direction[i] = temp;
330                 }
331                 revertDir(direction);
332             }
333         } else {
334             // If the direction vector has a single non-zero component, we push first in the
335             // direction of the vector
336             int temp;
337             for (int j = 0; j < 2; j++) {
338                 for (int i = 0; i < 2; i++) {
339                     if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
340                             solution)) {
341                         return true;
342                     }
343                     revertDir(direction);
344                 }
345                 // Swap the components
346                 temp = direction[1];
347                 direction[1] = direction[0];
348                 direction[0] = temp;
349             }
350         }
351         return false;
352     }
353 
addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop, int[] direction, View dragView, ItemConfiguration currentState)354     private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
355             int[] direction, View dragView, ItemConfiguration currentState) {
356         if (views.isEmpty()) return true;
357 
358         boolean success = false;
359         Rect boundingRect = new Rect();
360         // We construct a rect which represents the entire group of views passed in
361         currentState.getBoundingRectForViews(views, boundingRect);
362 
363         // Mark the occupied state as false for the group of views we want to move.
364         for (View v : views) {
365             CellAndSpan c = currentState.map.get(v);
366             mCellLayout.mTmpOccupied.markCells(c, false);
367         }
368 
369         GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(),
370                 boundingRect.height());
371         int top = boundingRect.top;
372         int left = boundingRect.left;
373         // We mark more precisely which parts of the bounding rect are truly occupied, allowing
374         // for interlocking.
375         for (View v : views) {
376             CellAndSpan c = currentState.map.get(v);
377             blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true);
378         }
379 
380         mCellLayout.mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
381 
382         int[] tmpLocation = findNearestArea(boundingRect.left, boundingRect.top,
383                 boundingRect.width(), boundingRect.height(), direction,
384                 mCellLayout.mTmpOccupied.cells, blockOccupied.cells, new int[2]);
385 
386         // If we successfully found a location by pushing the block of views, we commit it
387         if (tmpLocation[0] >= 0 && tmpLocation[1] >= 0) {
388             int deltaX = tmpLocation[0] - boundingRect.left;
389             int deltaY = tmpLocation[1] - boundingRect.top;
390             for (View v : views) {
391                 CellAndSpan c = currentState.map.get(v);
392                 c.cellX += deltaX;
393                 c.cellY += deltaY;
394             }
395             success = true;
396         }
397 
398         // In either case, we set the occupied array as marked for the location of the views
399         for (View v : views) {
400             CellAndSpan c = currentState.map.get(v);
401             mCellLayout.mTmpOccupied.markCells(c, true);
402         }
403         return success;
404     }
405 
406     /**
407      * Returns a "reorder" if there is empty space without rearranging anything.
408      *
409      * @return the configuration that represents the found reorder
410      */
dropInPlaceSolution(ReorderParameters reorderParameters)411     public ItemConfiguration dropInPlaceSolution(ReorderParameters reorderParameters) {
412         int[] result = mCellLayout.findNearestAreaIgnoreOccupied(reorderParameters.getPixelX(),
413                 reorderParameters.getPixelY(), reorderParameters.getSpanX(),
414                 reorderParameters.getSpanY(), new int[2]);
415         ItemConfiguration solution = new ItemConfiguration();
416         mCellLayout.copyCurrentStateToSolution(solution);
417 
418         solution.isSolution = !isConfigurationRegionOccupied(
419                 new Rect(result[0], result[1], result[0] + reorderParameters.getSpanX(),
420                         result[1] + reorderParameters.getSpanY()), solution,
421                 reorderParameters.getDragView());
422         if (!solution.isSolution) {
423             return solution;
424         }
425         solution.cellX = result[0];
426         solution.cellY = result[1];
427         solution.spanX = reorderParameters.getSpanX();
428         solution.spanY = reorderParameters.getSpanY();
429         return solution;
430     }
431 
isConfigurationRegionOccupied(Rect region, ItemConfiguration configuration, View ignoreView)432     private boolean isConfigurationRegionOccupied(Rect region, ItemConfiguration configuration,
433             View ignoreView) {
434         return configuration.map
435                 .entrySet()
436                 .stream()
437                 .filter(entry -> entry.getKey() != ignoreView)
438                 .map(Entry::getValue)
439                 .anyMatch(cellAndSpan -> region.intersect(
440                         cellAndSpan.cellX,
441                         cellAndSpan.cellY,
442                         cellAndSpan.cellX + cellAndSpan.spanX,
443                         cellAndSpan.cellY + cellAndSpan.spanY
444                         )
445                 );
446     }
447 
448     /**
449      * Returns a "reorder" where we simply drop the item in the closest empty space, without moving
450      * any other item in the way.
451      *
452      * @return the configuration that represents the found reorder
453      */
closestEmptySpaceReorder(ReorderParameters reorderParameters)454     public ItemConfiguration closestEmptySpaceReorder(ReorderParameters reorderParameters) {
455         ItemConfiguration solution = new ItemConfiguration();
456         int[] result = new int[2];
457         int[] resultSpan = new int[2];
458         mCellLayout.findNearestVacantArea(reorderParameters.getPixelX(),
459                 reorderParameters.getPixelY(), reorderParameters.getMinSpanX(),
460                 reorderParameters.getMinSpanY(), reorderParameters.getSpanX(),
461                 reorderParameters.getSpanY(), result, resultSpan);
462         if (result[0] >= 0 && result[1] >= 0) {
463             mCellLayout.copyCurrentStateToSolution(solution);
464             solution.cellX = result[0];
465             solution.cellY = result[1];
466             solution.spanX = resultSpan[0];
467             solution.spanY = resultSpan[1];
468             solution.isSolution = true;
469         } else {
470             solution.isSolution = false;
471         }
472         return solution;
473     }
474 
475     /**
476      * When the user drags an Item in the workspace sometimes we need to move the items already in
477      * the workspace to make space for the new item, this function return a solution for that
478      * reorder.
479      *
480      * @return returns a solution for the given parameters, the solution contains all the icons and
481      * the locations they should be in the given solution.
482      */
calculateReorder(ReorderParameters reorderParameters)483     public ItemConfiguration calculateReorder(ReorderParameters reorderParameters) {
484         getDirectionVectorForDrop(reorderParameters, mCellLayout.mDirectionVector);
485 
486         ItemConfiguration dropInPlaceSolution = dropInPlaceSolution(reorderParameters);
487 
488         // Find a solution involving pushing / displacing any items in the way
489         ItemConfiguration swapSolution = findReorderSolution(reorderParameters, true);
490 
491         // We attempt the approach which doesn't shuffle views at all
492         ItemConfiguration closestSpaceSolution = closestEmptySpaceReorder(reorderParameters);
493 
494         // If the reorder solution requires resizing (shrinking) the item being dropped, we instead
495         // favor a solution in which the item is not resized, but
496         if (swapSolution.isSolution && swapSolution.area() >= closestSpaceSolution.area()) {
497             return swapSolution;
498         } else if (closestSpaceSolution.isSolution) {
499             return closestSpaceSolution;
500         } else if (dropInPlaceSolution.isSolution) {
501             return dropInPlaceSolution;
502         }
503         return null;
504     }
505 
506     /*
507      * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between
508      * the provided point and the provided cell
509      */
computeDirectionVector(float deltaX, float deltaY, int[] result)510     private void computeDirectionVector(float deltaX, float deltaY, int[] result) {
511         double angle = Math.atan(deltaY / deltaX);
512 
513         result[0] = 0;
514         result[1] = 0;
515         if (Math.abs(Math.cos(angle)) > 0.5f) {
516             result[0] = (int) Math.signum(deltaX);
517         }
518         if (Math.abs(Math.sin(angle)) > 0.5f) {
519             result[1] = (int) Math.signum(deltaY);
520         }
521     }
522 
523     /**
524      * This seems like it should be obvious and straight-forward, but when the direction vector
525      * needs to match with the notion of the dragView pushing other views, we have to employ
526      * a slightly more subtle notion of the direction vector. The question is what two points is
527      * the vector between? The center of the dragView and its desired destination? Not quite, as
528      * this doesn't necessarily coincide with the interaction of the dragView and items occupying
529      * those cells. Instead we use some heuristics to often lock the vector to up, down, left
530      * or right, which helps make pushing feel right.
531      */
getDirectionVectorForDrop(ReorderParameters reorderParameters, int[] resultDirection)532     public void getDirectionVectorForDrop(ReorderParameters reorderParameters,
533             int[] resultDirection) {
534 
535         //TODO(adamcohen) b/151776141 use the items visual center for the direction vector
536         int[] targetDestination = new int[2];
537 
538         mCellLayout.findNearestAreaIgnoreOccupied(reorderParameters.getPixelX(),
539                 reorderParameters.getPixelY(), reorderParameters.getSpanX(),
540                 reorderParameters.getSpanY(), targetDestination);
541         Rect dragRect = new Rect();
542         mCellLayout.cellToRect(targetDestination[0], targetDestination[1],
543                 reorderParameters.getSpanX(), reorderParameters.getSpanY(), dragRect);
544         dragRect.offset(reorderParameters.getPixelX() - dragRect.centerX(),
545                 reorderParameters.getPixelY() - dragRect.centerY());
546 
547         Rect region = new Rect(targetDestination[0], targetDestination[1],
548                 targetDestination[0] + reorderParameters.getSpanX(),
549                 targetDestination[1] + reorderParameters.getSpanY());
550         Rect dropRegionRect = mCellLayout.getIntersectingRectanglesInRegion(region,
551                 reorderParameters.getDragView());
552         if (dropRegionRect == null) dropRegionRect = new Rect(region);
553 
554         int dropRegionSpanX = dropRegionRect.width();
555         int dropRegionSpanY = dropRegionRect.height();
556 
557         mCellLayout.cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
558                 dropRegionRect.height(), dropRegionRect);
559 
560         int deltaX = (dropRegionRect.centerX() - reorderParameters.getPixelX())
561                 / reorderParameters.getSpanX();
562         int deltaY = (dropRegionRect.centerY() - reorderParameters.getPixelY())
563                 / reorderParameters.getSpanY();
564 
565         if (dropRegionSpanX == mCellLayout.getCountX()
566                 || reorderParameters.getSpanX() == mCellLayout.getCountX()) {
567             deltaX = 0;
568         }
569         if (dropRegionSpanY == mCellLayout.getCountY()
570                 || reorderParameters.getSpanY() == mCellLayout.getCountY()) {
571             deltaY = 0;
572         }
573 
574         if (deltaX == 0 && deltaY == 0) {
575             // No idea what to do, give a random direction.
576             resultDirection[0] = 1;
577             resultDirection[1] = 0;
578         } else {
579             computeDirectionVector(deltaX, deltaY, resultDirection);
580         }
581     }
582 
583     /**
584      * Find a vacant area that will fit the given bounds nearest the requested
585      * cell location, and will also weigh in a suggested direction vector of the
586      * desired location. This method computers distance based on unit grid distances,
587      * not pixel distances.
588      *
589      * @param cellX         The X cell nearest to which you want to search for a vacant area.
590      * @param cellY         The Y cell nearest which you want to search for a vacant area.
591      * @param spanX         Horizontal span of the object.
592      * @param spanY         Vertical span of the object.
593      * @param direction     The favored direction in which the views should move from x, y
594      * @param occupied      The array which represents which cells in the CellLayout are occupied
595      * @param blockOccupied The array which represents which cells in the specified block (cellX,
596      *                      cellY, spanX, spanY) are occupied. This is used when try to move a group
597      *                      of views.
598      * @param result        Array in which to place the result, or null (in which case a new array
599      *                      will
600      *                      be allocated)
601      * @return The X, Y cell of a vacant area that can contain this object,
602      * nearest the requested location.
603      */
findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction, boolean[][] occupied, boolean[][] blockOccupied, int[] result)604     public int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction,
605             boolean[][] occupied, boolean[][] blockOccupied, int[] result) {
606         // Keep track of best-scoring drop area
607         final int[] bestXY = result != null ? result : new int[2];
608         float bestDistance = Float.MAX_VALUE;
609         int bestDirectionScore = Integer.MIN_VALUE;
610 
611         final int countX = mCellLayout.getCountX();
612         final int countY = mCellLayout.getCountY();
613 
614         for (int y = 0; y < countY - (spanY - 1); y++) {
615             inner:
616             for (int x = 0; x < countX - (spanX - 1); x++) {
617                 // First, let's see if this thing fits anywhere
618                 for (int i = 0; i < spanX; i++) {
619                     for (int j = 0; j < spanY; j++) {
620                         if (occupied[x + i][y + j] && (blockOccupied == null
621                                 || blockOccupied[i][j])) {
622                             continue inner;
623                         }
624                     }
625                 }
626 
627                 float distance = (float) Math.hypot(x - cellX, y - cellY);
628                 int[] curDirection = new int[2];
629                 computeDirectionVector(x - cellX, y - cellY, curDirection);
630                 // The direction score is just the dot product of the two candidate direction
631                 // and that passed in.
632                 int curDirectionScore =
633                         direction[0] * curDirection[0] + direction[1] * curDirection[1];
634                 if (Float.compare(distance, bestDistance) < 0 || (Float.compare(distance,
635                         bestDistance) == 0 && curDirectionScore > bestDirectionScore)) {
636                     bestDistance = distance;
637                     bestDirectionScore = curDirectionScore;
638                     bestXY[0] = x;
639                     bestXY[1] = y;
640                 }
641             }
642         }
643 
644         // Return -1, -1 if no suitable location found
645         if (bestDistance == Float.MAX_VALUE) {
646             bestXY[0] = -1;
647             bestXY[1] = -1;
648         }
649         return bestXY;
650     }
651 }
652