Merge "Decoupling the reorder logic from the CellLayout view" into tm-qpr-dev am: 90ff89e2b9
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Launcher3/+/20832424 Change-Id: I888958a5ea8890111506857c90c62a89ded16ba7 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -65,6 +65,7 @@ import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
|
||||
import com.android.launcher3.anim.Interpolators;
|
||||
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
|
||||
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
|
||||
import com.android.launcher3.celllayout.ReorderAlgorithm;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.dragndrop.DraggableView;
|
||||
import com.android.launcher3.folder.PreviewBackground;
|
||||
@@ -115,7 +116,7 @@ public class CellLayout extends ViewGroup {
|
||||
final PointF mTmpPointF = new PointF();
|
||||
|
||||
protected GridOccupancy mOccupied;
|
||||
protected GridOccupancy mTmpOccupied;
|
||||
public GridOccupancy mTmpOccupied;
|
||||
|
||||
private OnTouchListener mInterceptTouchListener;
|
||||
|
||||
@@ -194,7 +195,7 @@ public class CellLayout extends ViewGroup {
|
||||
|
||||
private final ArrayList<View> mIntersectingViews = new ArrayList<>();
|
||||
private final Rect mOccupiedRect = new Rect();
|
||||
private final int[] mDirectionVector = new int[2];
|
||||
public final int[] mDirectionVector = new int[2];
|
||||
|
||||
ItemConfiguration mPreviousSolution = null;
|
||||
private static final int INVALID_DIRECTION = -100;
|
||||
@@ -1246,8 +1247,8 @@ public class CellLayout extends ViewGroup {
|
||||
* @return The X, Y cell of a vacant area that can contain this object,
|
||||
* nearest the requested location.
|
||||
*/
|
||||
int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX,
|
||||
int spanY, int[] result, int[] resultSpan) {
|
||||
public int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY,
|
||||
int spanX, int spanY, int[] result, int[] resultSpan) {
|
||||
return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, false,
|
||||
result, resultSpan);
|
||||
}
|
||||
@@ -1380,6 +1381,10 @@ public class CellLayout extends ViewGroup {
|
||||
return bestXY;
|
||||
}
|
||||
|
||||
public GridOccupancy getOccupied() {
|
||||
return mOccupied;
|
||||
}
|
||||
|
||||
private void copySolutionToTempState(ItemConfiguration solution, View dragView) {
|
||||
mTmpOccupied.clear();
|
||||
|
||||
@@ -1655,38 +1660,8 @@ public class CellLayout extends ViewGroup {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a "reorder" where we simply drop the item in the closest empty space, without moving
|
||||
* any other item in the way.
|
||||
*
|
||||
* @param pixelX X coordinate in pixels in the screen
|
||||
* @param pixelY Y coordinate in pixels in the screen
|
||||
* @param spanX horizontal cell span
|
||||
* @param spanY vertical cell span
|
||||
* @return the configuration that represents the found reorder
|
||||
*/
|
||||
ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, int minSpanX,
|
||||
int minSpanY, int spanX, int spanY) {
|
||||
ItemConfiguration solution = new ItemConfiguration();
|
||||
int[] result = new int[2];
|
||||
int[] resultSpan = new int[2];
|
||||
findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, result,
|
||||
resultSpan);
|
||||
if (result[0] >= 0 && result[1] >= 0) {
|
||||
copyCurrentStateToSolution(solution, false);
|
||||
solution.cellX = result[0];
|
||||
solution.cellY = result[1];
|
||||
solution.spanX = resultSpan[0];
|
||||
solution.spanY = resultSpan[1];
|
||||
solution.isSolution = true;
|
||||
} else {
|
||||
solution.isSolution = false;
|
||||
}
|
||||
return solution;
|
||||
}
|
||||
|
||||
// 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,
|
||||
public void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY,
|
||||
View dragView, Rect boundingRect, ArrayList<View> intersectingViews) {
|
||||
if (boundingRect != null) {
|
||||
boundingRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
|
||||
@@ -1711,7 +1686,7 @@ public class CellLayout extends ViewGroup {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY,
|
||||
public boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY,
|
||||
View dragView, int[] result) {
|
||||
result = findNearestAreaIgnoreOccupied(pixelX, pixelY, spanX, spanY, result);
|
||||
getViewsIntersectingRegion(result[0], result[1], spanX, spanY, dragView, null,
|
||||
@@ -2257,7 +2232,7 @@ public class CellLayout extends ViewGroup {
|
||||
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,
|
||||
public 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
|
||||
@@ -2349,7 +2324,7 @@ public class CellLayout extends ViewGroup {
|
||||
return success;
|
||||
}
|
||||
|
||||
private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
|
||||
public 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;
|
||||
@@ -2405,55 +2380,18 @@ public class CellLayout extends ViewGroup {
|
||||
return true;
|
||||
}
|
||||
|
||||
public ReorderAlgorithm createReorderAlgorithm() {
|
||||
return new ReorderAlgorithm(this);
|
||||
}
|
||||
|
||||
protected ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX,
|
||||
int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
|
||||
ItemConfiguration solution) {
|
||||
return findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
|
||||
direction, dragView, decX, solution);
|
||||
return createReorderAlgorithm().findReorderSolution(pixelX, pixelY, minSpanX, minSpanY,
|
||||
spanX, spanY, direction, dragView, decX, solution);
|
||||
}
|
||||
|
||||
protected ItemConfiguration findReorderSolutionRecursive(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 = findNearestAreaIgnoreOccupied(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 findReorderSolutionRecursive(pixelX, pixelY, minSpanX, minSpanY, spanX - 1,
|
||||
spanY, direction, dragView, false, solution);
|
||||
} else if (spanY > minSpanY) {
|
||||
return findReorderSolutionRecursive(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;
|
||||
}
|
||||
|
||||
protected void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
|
||||
public void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
|
||||
int childCount = mShortcutsAndWidgets.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View child = mShortcutsAndWidgets.getChildAt(i);
|
||||
@@ -2468,35 +2406,6 @@ public class CellLayout extends ViewGroup {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a "reorder" if there is empty space without rearranging anything.
|
||||
*
|
||||
* @param pixelX X coordinate in pixels in the screen
|
||||
* @param pixelY Y coordinate in pixels in the screen
|
||||
* @param spanX horizontal cell span
|
||||
* @param spanY vertical cell span
|
||||
* @param dragView view being dragged in reorder
|
||||
* @return the configuration that represents the found reorder
|
||||
*/
|
||||
public ItemConfiguration dropInPlaceSolution(int pixelX, int pixelY, int spanX,
|
||||
int spanY, View dragView) {
|
||||
int[] result = new int[2];
|
||||
if (isNearestDropLocationOccupied(pixelX, pixelY, spanX, spanY, dragView, result)) {
|
||||
result[0] = result[1] = -1;
|
||||
}
|
||||
ItemConfiguration solution = new ItemConfiguration();
|
||||
copyCurrentStateToSolution(solution, false);
|
||||
solution.isSolution = result[0] != -1;
|
||||
if (!solution.isSolution) {
|
||||
return solution;
|
||||
}
|
||||
solution.cellX = result[0];
|
||||
solution.cellY = result[1];
|
||||
solution.spanX = spanX;
|
||||
solution.spanY = spanY;
|
||||
return solution;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user drags an Item in the workspace sometimes we need to move the items already in
|
||||
* the workspace to make space for the new item, this function return a solution for that
|
||||
@@ -2514,29 +2423,8 @@ public class CellLayout extends ViewGroup {
|
||||
*/
|
||||
public ItemConfiguration calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY,
|
||||
int spanX, int spanY, View dragView) {
|
||||
getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector);
|
||||
|
||||
ItemConfiguration dropInPlaceSolution = dropInPlaceSolution(pixelX, pixelY, spanX, spanY,
|
||||
dragView);
|
||||
|
||||
// Find a solution involving pushing / displacing any items in the way
|
||||
ItemConfiguration swapSolution = findReorderSolution(pixelX, pixelY, minSpanX, minSpanY,
|
||||
spanX, spanY, mDirectionVector, dragView, true, new ItemConfiguration());
|
||||
|
||||
// We attempt the approach which doesn't shuffle views at all
|
||||
ItemConfiguration closestSpaceSolution = closestEmptySpaceReorder(pixelX, pixelY, minSpanX,
|
||||
minSpanY, spanX, spanY);
|
||||
|
||||
// If the reorder solution requires resizing (shrinking) the item being dropped, we instead
|
||||
// favor a solution in which the item is not resized, but
|
||||
if (swapSolution.isSolution && swapSolution.area() >= closestSpaceSolution.area()) {
|
||||
return swapSolution;
|
||||
} else if (closestSpaceSolution.isSolution) {
|
||||
return closestSpaceSolution;
|
||||
} else if (dropInPlaceSolution.isSolution) {
|
||||
return dropInPlaceSolution;
|
||||
}
|
||||
return null;
|
||||
return createReorderAlgorithm().calculateReorder(pixelX, pixelY, minSpanX, minSpanY,
|
||||
spanX, spanY, dragView);
|
||||
}
|
||||
|
||||
int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
|
||||
@@ -2588,7 +2476,7 @@ public class CellLayout extends ViewGroup {
|
||||
* {@link MODE_ON_DROP}, {@link MODE_ON_DROP_EXTERNAL}, {@link MODE_ACCEPT_DROP}
|
||||
* defined in {@link CellLayout}.
|
||||
*/
|
||||
void performReorder(ItemConfiguration solution, View dragView, int mode) {
|
||||
public void performReorder(ItemConfiguration solution, View dragView, int mode) {
|
||||
if (mode == MODE_SHOW_REORDER_HINT) {
|
||||
beginOrAdjustReorderPreviewAnimations(solution, dragView,
|
||||
ReorderPreviewAnimation.MODE_HINT);
|
||||
@@ -2634,38 +2522,41 @@ public class CellLayout extends ViewGroup {
|
||||
return mItemPlacementDirty;
|
||||
}
|
||||
|
||||
static class ItemConfiguration extends CellAndSpan {
|
||||
final ArrayMap<View, CellAndSpan> map = new ArrayMap<>();
|
||||
/**
|
||||
* Represents the solution to a reorder of items in the Workspace.
|
||||
*/
|
||||
public static class ItemConfiguration extends CellAndSpan {
|
||||
public final ArrayMap<View, CellAndSpan> map = new ArrayMap<>();
|
||||
private final ArrayMap<View, CellAndSpan> savedMap = new ArrayMap<>();
|
||||
final ArrayList<View> sortedViews = new ArrayList<>();
|
||||
ArrayList<View> intersectingViews;
|
||||
boolean isSolution = false;
|
||||
public final ArrayList<View> sortedViews = new ArrayList<>();
|
||||
public ArrayList<View> intersectingViews;
|
||||
public boolean isSolution = false;
|
||||
|
||||
void save() {
|
||||
public void save() {
|
||||
// Copy current state into savedMap
|
||||
for (View v: map.keySet()) {
|
||||
savedMap.get(v).copyFrom(map.get(v));
|
||||
}
|
||||
}
|
||||
|
||||
void restore() {
|
||||
public void restore() {
|
||||
// Restore current state from savedMap
|
||||
for (View v: savedMap.keySet()) {
|
||||
map.get(v).copyFrom(savedMap.get(v));
|
||||
}
|
||||
}
|
||||
|
||||
void add(View v, CellAndSpan cs) {
|
||||
public void add(View v, CellAndSpan cs) {
|
||||
map.put(v, cs);
|
||||
savedMap.put(v, new CellAndSpan());
|
||||
sortedViews.add(v);
|
||||
}
|
||||
|
||||
int area() {
|
||||
public int area() {
|
||||
return spanX * spanY;
|
||||
}
|
||||
|
||||
void getBoundingRectForViews(ArrayList<View> views, Rect outRect) {
|
||||
public void getBoundingRectForViews(ArrayList<View> views, Rect outRect) {
|
||||
boolean first = true;
|
||||
for (View v: views) {
|
||||
CellAndSpan c = map.get(v);
|
||||
|
||||
@@ -23,11 +23,11 @@ import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
|
||||
import com.android.launcher3.celllayout.MulticellReorderAlgorithm;
|
||||
import com.android.launcher3.celllayout.ReorderAlgorithm;
|
||||
import com.android.launcher3.util.CellAndSpan;
|
||||
import com.android.launcher3.util.GridOccupancy;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* CellLayout that simulates a split in the middle for use in foldable devices.
|
||||
*/
|
||||
@@ -36,8 +36,6 @@ public class MultipageCellLayout extends CellLayout {
|
||||
private final Drawable mLeftBackground;
|
||||
private final Drawable mRightBackground;
|
||||
|
||||
private View mSeam;
|
||||
|
||||
private boolean mSeamWasAdded = false;
|
||||
|
||||
public MultipageCellLayout(Context context) {
|
||||
@@ -62,7 +60,6 @@ public class MultipageCellLayout extends CellLayout {
|
||||
|
||||
mCountX = deviceProfile.inv.numColumns * 2;
|
||||
mCountY = deviceProfile.inv.numRows;
|
||||
mSeam = new View(getContext());
|
||||
setGridSize(mCountX, mCountY);
|
||||
}
|
||||
|
||||
@@ -74,90 +71,18 @@ public class MultipageCellLayout extends CellLayout {
|
||||
cellX++;
|
||||
}
|
||||
int finalCellX = cellX;
|
||||
return simulateSeam(
|
||||
return ((MulticellReorderAlgorithm) createReorderAlgorithm()).simulateSeam(
|
||||
() -> super.createAreaForResize(finalCellX, cellY, spanX, spanY, dragView,
|
||||
direction, commit));
|
||||
}
|
||||
|
||||
@Override
|
||||
ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, int minSpanX, int minSpanY,
|
||||
int spanX, int spanY) {
|
||||
return removeSeamFromSolution(simulateSeam(
|
||||
() -> super.closestEmptySpaceReorder(pixelX, pixelY, minSpanX, minSpanY, spanX,
|
||||
spanY)));
|
||||
public ReorderAlgorithm createReorderAlgorithm() {
|
||||
return new MulticellReorderAlgorithm(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX,
|
||||
int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
|
||||
ItemConfiguration solution) {
|
||||
return removeSeamFromSolution(simulateSeam(
|
||||
() -> super.findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
|
||||
direction, dragView, decX, solution)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemConfiguration dropInPlaceSolution(int pixelX, int pixelY, int spanX, int spanY,
|
||||
View dragView) {
|
||||
return removeSeamFromSolution(simulateSeam(
|
||||
() -> super.dropInPlaceSolution(pixelX, pixelY, spanX, spanY, dragView)));
|
||||
}
|
||||
|
||||
void addSeam() {
|
||||
CellLayoutLayoutParams lp = new CellLayoutLayoutParams(mCountX / 2, 0, 1, mCountY);
|
||||
mSeamWasAdded = true;
|
||||
lp.canReorder = false;
|
||||
mCountX++;
|
||||
mShortcutsAndWidgets.addViewInLayout(mSeam, lp);
|
||||
mOccupied = createGridOccupancyWithSeam(mOccupied);
|
||||
mTmpOccupied = new GridOccupancy(mCountX, mCountY);
|
||||
}
|
||||
|
||||
void removeSeam() {
|
||||
mCountX--;
|
||||
mShortcutsAndWidgets.removeViewInLayout(mSeam);
|
||||
mTmpOccupied = new GridOccupancy(mCountX, mCountY);
|
||||
mSeamWasAdded = false;
|
||||
}
|
||||
|
||||
protected <T> T simulateSeam(Supplier<T> f) {
|
||||
if (mSeamWasAdded) {
|
||||
return f.get();
|
||||
}
|
||||
GridOccupancy auxGrid = mOccupied;
|
||||
addSeam();
|
||||
T res = f.get();
|
||||
removeSeam();
|
||||
mOccupied = auxGrid;
|
||||
return res;
|
||||
}
|
||||
|
||||
private ItemConfiguration removeSeamFromSolution(ItemConfiguration solution) {
|
||||
solution.map.forEach((view, cell) -> cell.cellX = cell.cellX > mCountX / 2
|
||||
? cell.cellX - 1 : cell.cellX);
|
||||
solution.cellX = solution.cellX > mCountX / 2 ? solution.cellX - 1 : solution.cellX;
|
||||
return solution;
|
||||
}
|
||||
|
||||
|
||||
|
||||
GridOccupancy createGridOccupancyWithSeam(GridOccupancy gridOccupancy) {
|
||||
GridOccupancy grid = new GridOccupancy(getCountX(), getCountY());
|
||||
for (int x = 0; x < getCountX(); x++) {
|
||||
for (int y = 0; y < getCountY(); y++) {
|
||||
int offset = x >= getCountX() / 2 ? 1 : 0;
|
||||
if (x == getCountX() / 2) {
|
||||
grid.cells[x][y] = true;
|
||||
} else {
|
||||
grid.cells[x][y] = gridOccupancy.cells[x - offset][y];
|
||||
}
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
|
||||
public void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
|
||||
int childCount = mShortcutsAndWidgets.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View child = mShortcutsAndWidgets.getChildAt(i);
|
||||
@@ -196,4 +121,24 @@ public class MultipageCellLayout extends CellLayout {
|
||||
mLeftBackground.setBounds(rect.left, rect.top, rect.right / 2 - 20, rect.bottom);
|
||||
mRightBackground.setBounds(rect.right / 2 + 20, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
|
||||
public void setCountX(int countX) {
|
||||
mCountX = countX;
|
||||
}
|
||||
|
||||
public void setCountY(int countY) {
|
||||
mCountY = countY;
|
||||
}
|
||||
|
||||
public void setOccupied(GridOccupancy occupied) {
|
||||
mOccupied = occupied;
|
||||
}
|
||||
|
||||
public boolean isSeamWasAdded() {
|
||||
return mSeamWasAdded;
|
||||
}
|
||||
|
||||
public void setSeamWasAdded(boolean seamWasAdded) {
|
||||
mSeamWasAdded = seamWasAdded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.celllayout;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.android.launcher3.CellLayout;
|
||||
import com.android.launcher3.MultipageCellLayout;
|
||||
import com.android.launcher3.util.GridOccupancy;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Variant of ReorderAlgorithm which simulates a foldable screen and adds a seam in the middle
|
||||
* to prevent items to be placed in the middle.
|
||||
*/
|
||||
public class MulticellReorderAlgorithm extends ReorderAlgorithm {
|
||||
|
||||
private final View mSeam;
|
||||
|
||||
public MulticellReorderAlgorithm(CellLayout cellLayout) {
|
||||
super(cellLayout);
|
||||
mSeam = new View(cellLayout.getContext());
|
||||
}
|
||||
|
||||
private CellLayout.ItemConfiguration removeSeamFromSolution(
|
||||
CellLayout.ItemConfiguration solution) {
|
||||
solution.map.forEach((view, cell) -> cell.cellX =
|
||||
cell.cellX > mCellLayout.getCountX() / 2 ? cell.cellX - 1 : cell.cellX);
|
||||
solution.cellX =
|
||||
solution.cellX > mCellLayout.getCountX() / 2 ? solution.cellX - 1 : solution.cellX;
|
||||
return solution;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellLayout.ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY,
|
||||
int minSpanX, int minSpanY,
|
||||
int spanX, int spanY) {
|
||||
return removeSeamFromSolution(simulateSeam(
|
||||
() -> super.closestEmptySpaceReorder(pixelX, pixelY, minSpanX, minSpanY, spanX,
|
||||
spanY)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellLayout.ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX,
|
||||
int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
|
||||
CellLayout.ItemConfiguration solution) {
|
||||
return removeSeamFromSolution(simulateSeam(
|
||||
() -> super.findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
|
||||
direction, dragView, decX, solution)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellLayout.ItemConfiguration dropInPlaceSolution(int pixelX, int pixelY, int spanX,
|
||||
int spanY,
|
||||
View dragView) {
|
||||
return removeSeamFromSolution(simulateSeam(
|
||||
() -> super.dropInPlaceSolution(pixelX, pixelY, spanX, spanY, dragView)));
|
||||
}
|
||||
|
||||
void addSeam() {
|
||||
MultipageCellLayout mcl = (MultipageCellLayout) mCellLayout;
|
||||
mcl.setSeamWasAdded(true);
|
||||
CellLayoutLayoutParams lp = new CellLayoutLayoutParams(mcl.getCountX() / 2, 0, 1,
|
||||
mcl.getCountY());
|
||||
lp.canReorder = false;
|
||||
mcl.setCountX(mcl.getCountX() + 1);
|
||||
mcl.getShortcutsAndWidgets().addViewInLayout(mSeam, lp);
|
||||
mcl.setOccupied(createGridOccupancyWithSeam(mcl.getOccupied()));
|
||||
mcl.mTmpOccupied = new GridOccupancy(mcl.getCountX(), mcl.getCountY());
|
||||
}
|
||||
|
||||
void removeSeam() {
|
||||
MultipageCellLayout mcl = (MultipageCellLayout) mCellLayout;
|
||||
mcl.setCountX(mcl.getCountX() - 1);
|
||||
mcl.getShortcutsAndWidgets().removeViewInLayout(mSeam);
|
||||
mcl.mTmpOccupied = new GridOccupancy(mcl.getCountX(), mcl.getCountY());
|
||||
mcl.setSeamWasAdded(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* The function supplied here will execute while the CellLayout has a simulated seam added.
|
||||
* @param f function to run under simulation
|
||||
* @param <T> return value of the supplied function
|
||||
* @return Value of supplied function
|
||||
*/
|
||||
public <T> T simulateSeam(Supplier<T> f) {
|
||||
MultipageCellLayout mcl = (MultipageCellLayout) mCellLayout;
|
||||
if (mcl.isSeamWasAdded()) {
|
||||
return f.get();
|
||||
}
|
||||
GridOccupancy auxGrid = mcl.getOccupied();
|
||||
addSeam();
|
||||
T res = f.get();
|
||||
removeSeam();
|
||||
mcl.setOccupied(auxGrid);
|
||||
return res;
|
||||
}
|
||||
|
||||
GridOccupancy createGridOccupancyWithSeam(GridOccupancy gridOccupancy) {
|
||||
GridOccupancy grid = new GridOccupancy(mCellLayout.getCountX(), mCellLayout.getCountY());
|
||||
for (int x = 0; x < mCellLayout.getCountX(); x++) {
|
||||
for (int y = 0; y < mCellLayout.getCountY(); y++) {
|
||||
int offset = x >= mCellLayout.getCountX() / 2 ? 1 : 0;
|
||||
if (x == mCellLayout.getCountX() / 2) {
|
||||
grid.cells[x][y] = true;
|
||||
} else {
|
||||
grid.cells[x][y] = gridOccupancy.cells[x - offset][y];
|
||||
}
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.launcher3.celllayout;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.android.launcher3.CellLayout;
|
||||
|
||||
/**
|
||||
* Contains the logic of a reorder.
|
||||
*
|
||||
* The content of this class was extracted from {@link CellLayout} and should mimic the exact
|
||||
* same behaviour.
|
||||
*/
|
||||
public class ReorderAlgorithm {
|
||||
|
||||
CellLayout mCellLayout;
|
||||
|
||||
public ReorderAlgorithm(CellLayout cellLayout) {
|
||||
mCellLayout = cellLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method differs from closestEmptySpaceReorder and dropInPlaceSolution because this method
|
||||
* will move items around and will change the shape of the item if possible to try to find a
|
||||
* solution.
|
||||
*
|
||||
* When changing the size of the widget this method will try first subtracting -1 in the x
|
||||
* dimension and then subtracting -1 in the y dimension until finding a possible solution or
|
||||
* until it no longer can reduce the span.
|
||||
*
|
||||
* @param pixelX X coordinate in pixels in the screen
|
||||
* @param pixelY Y coordinate in pixels in the screen
|
||||
* @param minSpanX minimum possible horizontal span it will try to find a solution for.
|
||||
* @param minSpanY minimum possible vertical span it will try to find a solution for.
|
||||
* @param spanX horizontal cell span
|
||||
* @param spanY vertical cell span
|
||||
* @param direction direction in which it will try to push the items intersecting the desired
|
||||
* view
|
||||
* @param dragView view being dragged in reorder
|
||||
* @param decX whether it will decrease the horizontal or vertical span if it can't find a
|
||||
* solution for the current span.
|
||||
* @param solution variable to store the solution
|
||||
* @return the same solution variable
|
||||
*/
|
||||
public CellLayout.ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX,
|
||||
int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
|
||||
CellLayout.ItemConfiguration solution) {
|
||||
// Copy the current state into the solution. This solution will be manipulated as necessary.
|
||||
mCellLayout.copyCurrentStateToSolution(solution, false);
|
||||
// Copy the current occupied array into the temporary occupied array. This array will be
|
||||
// manipulated as necessary to find a solution.
|
||||
mCellLayout.getOccupied().copyTo(mCellLayout.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 = mCellLayout.findNearestAreaIgnoreOccupied(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 = mCellLayout.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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a "reorder" if there is empty space without rearranging anything.
|
||||
*
|
||||
* @param pixelX X coordinate in pixels in the screen
|
||||
* @param pixelY Y coordinate in pixels in the screen
|
||||
* @param spanX horizontal cell span
|
||||
* @param spanY vertical cell span
|
||||
* @param dragView view being dragged in reorder
|
||||
* @return the configuration that represents the found reorder
|
||||
*/
|
||||
public CellLayout.ItemConfiguration dropInPlaceSolution(int pixelX, int pixelY, int spanX,
|
||||
int spanY, View dragView) {
|
||||
int[] result = new int[2];
|
||||
if (mCellLayout.isNearestDropLocationOccupied(pixelX, pixelY, spanX, spanY, dragView,
|
||||
result)) {
|
||||
result[0] = result[1] = -1;
|
||||
}
|
||||
CellLayout.ItemConfiguration solution = new CellLayout.ItemConfiguration();
|
||||
mCellLayout.copyCurrentStateToSolution(solution, false);
|
||||
solution.isSolution = result[0] != -1;
|
||||
if (!solution.isSolution) {
|
||||
return solution;
|
||||
}
|
||||
solution.cellX = result[0];
|
||||
solution.cellY = result[1];
|
||||
solution.spanX = spanX;
|
||||
solution.spanY = spanY;
|
||||
return solution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a "reorder" where we simply drop the item in the closest empty space, without moving
|
||||
* any other item in the way.
|
||||
*
|
||||
* @param pixelX X coordinate in pixels in the screen
|
||||
* @param pixelY Y coordinate in pixels in the screen
|
||||
* @param spanX horizontal cell span
|
||||
* @param spanY vertical cell span
|
||||
* @return the configuration that represents the found reorder
|
||||
*/
|
||||
public CellLayout.ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY,
|
||||
int minSpanX, int minSpanY, int spanX, int spanY) {
|
||||
CellLayout.ItemConfiguration solution = new CellLayout.ItemConfiguration();
|
||||
int[] result = new int[2];
|
||||
int[] resultSpan = new int[2];
|
||||
mCellLayout.findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, result,
|
||||
resultSpan);
|
||||
if (result[0] >= 0 && result[1] >= 0) {
|
||||
mCellLayout.copyCurrentStateToSolution(solution, false);
|
||||
solution.cellX = result[0];
|
||||
solution.cellY = result[1];
|
||||
solution.spanX = resultSpan[0];
|
||||
solution.spanY = resultSpan[1];
|
||||
solution.isSolution = true;
|
||||
} else {
|
||||
solution.isSolution = false;
|
||||
}
|
||||
return solution;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user drags an Item in the workspace sometimes we need to move the items already in
|
||||
* the workspace to make space for the new item, this function return a solution for that
|
||||
* reorder.
|
||||
*
|
||||
* @param pixelX X coordinate in the screen of the dragView in pixels
|
||||
* @param pixelY Y coordinate in the screen of the dragView in pixels
|
||||
* @param minSpanX minimum horizontal span the item can be shrunk to
|
||||
* @param minSpanY minimum vertical span the item can be shrunk to
|
||||
* @param spanX occupied horizontal span
|
||||
* @param spanY occupied vertical span
|
||||
* @param dragView the view of the item being draged
|
||||
* @return returns a solution for the given parameters, the solution contains all the icons and
|
||||
* the locations they should be in the given solution.
|
||||
*/
|
||||
public CellLayout.ItemConfiguration calculateReorder(int pixelX, int pixelY, int minSpanX,
|
||||
int minSpanY, int spanX, int spanY, View dragView) {
|
||||
mCellLayout.getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView,
|
||||
mCellLayout.mDirectionVector);
|
||||
|
||||
CellLayout.ItemConfiguration dropInPlaceSolution = dropInPlaceSolution(pixelX, pixelY,
|
||||
spanX, spanY,
|
||||
dragView);
|
||||
|
||||
// Find a solution involving pushing / displacing any items in the way
|
||||
CellLayout.ItemConfiguration swapSolution = findReorderSolution(pixelX, pixelY, minSpanX,
|
||||
minSpanY, spanX, spanY, mCellLayout.mDirectionVector, dragView, true,
|
||||
new CellLayout.ItemConfiguration());
|
||||
|
||||
// We attempt the approach which doesn't shuffle views at all
|
||||
CellLayout.ItemConfiguration closestSpaceSolution = closestEmptySpaceReorder(
|
||||
pixelX, pixelY, minSpanX, minSpanY, spanX, spanY);
|
||||
|
||||
// If the reorder solution requires resizing (shrinking) the item being dropped, we instead
|
||||
// favor a solution in which the item is not resized, but
|
||||
if (swapSolution.isSolution && swapSolution.area() >= closestSpaceSolution.area()) {
|
||||
return swapSolution;
|
||||
} else if (closestSpaceSolution.isSolution) {
|
||||
return closestSpaceSolution;
|
||||
} else if (dropInPlaceSolution.isSolution) {
|
||||
return dropInPlaceSolution;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user