1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3;
18 
19 import static com.android.launcher3.dragndrop.DraggableView.DRAGGABLE_ICON;
20 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
21 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_PREVIEW_OFFSET;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.TimeInterpolator;
26 import android.animation.ValueAnimator;
27 import android.animation.ValueAnimator.AnimatorUpdateListener;
28 import android.annotation.SuppressLint;
29 import android.content.Context;
30 import android.content.res.Resources;
31 import android.content.res.TypedArray;
32 import android.graphics.Canvas;
33 import android.graphics.Color;
34 import android.graphics.Paint;
35 import android.graphics.Point;
36 import android.graphics.Rect;
37 import android.graphics.RectF;
38 import android.graphics.drawable.Drawable;
39 import android.os.Parcelable;
40 import android.util.ArrayMap;
41 import android.util.AttributeSet;
42 import android.util.FloatProperty;
43 import android.util.Log;
44 import android.util.SparseArray;
45 import android.view.MotionEvent;
46 import android.view.View;
47 import android.view.ViewDebug;
48 import android.view.ViewGroup;
49 import android.view.accessibility.AccessibilityEvent;
50 
51 import androidx.annotation.IntDef;
52 import androidx.annotation.Nullable;
53 import androidx.annotation.Px;
54 import androidx.core.graphics.ColorUtils;
55 import androidx.core.view.ViewCompat;
56 
57 import com.android.app.animation.Interpolators;
58 import com.android.launcher3.LauncherSettings.Favorites;
59 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
60 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
61 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
62 import com.android.launcher3.celllayout.DelegatedCellDrawing;
63 import com.android.launcher3.celllayout.ItemConfiguration;
64 import com.android.launcher3.celllayout.ReorderAlgorithm;
65 import com.android.launcher3.celllayout.ReorderParameters;
66 import com.android.launcher3.celllayout.ReorderPreviewAnimation;
67 import com.android.launcher3.config.FeatureFlags;
68 import com.android.launcher3.dragndrop.DraggableView;
69 import com.android.launcher3.folder.PreviewBackground;
70 import com.android.launcher3.model.data.ItemInfo;
71 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
72 import com.android.launcher3.util.CellAndSpan;
73 import com.android.launcher3.util.GridOccupancy;
74 import com.android.launcher3.util.MultiTranslateDelegate;
75 import com.android.launcher3.util.ParcelableSparseArray;
76 import com.android.launcher3.util.Themes;
77 import com.android.launcher3.util.Thunk;
78 import com.android.launcher3.views.ActivityContext;
79 import com.android.launcher3.widget.LauncherAppWidgetHostView;
80 
81 import java.lang.annotation.Retention;
82 import java.lang.annotation.RetentionPolicy;
83 import java.util.ArrayList;
84 import java.util.Arrays;
85 import java.util.Stack;
86 
87 public class CellLayout extends ViewGroup {
88     private static final String TAG = "CellLayout";
89     private static final boolean LOGD = false;
90 
91     /** The color of the "leave-behind" shape when a folder is opened from Hotseat. */
92     private static final int FOLDER_LEAVE_BEHIND_COLOR = Color.argb(160, 245, 245, 245);
93 
94     protected final ActivityContext mActivity;
95     @ViewDebug.ExportedProperty(category = "launcher")
96     @Thunk int mCellWidth;
97     @ViewDebug.ExportedProperty(category = "launcher")
98     @Thunk int mCellHeight;
99     private int mFixedCellWidth;
100     private int mFixedCellHeight;
101     @ViewDebug.ExportedProperty(category = "launcher")
102     protected Point mBorderSpace;
103 
104     @ViewDebug.ExportedProperty(category = "launcher")
105     protected int mCountX;
106     @ViewDebug.ExportedProperty(category = "launcher")
107     protected int mCountY;
108 
109     private boolean mDropPending = false;
110 
111     // These are temporary variables to prevent having to allocate a new object just to
112     // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
113     @Thunk final int[] mTmpPoint = new int[2];
114     @Thunk final int[] mTempLocation = new int[2];
115 
116     @Thunk final Rect mTempOnDrawCellToRect = new Rect();
117 
118     protected GridOccupancy mOccupied;
119     public GridOccupancy mTmpOccupied;
120 
121     private OnTouchListener mInterceptTouchListener;
122 
123     private final ArrayList<DelegatedCellDrawing> mDelegatedCellDrawings = new ArrayList<>();
124     final PreviewBackground mFolderLeaveBehind = new PreviewBackground(getContext());
125 
126     private static final int[] BACKGROUND_STATE_ACTIVE = new int[] { android.R.attr.state_active };
127     private static final int[] BACKGROUND_STATE_DEFAULT = EMPTY_STATE_SET;
128     protected final Drawable mBackground;
129 
130     // These values allow a fixed measurement to be set on the CellLayout.
131     private int mFixedWidth = -1;
132     private int mFixedHeight = -1;
133 
134     // If we're actively dragging something over this screen, mIsDragOverlapping is true
135     private boolean mIsDragOverlapping = false;
136 
137     // These arrays are used to implement the drag visualization on x-large screens.
138     // They are used as circular arrays, indexed by mDragOutlineCurrent.
139     @Thunk final CellLayoutLayoutParams[] mDragOutlines = new CellLayoutLayoutParams[4];
140     @Thunk final float[] mDragOutlineAlphas = new float[mDragOutlines.length];
141     private final InterruptibleInOutAnimator[] mDragOutlineAnims =
142             new InterruptibleInOutAnimator[mDragOutlines.length];
143 
144     // Used as an index into the above 3 arrays; indicates which is the most current value.
145     private int mDragOutlineCurrent = 0;
146     private final Paint mDragOutlinePaint = new Paint();
147 
148     @Thunk final ArrayMap<CellLayoutLayoutParams, Animator> mReorderAnimators = new ArrayMap<>();
149     @Thunk final ArrayMap<Reorderable, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>();
150 
151     private boolean mItemPlacementDirty = false;
152 
153     // Used to visualize the grid and drop locations
154     private boolean mVisualizeCells = false;
155     private boolean mVisualizeDropLocation = true;
156     private RectF mVisualizeGridRect = new RectF();
157     private Paint mVisualizeGridPaint = new Paint();
158     private int mGridVisualizationRoundingRadius;
159     private float mGridAlpha = 0f;
160     private int mGridColor = 0;
161     protected float mSpringLoadedProgress = 0f;
162     private float mScrollProgress = 0f;
163 
164     // When a drag operation is in progress, holds the nearest cell to the touch point
165     private final int[] mDragCell = new int[2];
166     private final int[] mDragCellSpan = new int[2];
167 
168     private boolean mDragging = false;
169 
170     private final TimeInterpolator mEaseOutInterpolator;
171     protected final ShortcutAndWidgetContainer mShortcutsAndWidgets;
172     @Px
173     protected int mSpaceBetweenCellLayoutsPx = 0;
174 
175     @Retention(RetentionPolicy.SOURCE)
176     @IntDef({WORKSPACE, HOTSEAT, FOLDER})
177     public @interface ContainerType{}
178     public static final int WORKSPACE = 0;
179     public static final int HOTSEAT = 1;
180     public static final int FOLDER = 2;
181 
182     @ContainerType private final int mContainerType;
183 
184     public static final float DEFAULT_SCALE = 1f;
185 
186     public static final int MODE_SHOW_REORDER_HINT = 0;
187     public static final int MODE_DRAG_OVER = 1;
188     public static final int MODE_ON_DROP = 2;
189     public static final int MODE_ON_DROP_EXTERNAL = 3;
190     public static final int MODE_ACCEPT_DROP = 4;
191     private static final boolean DESTRUCTIVE_REORDER = false;
192     private static final boolean DEBUG_VISUALIZE_OCCUPIED = false;
193 
194     public static final float REORDER_PREVIEW_MAGNITUDE = 0.12f;
195     public static final int REORDER_ANIMATION_DURATION = 150;
196     @Thunk final float mReorderPreviewAnimationMagnitude;
197 
198     public final int[] mDirectionVector = new int[2];
199 
200     ItemConfiguration mPreviousSolution = null;
201 
202     private final Rect mTempRect = new Rect();
203 
204     private static final Paint sPaint = new Paint();
205 
206     // Related to accessible drag and drop
207     DragAndDropAccessibilityDelegate mTouchHelper;
208 
209     CellLayoutContainer mCellLayoutContainer;
210 
211     public static final FloatProperty<CellLayout> SPRING_LOADED_PROGRESS =
212             new FloatProperty<CellLayout>("spring_loaded_progress") {
213                 @Override
214                 public Float get(CellLayout cl) {
215                     return cl.getSpringLoadedProgress();
216                 }
217 
218                 @Override
219                 public void setValue(CellLayout cl, float progress) {
220                     cl.setSpringLoadedProgress(progress);
221                 }
222             };
223 
CellLayout(Context context, CellLayoutContainer container)224     public CellLayout(Context context, CellLayoutContainer container) {
225         this(context, (AttributeSet) null);
226         this.mCellLayoutContainer = container;
227     }
228 
CellLayout(Context context, AttributeSet attrs)229     public CellLayout(Context context, AttributeSet attrs) {
230         this(context, attrs, 0);
231     }
232 
CellLayout(Context context, AttributeSet attrs, int defStyle)233     public CellLayout(Context context, AttributeSet attrs, int defStyle) {
234         super(context, attrs, defStyle);
235         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0);
236         mContainerType = a.getInteger(R.styleable.CellLayout_containerType, WORKSPACE);
237         a.recycle();
238 
239         // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show
240         // the user where a dragged item will land when dropped.
241         setWillNotDraw(false);
242         setClipToPadding(false);
243         setClipChildren(false);
244         mActivity = ActivityContext.lookupContext(context);
245         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
246 
247         resetCellSizeInternal(deviceProfile);
248 
249         mCountX = deviceProfile.inv.numColumns;
250         mCountY = deviceProfile.inv.numRows;
251         mOccupied =  new GridOccupancy(mCountX, mCountY);
252         mTmpOccupied = new GridOccupancy(mCountX, mCountY);
253 
254         mFolderLeaveBehind.mDelegateCellX = -1;
255         mFolderLeaveBehind.mDelegateCellY = -1;
256 
257         setAlwaysDrawnWithCacheEnabled(false);
258 
259         Resources res = getResources();
260 
261         mBackground = getContext().getDrawable(R.drawable.bg_celllayout);
262         mBackground.setCallback(this);
263         mBackground.setAlpha(0);
264 
265         mGridColor = Themes.getAttrColor(getContext(), R.attr.workspaceAccentColor);
266         mGridVisualizationRoundingRadius =
267                 res.getDimensionPixelSize(R.dimen.grid_visualization_rounding_radius);
268         mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * deviceProfile.iconSizePx);
269 
270         // Initialize the data structures used for the drag visualization.
271         mEaseOutInterpolator = Interpolators.DECELERATE_QUINT; // Quint ease out
272         mDragCell[0] = mDragCell[1] = -1;
273         mDragCellSpan[0] = mDragCellSpan[1] = -1;
274         for (int i = 0; i < mDragOutlines.length; i++) {
275             mDragOutlines[i] = new CellLayoutLayoutParams(0, 0, 0, 0);
276         }
277         mDragOutlinePaint.setColor(Themes.getAttrColor(context, R.attr.workspaceTextColor));
278 
279         // When dragging things around the home screens, we show a green outline of
280         // where the item will land. The outlines gradually fade out, leaving a trail
281         // behind the drag path.
282         // Set up all the animations that are used to implement this fading.
283         final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime);
284         final float fromAlphaValue = 0;
285         final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha);
286 
287         Arrays.fill(mDragOutlineAlphas, fromAlphaValue);
288 
289         for (int i = 0; i < mDragOutlineAnims.length; i++) {
290             final InterruptibleInOutAnimator anim =
291                     new InterruptibleInOutAnimator(duration, fromAlphaValue, toAlphaValue);
292             anim.getAnimator().setInterpolator(mEaseOutInterpolator);
293             final int thisIndex = i;
294             anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() {
295                 public void onAnimationUpdate(ValueAnimator animation) {
296                     // If an animation is started and then stopped very quickly, we can still
297                     // get spurious updates we've cleared the tag. Guard against this.
298                     mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue();
299                     CellLayout.this.invalidate();
300                 }
301             });
302             // The animation holds a reference to the drag outline bitmap as long is it's
303             // running. This way the bitmap can be GCed when the animations are complete.
304             mDragOutlineAnims[i] = anim;
305         }
306 
307         mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context, mContainerType);
308         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
309                 mBorderSpace);
310         addView(mShortcutsAndWidgets);
311     }
312 
getCellLayoutContainer()313     public CellLayoutContainer getCellLayoutContainer() {
314         return mCellLayoutContainer;
315     }
316 
setCellLayoutContainer(CellLayoutContainer cellLayoutContainer)317     public void setCellLayoutContainer(CellLayoutContainer cellLayoutContainer) {
318         mCellLayoutContainer = cellLayoutContainer;
319     }
320 
321     /**
322      * Sets or clears a delegate used for accessible drag and drop
323      */
setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate)324     public void setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate) {
325         ViewCompat.setAccessibilityDelegate(this, delegate);
326 
327         mTouchHelper = delegate;
328         int accessibilityFlag = mTouchHelper != null
329                 ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO;
330         setImportantForAccessibility(accessibilityFlag);
331         getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
332         // ExploreByTouchHelper sets focusability. Clear it when the delegate is cleared.
333         setFocusable(delegate != null);
334         // Invalidate the accessibility hierarchy
335         if (getParent() != null) {
336             getParent().notifySubtreeAccessibilityStateChanged(
337                     this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
338         }
339     }
340 
341     /**
342      * Returns the currently set accessibility delegate
343      */
getDragAndDropAccessibilityDelegate()344     public DragAndDropAccessibilityDelegate getDragAndDropAccessibilityDelegate() {
345         return mTouchHelper;
346     }
347 
348     @Override
dispatchHoverEvent(MotionEvent event)349     public boolean dispatchHoverEvent(MotionEvent event) {
350         // Always attempt to dispatch hover events to accessibility first.
351         if (mTouchHelper != null && mTouchHelper.dispatchHoverEvent(event)) {
352             return true;
353         }
354         return super.dispatchHoverEvent(event);
355     }
356 
357     @Override
onInterceptTouchEvent(MotionEvent ev)358     public boolean onInterceptTouchEvent(MotionEvent ev) {
359         return mTouchHelper != null
360                 || (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev));
361     }
362 
enableHardwareLayer(boolean hasLayer)363     public void enableHardwareLayer(boolean hasLayer) {
364         mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint);
365     }
366 
isHardwareLayerEnabled()367     public boolean isHardwareLayerEnabled() {
368         return mShortcutsAndWidgets.getLayerType() == LAYER_TYPE_HARDWARE;
369     }
370 
371     /**
372      * Change sizes of cells
373      *
374      * @param width  the new width of the cells
375      * @param height the new height of the cells
376      */
setCellDimensions(int width, int height)377     public void setCellDimensions(int width, int height) {
378         mFixedCellWidth = mCellWidth = width;
379         mFixedCellHeight = mCellHeight = height;
380         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
381                 mBorderSpace);
382     }
383 
resetCellSizeInternal(DeviceProfile deviceProfile)384     private void resetCellSizeInternal(DeviceProfile deviceProfile) {
385         switch (mContainerType) {
386             case FOLDER:
387                 mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx);
388                 break;
389             case HOTSEAT:
390                 mBorderSpace = new Point(deviceProfile.hotseatBorderSpace,
391                         deviceProfile.hotseatBorderSpace);
392                 break;
393             case WORKSPACE:
394             default:
395                 mBorderSpace = new Point(deviceProfile.cellLayoutBorderSpacePx);
396                 break;
397         }
398 
399         mCellWidth = mCellHeight = -1;
400         mFixedCellWidth = mFixedCellHeight = -1;
401     }
402 
403     /**
404      * Reset the cell sizes and border space
405      */
resetCellSize(DeviceProfile deviceProfile)406     public void resetCellSize(DeviceProfile deviceProfile) {
407         resetCellSizeInternal(deviceProfile);
408         requestLayout();
409     }
410 
setGridSize(int x, int y)411     public void setGridSize(int x, int y) {
412         mCountX = x;
413         mCountY = y;
414         mOccupied = new GridOccupancy(mCountX, mCountY);
415         mTmpOccupied = new GridOccupancy(mCountX, mCountY);
416         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
417                 mBorderSpace);
418         requestLayout();
419     }
420 
421     // Set whether or not to invert the layout horizontally if the layout is in RTL mode.
setInvertIfRtl(boolean invert)422     public void setInvertIfRtl(boolean invert) {
423         mShortcutsAndWidgets.setInvertIfRtl(invert);
424     }
425 
setDropPending(boolean pending)426     public void setDropPending(boolean pending) {
427         mDropPending = pending;
428     }
429 
isDropPending()430     public boolean isDropPending() {
431         return mDropPending;
432     }
433 
setIsDragOverlapping(boolean isDragOverlapping)434     void setIsDragOverlapping(boolean isDragOverlapping) {
435         if (mIsDragOverlapping != isDragOverlapping) {
436             mIsDragOverlapping = isDragOverlapping;
437             mBackground.setState(mIsDragOverlapping
438                     ? BACKGROUND_STATE_ACTIVE : BACKGROUND_STATE_DEFAULT);
439             invalidate();
440         }
441     }
442 
443     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)444     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
445         ParcelableSparseArray jail = getJailedArray(container);
446         super.dispatchSaveInstanceState(jail);
447         container.put(R.id.cell_layout_jail_id, jail);
448     }
449 
450     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)451     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
452         super.dispatchRestoreInstanceState(getJailedArray(container));
453     }
454 
455     /**
456      * Wrap the SparseArray in another Parcelable so that the item ids do not conflict with our
457      * our internal resource ids
458      */
getJailedArray(SparseArray<Parcelable> container)459     private ParcelableSparseArray getJailedArray(SparseArray<Parcelable> container) {
460         final Parcelable parcelable = container.get(R.id.cell_layout_jail_id);
461         return parcelable instanceof ParcelableSparseArray ?
462                 (ParcelableSparseArray) parcelable : new ParcelableSparseArray();
463     }
464 
getIsDragOverlapping()465     public boolean getIsDragOverlapping() {
466         return mIsDragOverlapping;
467     }
468 
469     @Override
onDraw(Canvas canvas)470     protected void onDraw(Canvas canvas) {
471         // When we're large, we are either drawn in a "hover" state (ie when dragging an item to
472         // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f)
473         // When we're small, we are either drawn normally or in the "accepts drops" state (during
474         // a drag). However, we also drag the mini hover background *over* one of those two
475         // backgrounds
476         if (mBackground.getAlpha() > 0) {
477             mBackground.draw(canvas);
478         }
479 
480         if (DEBUG_VISUALIZE_OCCUPIED) {
481             Rect cellBounds = new Rect();
482             // Will contain the bounds of the cell including spacing between cells.
483             Rect cellBoundsWithSpacing = new Rect();
484             int[] targetCell = new int[2];
485             int[] cellCenter = new int[2];
486             Paint debugPaint = new Paint();
487             debugPaint.setStrokeWidth(Utilities.dpToPx(1));
488             for (int x = 0; x < mCountX; x++) {
489                 for (int y = 0; y < mCountY; y++) {
490                     if (!mOccupied.cells[x][y]) {
491                         continue;
492                     }
493                     targetCell[0] = x;
494                     targetCell[1] = y;
495 
496                     boolean canCreateFolder = canCreateFolder(getChildAt(x, y));
497                     cellToRect(x, y, 1, 1, cellBounds);
498                     cellBoundsWithSpacing.set(cellBounds);
499                     cellBoundsWithSpacing.inset(-mBorderSpace.x / 2, -mBorderSpace.y / 2);
500                     getWorkspaceCellVisualCenter(x, y, cellCenter);
501 
502                     canvas.save();
503                     canvas.clipRect(cellBoundsWithSpacing);
504 
505                     // Draw reorder drag target.
506                     debugPaint.setColor(Color.RED);
507                     canvas.drawCircle(cellCenter[0], cellCenter[1],
508                             getReorderRadius(targetCell, 1, 1), debugPaint);
509 
510                     // Draw folder creation drag target.
511                     if (canCreateFolder) {
512                         debugPaint.setColor(Color.GREEN);
513                         canvas.drawCircle(cellCenter[0], cellCenter[1],
514                                 getFolderCreationRadius(targetCell), debugPaint);
515                     }
516 
517                     canvas.restore();
518                 }
519             }
520         }
521 
522         for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
523             DelegatedCellDrawing cellDrawing = mDelegatedCellDrawings.get(i);
524             cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation);
525             canvas.save();
526             canvas.translate(mTempLocation[0], mTempLocation[1]);
527             cellDrawing.drawUnderItem(canvas);
528             canvas.restore();
529         }
530 
531         if (mFolderLeaveBehind.mDelegateCellX >= 0 && mFolderLeaveBehind.mDelegateCellY >= 0) {
532             cellToPoint(mFolderLeaveBehind.mDelegateCellX,
533                     mFolderLeaveBehind.mDelegateCellY, mTempLocation);
534             canvas.save();
535             canvas.translate(mTempLocation[0], mTempLocation[1]);
536             mFolderLeaveBehind.drawLeaveBehind(canvas, FOLDER_LEAVE_BEHIND_COLOR);
537             canvas.restore();
538         }
539 
540         if (mVisualizeCells || mVisualizeDropLocation) {
541             visualizeGrid(canvas);
542         }
543     }
544 
545     /**
546      * Returns whether dropping an icon on the given View can create (or add to) a folder.
547      */
canCreateFolder(View child)548     private boolean canCreateFolder(View child) {
549         return child instanceof DraggableView
550                 && ((DraggableView) child).getViewType() == DRAGGABLE_ICON;
551     }
552 
553     /**
554      * Indicates the progress of the Workspace entering the SpringLoaded state; allows the
555      * CellLayout to update various visuals for this state.
556      *
557      * @param progress
558      */
setSpringLoadedProgress(float progress)559     public void setSpringLoadedProgress(float progress) {
560         if (Float.compare(progress, mSpringLoadedProgress) != 0) {
561             mSpringLoadedProgress = progress;
562             updateBgAlpha();
563             setGridAlpha(progress);
564         }
565     }
566 
567     /**
568      * See setSpringLoadedProgress
569      * @return progress
570      */
getSpringLoadedProgress()571     public float getSpringLoadedProgress() {
572         return mSpringLoadedProgress;
573     }
574 
updateBgAlpha()575     protected void updateBgAlpha() {
576         mBackground.setAlpha((int) (mSpringLoadedProgress * 255));
577     }
578 
579     /**
580      * Set the progress of this page's scroll
581      *
582      * @param progress 0 if the screen is centered, +/-1 if it is to the right / left respectively
583      */
setScrollProgress(float progress)584     public void setScrollProgress(float progress) {
585         if (Float.compare(Math.abs(progress), mScrollProgress) != 0) {
586             mScrollProgress = Math.abs(progress);
587             updateBgAlpha();
588         }
589     }
590 
setGridAlpha(float gridAlpha)591     private void setGridAlpha(float gridAlpha) {
592         if (Float.compare(gridAlpha, mGridAlpha) != 0) {
593             mGridAlpha = gridAlpha;
594             invalidate();
595         }
596     }
597 
visualizeGrid(Canvas canvas)598     protected void visualizeGrid(Canvas canvas) {
599         DeviceProfile dp = mActivity.getDeviceProfile();
600         int paddingX = Math.min((mCellWidth - dp.iconSizePx) / 2, dp.gridVisualizationPaddingX);
601         int paddingY = Math.min((mCellHeight - dp.iconSizePx) / 2, dp.gridVisualizationPaddingY);
602 
603         mVisualizeGridPaint.setStrokeWidth(8);
604 
605         // This is used for debugging purposes only
606         if (mVisualizeCells) {
607             int paintAlpha = (int) (120 * mGridAlpha);
608             mVisualizeGridPaint.setColor(ColorUtils.setAlphaComponent(mGridColor, paintAlpha));
609             for (int i = 0; i < mCountX; i++) {
610                 for (int j = 0; j < mCountY; j++) {
611                     cellToRect(i, j, 1, 1, mTempOnDrawCellToRect);
612                     mVisualizeGridRect.set(mTempOnDrawCellToRect);
613                     mVisualizeGridRect.inset(paddingX, paddingY);
614                     mVisualizeGridPaint.setStyle(Paint.Style.FILL);
615                     canvas.drawRoundRect(mVisualizeGridRect, mGridVisualizationRoundingRadius,
616                             mGridVisualizationRoundingRadius, mVisualizeGridPaint);
617                 }
618             }
619         }
620 
621         if (mVisualizeDropLocation) {
622             for (int i = 0; i < mDragOutlines.length; i++) {
623                 final float alpha = mDragOutlineAlphas[i];
624                 if (alpha <= 0) continue;
625                 CellLayoutLayoutParams params = mDragOutlines[i];
626                 cellToRect(params.getCellX(), params.getCellY(), params.cellHSpan, params.cellVSpan,
627                         mTempOnDrawCellToRect);
628                 mVisualizeGridRect.set(mTempOnDrawCellToRect);
629                 mVisualizeGridRect.inset(paddingX, paddingY);
630 
631                 mVisualizeGridPaint.setAlpha(255);
632                 mVisualizeGridPaint.setStyle(Paint.Style.STROKE);
633                 mVisualizeGridPaint.setColor(Color.argb((int) (alpha),
634                         Color.red(mGridColor), Color.green(mGridColor), Color.blue(mGridColor)));
635 
636                 canvas.save();
637                 canvas.translate(getMarginForGivenCellParams(params), 0);
638                 canvas.drawRoundRect(mVisualizeGridRect, mGridVisualizationRoundingRadius,
639                         mGridVisualizationRoundingRadius, mVisualizeGridPaint);
640                 canvas.restore();
641             }
642         }
643     }
644 
getMarginForGivenCellParams(CellLayoutLayoutParams params)645     protected float getMarginForGivenCellParams(CellLayoutLayoutParams params) {
646         return 0;
647     }
648 
649     @Override
dispatchDraw(Canvas canvas)650     protected void dispatchDraw(Canvas canvas) {
651         super.dispatchDraw(canvas);
652 
653         for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
654             DelegatedCellDrawing bg = mDelegatedCellDrawings.get(i);
655             cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation);
656             canvas.save();
657             canvas.translate(mTempLocation[0], mTempLocation[1]);
658             bg.drawOverItem(canvas);
659             canvas.restore();
660         }
661     }
662 
663     /**
664      * Add Delegated cell drawing
665      */
addDelegatedCellDrawing(DelegatedCellDrawing bg)666     public void addDelegatedCellDrawing(DelegatedCellDrawing bg) {
667         mDelegatedCellDrawings.add(bg);
668     }
669 
670     /**
671      * Remove item from DelegatedCellDrawings
672      */
removeDelegatedCellDrawing(DelegatedCellDrawing bg)673     public void removeDelegatedCellDrawing(DelegatedCellDrawing bg) {
674         mDelegatedCellDrawings.remove(bg);
675     }
676 
setFolderLeaveBehindCell(int x, int y)677     public void setFolderLeaveBehindCell(int x, int y) {
678         View child = getChildAt(x, y);
679         mFolderLeaveBehind.setup(getContext(), mActivity, null,
680                 child.getMeasuredWidth(), child.getPaddingTop());
681 
682         mFolderLeaveBehind.mDelegateCellX = x;
683         mFolderLeaveBehind.mDelegateCellY = y;
684         invalidate();
685     }
686 
clearFolderLeaveBehind()687     public void clearFolderLeaveBehind() {
688         mFolderLeaveBehind.mDelegateCellX = -1;
689         mFolderLeaveBehind.mDelegateCellY = -1;
690         invalidate();
691     }
692 
693     @Override
shouldDelayChildPressedState()694     public boolean shouldDelayChildPressedState() {
695         return false;
696     }
697 
restoreInstanceState(SparseArray<Parcelable> states)698     public void restoreInstanceState(SparseArray<Parcelable> states) {
699         try {
700             dispatchRestoreInstanceState(states);
701         } catch (IllegalArgumentException ex) {
702             if (FeatureFlags.IS_STUDIO_BUILD) {
703                 throw ex;
704             }
705             // Mismatched viewId / viewType preventing restore. Skip restore on production builds.
706             Log.e(TAG, "Ignoring an error while restoring a view instance state", ex);
707         }
708     }
709 
710     @Override
cancelLongPress()711     public void cancelLongPress() {
712         super.cancelLongPress();
713 
714         // Cancel long press for all children
715         final int count = getChildCount();
716         for (int i = 0; i < count; i++) {
717             final View child = getChildAt(i);
718             child.cancelLongPress();
719         }
720     }
721 
setOnInterceptTouchListener(View.OnTouchListener listener)722     public void setOnInterceptTouchListener(View.OnTouchListener listener) {
723         mInterceptTouchListener = listener;
724     }
725 
getCountX()726     public int getCountX() {
727         return mCountX;
728     }
729 
getCountY()730     public int getCountY() {
731         return mCountY;
732     }
733 
acceptsWidget()734     public boolean acceptsWidget() {
735         return mContainerType == WORKSPACE;
736     }
737 
738     /**
739      * Adds the given view to the CellLayout
740      *
741      * @param child view to add.
742      * @param index index of the CellLayout children where to add the view.
743      * @param childId id of the view.
744      * @param params represent the logic of the view on the CellLayout.
745      * @param markCells if the occupied cells should be marked or not
746      * @return if adding the view was successful
747      */
addViewToCellLayout(View child, int index, int childId, CellLayoutLayoutParams params, boolean markCells)748     public boolean addViewToCellLayout(View child, int index, int childId,
749             CellLayoutLayoutParams params, boolean markCells) {
750         final CellLayoutLayoutParams lp = params;
751 
752         // Hotseat icons - remove text
753         if (child instanceof BubbleTextView) {
754             BubbleTextView bubbleChild = (BubbleTextView) child;
755             bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
756         }
757 
758         child.setScaleX(DEFAULT_SCALE);
759         child.setScaleY(DEFAULT_SCALE);
760 
761         // Generate an id for each view, this assumes we have at most 256x256 cells
762         // per workspace screen
763         if (lp.getCellX() >= 0 && lp.getCellX() <= mCountX - 1
764                 && lp.getCellY() >= 0 && lp.getCellY() <= mCountY - 1) {
765             // If the horizontal or vertical span is set to -1, it is taken to
766             // mean that it spans the extent of the CellLayout
767             if (lp.cellHSpan < 0) lp.cellHSpan = mCountX;
768             if (lp.cellVSpan < 0) lp.cellVSpan = mCountY;
769 
770             child.setId(childId);
771             if (LOGD) {
772                 Log.d(TAG, "Adding view to ShortcutsAndWidgetsContainer: " + child);
773             }
774             mShortcutsAndWidgets.addView(child, index, lp);
775 
776             if (markCells) markCellsAsOccupiedForView(child);
777 
778             return true;
779         }
780         return false;
781     }
782 
783     @Override
removeAllViews()784     public void removeAllViews() {
785         mOccupied.clear();
786         mShortcutsAndWidgets.removeAllViews();
787     }
788 
789     @Override
removeAllViewsInLayout()790     public void removeAllViewsInLayout() {
791         if (mShortcutsAndWidgets.getChildCount() > 0) {
792             mOccupied.clear();
793             mShortcutsAndWidgets.removeAllViewsInLayout();
794         }
795     }
796 
797     @Override
removeView(View view)798     public void removeView(View view) {
799         markCellsAsUnoccupiedForView(view);
800         mShortcutsAndWidgets.removeView(view);
801     }
802 
803     @Override
removeViewAt(int index)804     public void removeViewAt(int index) {
805         markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index));
806         mShortcutsAndWidgets.removeViewAt(index);
807     }
808 
809     @Override
removeViewInLayout(View view)810     public void removeViewInLayout(View view) {
811         markCellsAsUnoccupiedForView(view);
812         mShortcutsAndWidgets.removeViewInLayout(view);
813     }
814 
815     @Override
removeViews(int start, int count)816     public void removeViews(int start, int count) {
817         for (int i = start; i < start + count; i++) {
818             markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i));
819         }
820         mShortcutsAndWidgets.removeViews(start, count);
821     }
822 
823     @Override
removeViewsInLayout(int start, int count)824     public void removeViewsInLayout(int start, int count) {
825         for (int i = start; i < start + count; i++) {
826             markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i));
827         }
828         mShortcutsAndWidgets.removeViewsInLayout(start, count);
829     }
830 
831     /**
832      * Given a point, return the cell that strictly encloses that point
833      * @param x X coordinate of the point
834      * @param y Y coordinate of the point
835      * @param result Array of 2 ints to hold the x and y coordinate of the cell
836      */
pointToCellExact(int x, int y, int[] result)837     public void pointToCellExact(int x, int y, int[] result) {
838         final int hStartPadding = getPaddingLeft();
839         final int vStartPadding = getPaddingTop();
840 
841         result[0] = (x - hStartPadding) / (mCellWidth + mBorderSpace.x);
842         result[1] = (y - vStartPadding) / (mCellHeight + mBorderSpace.y);
843 
844         final int xAxis = mCountX;
845         final int yAxis = mCountY;
846 
847         if (result[0] < 0) result[0] = 0;
848         if (result[0] >= xAxis) result[0] = xAxis - 1;
849         if (result[1] < 0) result[1] = 0;
850         if (result[1] >= yAxis) result[1] = yAxis - 1;
851     }
852 
853     /**
854      * Given a cell coordinate, return the point that represents the upper left corner of that cell
855      *
856      * @param cellX X coordinate of the cell
857      * @param cellY Y coordinate of the cell
858      *
859      * @param result Array of 2 ints to hold the x and y coordinate of the point
860      */
cellToPoint(int cellX, int cellY, int[] result)861     void cellToPoint(int cellX, int cellY, int[] result) {
862         cellToRect(cellX, cellY, 1, 1, mTempRect);
863         result[0] = mTempRect.left;
864         result[1] = mTempRect.top;
865     }
866 
867     /**
868      * Given a cell coordinate, return the point that represents the center of the cell
869      *
870      * @param cellX X coordinate of the cell
871      * @param cellY Y coordinate of the cell
872      *
873      * @param result Array of 2 ints to hold the x and y coordinate of the point
874      */
cellToCenterPoint(int cellX, int cellY, int[] result)875     void cellToCenterPoint(int cellX, int cellY, int[] result) {
876         regionToCenterPoint(cellX, cellY, 1, 1, result);
877     }
878 
879     /**
880      * Given a cell coordinate and span return the point that represents the center of the region
881      *
882      * @param cellX X coordinate of the cell
883      * @param cellY Y coordinate of the cell
884      *
885      * @param result Array of 2 ints to hold the x and y coordinate of the point
886      */
regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result)887     public void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
888         cellToRect(cellX, cellY, spanX, spanY, mTempRect);
889         result[0] = mTempRect.centerX();
890         result[1] = mTempRect.centerY();
891     }
892 
893     /**
894      * Returns the distance between the given coordinate and the visual center of the given cell.
895      */
getDistanceFromWorkspaceCellVisualCenter(float x, float y, int[] cell)896     public float getDistanceFromWorkspaceCellVisualCenter(float x, float y, int[] cell) {
897         getWorkspaceCellVisualCenter(cell[0], cell[1], mTmpPoint);
898         return (float) Math.hypot(x - mTmpPoint[0], y - mTmpPoint[1]);
899     }
900 
getWorkspaceCellVisualCenter(int cellX, int cellY, int[] outPoint)901     private void getWorkspaceCellVisualCenter(int cellX, int cellY, int[] outPoint) {
902         View child = getChildAt(cellX, cellY);
903         if (child instanceof DraggableView) {
904             DraggableView draggableChild = (DraggableView) child;
905             if (draggableChild.getViewType() == DRAGGABLE_ICON) {
906                 cellToPoint(cellX, cellY, outPoint);
907                 draggableChild.getWorkspaceVisualDragBounds(mTempRect);
908                 mTempRect.offset(outPoint[0], outPoint[1]);
909                 outPoint[0] = mTempRect.centerX();
910                 outPoint[1] = mTempRect.centerY();
911                 return;
912             }
913         }
914         cellToCenterPoint(cellX, cellY, outPoint);
915     }
916 
917     /**
918      * Returns the max distance from the center of a cell that can accept a drop to create a folder.
919      */
getFolderCreationRadius(int[] targetCell)920     public float getFolderCreationRadius(int[] targetCell) {
921         DeviceProfile grid = mActivity.getDeviceProfile();
922         float iconVisibleRadius = ICON_VISIBLE_AREA_FACTOR * grid.iconSizePx / 2;
923         // Halfway between reorder radius and icon.
924         return (getReorderRadius(targetCell, 1, 1) + iconVisibleRadius) / 2;
925     }
926 
927     /**
928      * Returns the max distance from the center of a cell that will start to reorder on drag over.
929      */
getReorderRadius(int[] targetCell, int spanX, int spanY)930     public float getReorderRadius(int[] targetCell, int spanX, int spanY) {
931         int[] centerPoint = mTmpPoint;
932         getWorkspaceCellVisualCenter(targetCell[0], targetCell[1], centerPoint);
933 
934         Rect cellBoundsWithSpacing = mTempRect;
935         cellToRect(targetCell[0], targetCell[1], spanX, spanY, cellBoundsWithSpacing);
936         cellBoundsWithSpacing.inset(-mBorderSpace.x / 2, -mBorderSpace.y / 2);
937 
938         if (canCreateFolder(getChildAt(targetCell[0], targetCell[1])) && spanX == 1 && spanY == 1) {
939             // Take only the circle in the smaller dimension, to ensure we don't start reordering
940             // too soon before accepting a folder drop.
941             int minRadius = centerPoint[0] - cellBoundsWithSpacing.left;
942             minRadius = Math.min(minRadius, centerPoint[1] - cellBoundsWithSpacing.top);
943             minRadius = Math.min(minRadius, cellBoundsWithSpacing.right - centerPoint[0]);
944             minRadius = Math.min(minRadius, cellBoundsWithSpacing.bottom - centerPoint[1]);
945             return minRadius;
946         }
947         // Take up the entire cell, including space between this cell and the adjacent ones.
948         // Multiply by span to scale radius
949         return (float) Math.hypot(spanX * cellBoundsWithSpacing.width() / 2f,
950                 spanY * cellBoundsWithSpacing.height() / 2f);
951     }
952 
getCellWidth()953     public int getCellWidth() {
954         return mCellWidth;
955     }
956 
getCellHeight()957     public int getCellHeight() {
958         return mCellHeight;
959     }
960 
setFixedSize(int width, int height)961     public void setFixedSize(int width, int height) {
962         mFixedWidth = width;
963         mFixedHeight = height;
964     }
965 
966     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)967     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
968         int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
969         int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
970         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
971         int heightSize =  MeasureSpec.getSize(heightMeasureSpec);
972         int childWidthSize = widthSize - (getPaddingLeft() + getPaddingRight());
973         int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom());
974 
975         if (mFixedCellWidth < 0 || mFixedCellHeight < 0) {
976             int cw = DeviceProfile.calculateCellWidth(childWidthSize, mBorderSpace.x,
977                     mCountX);
978             int ch = DeviceProfile.calculateCellHeight(childHeightSize, mBorderSpace.y,
979                     mCountY);
980             if (cw != mCellWidth || ch != mCellHeight) {
981                 mCellWidth = cw;
982                 mCellHeight = ch;
983                 mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
984                         mBorderSpace);
985             }
986         }
987 
988         int newWidth = childWidthSize;
989         int newHeight = childHeightSize;
990         if (mFixedWidth > 0 && mFixedHeight > 0) {
991             newWidth = mFixedWidth;
992             newHeight = mFixedHeight;
993         } else if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
994             throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions");
995         }
996 
997         mShortcutsAndWidgets.measure(
998                 MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY),
999                 MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY));
1000 
1001         int maxWidth = mShortcutsAndWidgets.getMeasuredWidth();
1002         int maxHeight = mShortcutsAndWidgets.getMeasuredHeight();
1003         if (mFixedWidth > 0 && mFixedHeight > 0) {
1004             setMeasuredDimension(maxWidth, maxHeight);
1005         } else {
1006             setMeasuredDimension(widthSize, heightSize);
1007         }
1008     }
1009 
1010     @Override
onLayout(boolean changed, int l, int t, int r, int b)1011     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1012         int left = getPaddingLeft();
1013         left += (int) Math.ceil(getUnusedHorizontalSpace() / 2f);
1014         int right = r - l - getPaddingRight();
1015         right -= (int) Math.ceil(getUnusedHorizontalSpace() / 2f);
1016 
1017         int top = getPaddingTop();
1018         int bottom = b - t - getPaddingBottom();
1019 
1020         // Expand the background drawing bounds by the padding baked into the background drawable
1021         mBackground.getPadding(mTempRect);
1022         mBackground.setBounds(
1023                 left - mTempRect.left - getPaddingLeft(),
1024                 top - mTempRect.top - getPaddingTop(),
1025                 right + mTempRect.right + getPaddingRight(),
1026                 bottom + mTempRect.bottom + getPaddingBottom());
1027 
1028         mShortcutsAndWidgets.layout(left, top, right, bottom);
1029     }
1030 
1031     /**
1032      * Returns the amount of space left over after subtracting padding and cells. This space will be
1033      * very small, a few pixels at most, and is a result of rounding down when calculating the cell
1034      * width in {@link DeviceProfile#calculateCellWidth(int, int, int)}.
1035      */
getUnusedHorizontalSpace()1036     public int getUnusedHorizontalSpace() {
1037         return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth)
1038                 - ((mCountX - 1) * mBorderSpace.x);
1039     }
1040 
1041     @Override
verifyDrawable(Drawable who)1042     protected boolean verifyDrawable(Drawable who) {
1043         return super.verifyDrawable(who) || (who == mBackground);
1044     }
1045 
getShortcutsAndWidgets()1046     public ShortcutAndWidgetContainer getShortcutsAndWidgets() {
1047         return mShortcutsAndWidgets;
1048     }
1049 
getChildAt(int cellX, int cellY)1050     public View getChildAt(int cellX, int cellY) {
1051         return mShortcutsAndWidgets.getChildAt(cellX, cellY);
1052     }
1053 
animateChildToPosition(final View child, int cellX, int cellY, int duration, int delay, boolean permanent, boolean adjustOccupied)1054     public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
1055             int delay, boolean permanent, boolean adjustOccupied) {
1056         ShortcutAndWidgetContainer clc = getShortcutsAndWidgets();
1057 
1058         if (clc.indexOfChild(child) != -1 && (child instanceof Reorderable)) {
1059             final CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1060             final ItemInfo info = (ItemInfo) child.getTag();
1061             final Reorderable item = (Reorderable) child;
1062 
1063             // We cancel any existing animations
1064             if (mReorderAnimators.containsKey(lp)) {
1065                 mReorderAnimators.get(lp).cancel();
1066                 mReorderAnimators.remove(lp);
1067             }
1068 
1069 
1070             if (adjustOccupied) {
1071                 GridOccupancy occupied = permanent ? mOccupied : mTmpOccupied;
1072                 occupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, false);
1073                 occupied.markCells(cellX, cellY, lp.cellHSpan, lp.cellVSpan, true);
1074             }
1075 
1076             // Compute the new x and y position based on the new cellX and cellY
1077             // We leverage the actual layout logic in the layout params and hence need to modify
1078             // state and revert that state.
1079             final int oldX = lp.x;
1080             final int oldY = lp.y;
1081             lp.isLockedToGrid = true;
1082             if (permanent) {
1083                 lp.setCellX(cellX);
1084                 lp.setCellY(cellY);
1085             } else {
1086                 lp.setTmpCellX(cellX);
1087                 lp.setTmpCellY(cellY);
1088             }
1089             clc.setupLp(child);
1090             final int newX = lp.x;
1091             final int newY = lp.y;
1092             lp.x = oldX;
1093             lp.y = oldY;
1094             lp.isLockedToGrid = false;
1095             // End compute new x and y
1096 
1097             MultiTranslateDelegate mtd = item.getTranslateDelegate();
1098             float initPreviewOffsetX = mtd.getTranslationX(INDEX_REORDER_PREVIEW_OFFSET).getValue();
1099             float initPreviewOffsetY = mtd.getTranslationY(INDEX_REORDER_PREVIEW_OFFSET).getValue();
1100             final float finalPreviewOffsetX = newX - oldX;
1101             final float finalPreviewOffsetY = newY - oldY;
1102 
1103             // Exit early if we're not actually moving the view
1104             if (finalPreviewOffsetX == 0 && finalPreviewOffsetY == 0
1105                     && initPreviewOffsetX == 0 && initPreviewOffsetY == 0) {
1106                 lp.isLockedToGrid = true;
1107                 return true;
1108             }
1109 
1110             ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
1111             va.setDuration(duration);
1112             mReorderAnimators.put(lp, va);
1113 
1114             va.addUpdateListener(new AnimatorUpdateListener() {
1115                 @Override
1116                 public void onAnimationUpdate(ValueAnimator animation) {
1117                     float r = (Float) animation.getAnimatedValue();
1118                     float x = (1 - r) * initPreviewOffsetX + r * finalPreviewOffsetX;
1119                     float y = (1 - r) * initPreviewOffsetY + r * finalPreviewOffsetY;
1120                     item.getTranslateDelegate().setTranslation(INDEX_REORDER_PREVIEW_OFFSET, x, y);
1121                 }
1122             });
1123             va.addListener(new AnimatorListenerAdapter() {
1124                 boolean cancelled = false;
1125                 public void onAnimationEnd(Animator animation) {
1126                     // If the animation was cancelled, it means that another animation
1127                     // has interrupted this one, and we don't want to lock the item into
1128                     // place just yet.
1129                     if (!cancelled) {
1130                         lp.isLockedToGrid = true;
1131                         item.getTranslateDelegate()
1132                                 .setTranslation(INDEX_REORDER_PREVIEW_OFFSET, 0, 0);
1133                         child.requestLayout();
1134                     }
1135                     if (mReorderAnimators.containsKey(lp)) {
1136                         mReorderAnimators.remove(lp);
1137                     }
1138                 }
1139                 public void onAnimationCancel(Animator animation) {
1140                     cancelled = true;
1141                 }
1142             });
1143             va.setStartDelay(delay);
1144             va.start();
1145             return true;
1146         }
1147         return false;
1148     }
1149 
visualizeDropLocation(int cellX, int cellY, int spanX, int spanY, DropTarget.DragObject dragObject)1150     void visualizeDropLocation(int cellX, int cellY, int spanX, int spanY,
1151             DropTarget.DragObject dragObject) {
1152         if (mDragCell[0] != cellX || mDragCell[1] != cellY || mDragCellSpan[0] != spanX
1153                 || mDragCellSpan[1] != spanY) {
1154             mDragCell[0] = cellX;
1155             mDragCell[1] = cellY;
1156             mDragCellSpan[0] = spanX;
1157             mDragCellSpan[1] = spanY;
1158 
1159             final int oldIndex = mDragOutlineCurrent;
1160             mDragOutlineAnims[oldIndex].animateOut();
1161             mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
1162 
1163             CellLayoutLayoutParams cell = mDragOutlines[mDragOutlineCurrent];
1164             cell.setCellX(cellX);
1165             cell.setCellY(cellY);
1166             cell.cellHSpan = spanX;
1167             cell.cellVSpan = spanY;
1168 
1169             mDragOutlineAnims[mDragOutlineCurrent].animateIn();
1170             invalidate();
1171 
1172             if (dragObject.stateAnnouncer != null) {
1173                 dragObject.stateAnnouncer.announce(getItemMoveDescription(cellX, cellY));
1174             }
1175 
1176         }
1177     }
1178 
1179     @SuppressLint("StringFormatMatches")
getItemMoveDescription(int cellX, int cellY)1180     public String getItemMoveDescription(int cellX, int cellY) {
1181         if (mContainerType == HOTSEAT) {
1182             return getContext().getString(R.string.move_to_hotseat_position,
1183                     Math.max(cellX, cellY) + 1);
1184         } else {
1185             int row = cellY + 1;
1186             int col = Utilities.isRtl(getResources()) ? mCountX - cellX : cellX + 1;
1187             int panelCount = mCellLayoutContainer.getPanelCount();
1188             int pageIndex = mCellLayoutContainer.getCellLayoutIndex(this);
1189             if (panelCount > 1) {
1190                 // Increment the column if the target is on the right side of a two panel home
1191                 col += (pageIndex % panelCount) * mCountX;
1192             }
1193             return getContext().getString(R.string.move_to_empty_cell_description, row, col,
1194                     mCellLayoutContainer.getPageDescription(pageIndex));
1195         }
1196     }
1197 
clearDragOutlines()1198     public void clearDragOutlines() {
1199         final int oldIndex = mDragOutlineCurrent;
1200         mDragOutlineAnims[oldIndex].animateOut();
1201         mDragCell[0] = mDragCell[1] = -1;
1202     }
1203 
1204     /**
1205      * Find a vacant area that will fit the given bounds nearest the requested
1206      * cell location. Uses Euclidean distance to score multiple vacant areas.
1207      *
1208      * @param pixelX The X location at which you want to search for a vacant area.
1209      * @param pixelY The Y location at which you want to search for a vacant area.
1210      * @param minSpanX The minimum horizontal span required
1211      * @param minSpanY The minimum vertical span required
1212      * @param spanX Horizontal span of the object.
1213      * @param spanY Vertical span of the object.
1214      * @param result Array in which to place the result, or null (in which case a new array will
1215      *        be allocated)
1216      * @return The X, Y cell of a vacant area that can contain this object,
1217      *         nearest the requested location.
1218      */
findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] result, int[] resultSpan)1219     public int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY,
1220             int spanX, int spanY, int[] result, int[] resultSpan) {
1221         return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, false,
1222                 result, resultSpan);
1223     }
1224 
1225     /**
1226      * Find a vacant area that will fit the given bounds nearest the requested
1227      * cell location. Uses Euclidean distance to score multiple vacant areas.
1228      * @param relativeXPos The X location relative to the Cell layout at which you want to search
1229      *                     for a vacant area.
1230      * @param relativeYPos The Y location relative to the Cell layout at which you want to search
1231      *                     for a vacant area.
1232      * @param minSpanX The minimum horizontal span required
1233      * @param minSpanY The minimum vertical span required
1234      * @param spanX Horizontal span of the object.
1235      * @param spanY Vertical span of the object.
1236      * @param ignoreOccupied If true, the result can be an occupied cell
1237      * @param result Array in which to place the result, or null (in which case a new array will
1238      *        be allocated)
1239      * @return The X, Y cell of a vacant area that can contain this object,
1240      *         nearest the requested location.
1241      */
findNearestArea(int relativeXPos, int relativeYPos, int minSpanX, int minSpanY, int spanX, int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan)1242     protected int[] findNearestArea(int relativeXPos, int relativeYPos, int minSpanX, int minSpanY,
1243             int spanX, int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan) {
1244         // For items with a spanX / spanY > 1, the passed in point (relativeXPos, relativeYPos)
1245         // corresponds to the center of the item, but we are searching based on the top-left cell,
1246         // so we translate the point over to correspond to the top-left.
1247         relativeXPos = (int) (relativeXPos - (mCellWidth + mBorderSpace.x) * (spanX - 1) / 2f);
1248         relativeYPos = (int) (relativeYPos - (mCellHeight + mBorderSpace.y) * (spanY - 1) / 2f);
1249 
1250         // Keep track of best-scoring drop area
1251         final int[] bestXY = result != null ? result : new int[2];
1252         double bestDistance = Double.MAX_VALUE;
1253         final Rect bestRect = new Rect(-1, -1, -1, -1);
1254         final Stack<Rect> validRegions = new Stack<>();
1255 
1256         final int countX = mCountX;
1257         final int countY = mCountY;
1258 
1259         if (minSpanX <= 0 || minSpanY <= 0 || spanX <= 0 || spanY <= 0 ||
1260                 spanX < minSpanX || spanY < minSpanY) {
1261             return bestXY;
1262         }
1263 
1264         for (int y = 0; y < countY - (minSpanY - 1); y++) {
1265             inner:
1266             for (int x = 0; x < countX - (minSpanX - 1); x++) {
1267                 int ySize = -1;
1268                 int xSize = -1;
1269                 if (!ignoreOccupied) {
1270                     // First, let's see if this thing fits anywhere
1271                     for (int i = 0; i < minSpanX; i++) {
1272                         for (int j = 0; j < minSpanY; j++) {
1273                             if (mOccupied.cells[x + i][y + j]) {
1274                                 continue inner;
1275                             }
1276                         }
1277                     }
1278                     xSize = minSpanX;
1279                     ySize = minSpanY;
1280 
1281                     // We know that the item will fit at _some_ acceptable size, now let's see
1282                     // how big we can make it. We'll alternate between incrementing x and y spans
1283                     // until we hit a limit.
1284                     boolean incX = true;
1285                     boolean hitMaxX = xSize >= spanX;
1286                     boolean hitMaxY = ySize >= spanY;
1287                     while (!(hitMaxX && hitMaxY)) {
1288                         if (incX && !hitMaxX) {
1289                             for (int j = 0; j < ySize; j++) {
1290                                 if (x + xSize > countX -1 || mOccupied.cells[x + xSize][y + j]) {
1291                                     // We can't move out horizontally
1292                                     hitMaxX = true;
1293                                 }
1294                             }
1295                             if (!hitMaxX) {
1296                                 xSize++;
1297                             }
1298                         } else if (!hitMaxY) {
1299                             for (int i = 0; i < xSize; i++) {
1300                                 if (y + ySize > countY - 1 || mOccupied.cells[x + i][y + ySize]) {
1301                                     // We can't move out vertically
1302                                     hitMaxY = true;
1303                                 }
1304                             }
1305                             if (!hitMaxY) {
1306                                 ySize++;
1307                             }
1308                         }
1309                         hitMaxX |= xSize >= spanX;
1310                         hitMaxY |= ySize >= spanY;
1311                         incX = !incX;
1312                     }
1313                 }
1314                 final int[] cellXY = mTmpPoint;
1315                 cellToCenterPoint(x, y, cellXY);
1316 
1317                 // We verify that the current rect is not a sub-rect of any of our previous
1318                 // candidates. In this case, the current rect is disqualified in favour of the
1319                 // containing rect.
1320                 Rect currentRect = new Rect(x, y, x + xSize, y + ySize);
1321                 boolean contained = false;
1322                 for (Rect r : validRegions) {
1323                     if (r.contains(currentRect)) {
1324                         contained = true;
1325                         break;
1326                     }
1327                 }
1328                 validRegions.push(currentRect);
1329                 double distance = Math.hypot(cellXY[0] - relativeXPos,  cellXY[1] - relativeYPos);
1330 
1331                 if ((distance <= bestDistance && !contained) ||
1332                         currentRect.contains(bestRect)) {
1333                     bestDistance = distance;
1334                     bestXY[0] = x;
1335                     bestXY[1] = y;
1336                     if (resultSpan != null) {
1337                         resultSpan[0] = xSize;
1338                         resultSpan[1] = ySize;
1339                     }
1340                     bestRect.set(currentRect);
1341                 }
1342             }
1343         }
1344 
1345         // Return -1, -1 if no suitable location found
1346         if (bestDistance == Double.MAX_VALUE) {
1347             bestXY[0] = -1;
1348             bestXY[1] = -1;
1349         }
1350         return bestXY;
1351     }
1352 
getOccupied()1353     public GridOccupancy getOccupied() {
1354         return mOccupied;
1355     }
1356 
copySolutionToTempState(ItemConfiguration solution, View dragView)1357     private void copySolutionToTempState(ItemConfiguration solution, View dragView) {
1358         mTmpOccupied.clear();
1359 
1360         int childCount = mShortcutsAndWidgets.getChildCount();
1361         for (int i = 0; i < childCount; i++) {
1362             View child = mShortcutsAndWidgets.getChildAt(i);
1363             if (child == dragView) continue;
1364             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1365             CellAndSpan c = solution.map.get(child);
1366             if (c != null) {
1367                 lp.setTmpCellX(c.cellX);
1368                 lp.setTmpCellY(c.cellY);
1369                 lp.cellHSpan = c.spanX;
1370                 lp.cellVSpan = c.spanY;
1371                 mTmpOccupied.markCells(c, true);
1372             }
1373         }
1374         mTmpOccupied.markCells(solution, true);
1375     }
1376 
animateItemsToSolution(ItemConfiguration solution, View dragView, boolean commitDragView)1377     private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean
1378             commitDragView) {
1379 
1380         GridOccupancy occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied;
1381         occupied.clear();
1382 
1383         int childCount = mShortcutsAndWidgets.getChildCount();
1384         for (int i = 0; i < childCount; i++) {
1385             View child = mShortcutsAndWidgets.getChildAt(i);
1386             if (child == dragView) continue;
1387             CellAndSpan c = solution.map.get(child);
1388             if (c != null) {
1389                 animateChildToPosition(child, c.cellX, c.cellY, REORDER_ANIMATION_DURATION, 0,
1390                         DESTRUCTIVE_REORDER, false);
1391                 occupied.markCells(c, true);
1392             }
1393         }
1394         if (commitDragView) {
1395             occupied.markCells(solution, true);
1396         }
1397     }
1398 
1399 
1400     // This method starts or changes the reorder preview animations
beginOrAdjustReorderPreviewAnimations(ItemConfiguration solution, View dragView, int mode)1401     private void beginOrAdjustReorderPreviewAnimations(ItemConfiguration solution,
1402             View dragView, int mode) {
1403         int childCount = mShortcutsAndWidgets.getChildCount();
1404         for (int i = 0; i < childCount; i++) {
1405             View child = mShortcutsAndWidgets.getChildAt(i);
1406             if (child == dragView) continue;
1407             CellAndSpan c = solution.map.get(child);
1408             boolean skip = mode == ReorderPreviewAnimation.MODE_HINT
1409                     && !solution.intersectingViews.contains(child);
1410 
1411             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1412             if (c != null && !skip && (child instanceof Reorderable)) {
1413                 ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode,
1414                         lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY,
1415                         mReorderPreviewAnimationMagnitude, this, mShakeAnimators);
1416                 rha.animate();
1417             }
1418         }
1419     }
1420 
completeAndClearReorderPreviewAnimations()1421     private void completeAndClearReorderPreviewAnimations() {
1422         for (ReorderPreviewAnimation a: mShakeAnimators.values()) {
1423             a.finishAnimation();
1424         }
1425         mShakeAnimators.clear();
1426     }
1427 
commitTempPlacement(View dragView)1428     private void commitTempPlacement(View dragView) {
1429         mTmpOccupied.copyTo(mOccupied);
1430 
1431         int screenId = mCellLayoutContainer.getCellLayoutId(this);
1432         int container = Favorites.CONTAINER_DESKTOP;
1433 
1434         if (mContainerType == HOTSEAT) {
1435             screenId = -1;
1436             container = Favorites.CONTAINER_HOTSEAT;
1437         }
1438 
1439         int childCount = mShortcutsAndWidgets.getChildCount();
1440         for (int i = 0; i < childCount; i++) {
1441             View child = mShortcutsAndWidgets.getChildAt(i);
1442             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1443             ItemInfo info = (ItemInfo) child.getTag();
1444             // We do a null check here because the item info can be null in the case of the
1445             // AllApps button in the hotseat.
1446             if (info != null && child != dragView) {
1447                 CellPos presenterPos = mActivity.getCellPosMapper().mapModelToPresenter(info);
1448                 final boolean requiresDbUpdate = (presenterPos.cellX != lp.getTmpCellX()
1449                         || presenterPos.cellY != lp.getTmpCellY() || info.spanX != lp.cellHSpan
1450                         || info.spanY != lp.cellVSpan || presenterPos.screenId != screenId);
1451 
1452                 lp.setCellX(lp.getTmpCellX());
1453                 lp.setCellY(lp.getTmpCellY());
1454                 if (requiresDbUpdate) {
1455                     Launcher.cast(mActivity).getModelWriter().modifyItemInDatabase(info, container,
1456                             screenId, lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan);
1457                 }
1458             }
1459         }
1460     }
1461 
setUseTempCoords(boolean useTempCoords)1462     private void setUseTempCoords(boolean useTempCoords) {
1463         int childCount = mShortcutsAndWidgets.getChildCount();
1464         for (int i = 0; i < childCount; i++) {
1465             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mShortcutsAndWidgets.getChildAt(
1466                     i).getLayoutParams();
1467             lp.useTmpCoords = useTempCoords;
1468         }
1469     }
1470 
1471     /**
1472      * For a given region, return the rectangle of the overlapping cell and span with the given
1473      * region including the region itself. If there is no overlap the rectangle will be
1474      * invalid i.e. -1, 0, -1, 0.
1475      */
1476     @Nullable
getIntersectingRectanglesInRegion(final Rect region, final View dragView)1477     public Rect getIntersectingRectanglesInRegion(final Rect region, final View dragView) {
1478         Rect boundingRect = new Rect(region);
1479         Rect r1 = new Rect();
1480         boolean isOverlapping = false;
1481         final int count = mShortcutsAndWidgets.getChildCount();
1482         for (int i = 0; i < count; i++) {
1483             View child = mShortcutsAndWidgets.getChildAt(i);
1484             if (child == dragView) continue;
1485             CellLayoutLayoutParams
1486                     lp = (CellLayoutLayoutParams) child.getLayoutParams();
1487             r1.set(lp.getCellX(), lp.getCellY(), lp.getCellX() + lp.cellHSpan,
1488                     lp.getCellY() + lp.cellVSpan);
1489             if (Rect.intersects(region, r1)) {
1490                 isOverlapping = true;
1491                 boundingRect.union(r1);
1492             }
1493         }
1494         return isOverlapping ? boundingRect : null;
1495     }
1496 
isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY, View dragView, int[] result)1497     public boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY,
1498             View dragView, int[] result) {
1499         result = findNearestAreaIgnoreOccupied(pixelX, pixelY, spanX, spanY, result);
1500         return getIntersectingRectanglesInRegion(
1501                 new Rect(result[0], result[1], result[0] + spanX, result[1] + spanY),
1502                 dragView
1503         ) != null;
1504     }
1505 
revertTempState()1506     void revertTempState() {
1507         completeAndClearReorderPreviewAnimations();
1508         if (isItemPlacementDirty() && !DESTRUCTIVE_REORDER) {
1509             final int count = mShortcutsAndWidgets.getChildCount();
1510             for (int i = 0; i < count; i++) {
1511                 View child = mShortcutsAndWidgets.getChildAt(i);
1512                 CellLayoutLayoutParams
1513                         lp = (CellLayoutLayoutParams) child.getLayoutParams();
1514                 if (lp.getTmpCellX() != lp.getCellX() || lp.getTmpCellY() != lp.getCellY()) {
1515                     lp.setTmpCellX(lp.getCellX());
1516                     lp.setTmpCellY(lp.getCellY());
1517                     animateChildToPosition(child, lp.getCellX(), lp.getCellY(),
1518                             REORDER_ANIMATION_DURATION, 0, false, false);
1519                 }
1520             }
1521             setItemPlacementDirty(false);
1522         }
1523     }
1524 
createAreaForResize(int cellX, int cellY, int spanX, int spanY, View dragView, int[] direction, boolean commit)1525     boolean createAreaForResize(int cellX, int cellY, int spanX, int spanY,
1526             View dragView, int[] direction, boolean commit) {
1527         int[] pixelXY = new int[2];
1528         regionToCenterPoint(cellX, cellY, spanX, spanY, pixelXY);
1529 
1530         // First we determine if things have moved enough to cause a different layout
1531         ItemConfiguration swapSolution = findReorderSolution(pixelXY[0], pixelXY[1], spanX, spanY,
1532                 spanX,  spanY, direction, dragView,  true);
1533 
1534         setUseTempCoords(true);
1535         if (swapSolution != null && swapSolution.isSolution) {
1536             // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
1537             // committing anything or animating anything as we just want to determine if a solution
1538             // exists
1539             copySolutionToTempState(swapSolution, dragView);
1540             setItemPlacementDirty(true);
1541             animateItemsToSolution(swapSolution, dragView, commit);
1542 
1543             if (commit) {
1544                 commitTempPlacement(null);
1545                 completeAndClearReorderPreviewAnimations();
1546                 setItemPlacementDirty(false);
1547             } else {
1548                 beginOrAdjustReorderPreviewAnimations(swapSolution, dragView,
1549                         ReorderPreviewAnimation.MODE_PREVIEW);
1550             }
1551             mShortcutsAndWidgets.requestLayout();
1552         }
1553         return swapSolution.isSolution;
1554     }
1555 
createReorderAlgorithm()1556     public ReorderAlgorithm createReorderAlgorithm() {
1557         return new ReorderAlgorithm(this);
1558     }
1559 
findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX)1560     protected ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX,
1561             int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX) {
1562         ItemConfiguration configuration = new ItemConfiguration();
1563         copyCurrentStateToSolution(configuration);
1564         ReorderParameters parameters = new ReorderParameters(pixelX, pixelY, spanX, spanY, minSpanX,
1565                 minSpanY, dragView, configuration);
1566         int[] directionVector = direction != null ? direction : mDirectionVector;
1567         return createReorderAlgorithm().findReorderSolution(parameters, directionVector, decX);
1568     }
1569 
copyCurrentStateToSolution(ItemConfiguration solution)1570     public void copyCurrentStateToSolution(ItemConfiguration solution) {
1571         int childCount = mShortcutsAndWidgets.getChildCount();
1572         for (int i = 0; i < childCount; i++) {
1573             View child = mShortcutsAndWidgets.getChildAt(i);
1574             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1575             solution.add(child,
1576                     new CellAndSpan(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan));
1577         }
1578     }
1579 
1580     /**
1581      * When the user drags an Item in the workspace sometimes we need to move the items already in
1582      * the workspace to make space for the new item, this function return a solution for that
1583      * reorder.
1584      *
1585      * @param pixelX X coordinate in the screen of the dragView in pixels
1586      * @param pixelY Y coordinate in the screen of the dragView in pixels
1587      * @param minSpanX minimum horizontal span the item can be shrunk to
1588      * @param minSpanY minimum vertical span the item can be shrunk to
1589      * @param spanX occupied horizontal span
1590      * @param spanY occupied vertical span
1591      * @param dragView the view of the item being draged
1592      * @return returns a solution for the given parameters, the solution contains all the icons and
1593      *         the locations they should be in the given solution.
1594      */
calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, View dragView)1595     public ItemConfiguration calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY,
1596             int spanX, int spanY, View dragView) {
1597         ItemConfiguration configuration = new ItemConfiguration();
1598         copyCurrentStateToSolution(configuration);
1599         return createReorderAlgorithm().calculateReorder(
1600                 new ReorderParameters(pixelX, pixelY, spanX, spanY,  minSpanX, minSpanY, dragView,
1601                         configuration)
1602         );
1603     }
1604 
performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, View dragView, int[] result, int[] resultSpan, int mode)1605     int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
1606             View dragView, int[] result, int[] resultSpan, int mode) {
1607         if (resultSpan == null) {
1608             resultSpan = new int[]{-1, -1};
1609         }
1610         if (result == null) {
1611             result = new int[]{-1, -1};
1612         }
1613 
1614         ItemConfiguration finalSolution = null;
1615         // We want the solution to match the animation of the preview and to match the drop so we
1616         // only recalculate in mode MODE_SHOW_REORDER_HINT because that the first one to run in the
1617         // reorder cycle.
1618         if (mode == MODE_SHOW_REORDER_HINT || mPreviousSolution == null) {
1619             finalSolution = calculateReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
1620                     dragView);
1621             mPreviousSolution = finalSolution;
1622         } else {
1623             finalSolution = mPreviousSolution;
1624             // We reset this vector after drop
1625             if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
1626                 mPreviousSolution = null;
1627             }
1628         }
1629 
1630         if (finalSolution == null || !finalSolution.isSolution) {
1631             result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
1632         } else {
1633             result[0] = finalSolution.cellX;
1634             result[1] = finalSolution.cellY;
1635             resultSpan[0] = finalSolution.spanX;
1636             resultSpan[1] = finalSolution.spanY;
1637             performReorder(finalSolution, dragView, mode);
1638         }
1639         return result;
1640     }
1641 
1642     /**
1643      * Animates and submits in the DB the given ItemConfiguration depending of the mode.
1644      *
1645      * @param solution represents widgets on the screen which the Workspace will animate to and
1646      * would be submitted to the database.
1647      * @param dragView view which is being dragged over the workspace that trigger the reorder
1648      * @param mode depending on the mode different animations would be played and depending on the
1649      *             mode the solution would be submitted or not the database.
1650      *             The possible modes are {@link MODE_SHOW_REORDER_HINT}, {@link MODE_DRAG_OVER},
1651      *             {@link MODE_ON_DROP}, {@link MODE_ON_DROP_EXTERNAL}, {@link  MODE_ACCEPT_DROP}
1652      *             defined in {@link CellLayout}.
1653      */
performReorder(ItemConfiguration solution, View dragView, int mode)1654     public void performReorder(ItemConfiguration solution, View dragView, int mode) {
1655         if (mode == MODE_SHOW_REORDER_HINT) {
1656             beginOrAdjustReorderPreviewAnimations(solution, dragView,
1657                     ReorderPreviewAnimation.MODE_HINT);
1658             return;
1659         }
1660         // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
1661         // committing anything or animating anything as we just want to determine if a solution
1662         // exists
1663         if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
1664             if (!DESTRUCTIVE_REORDER) {
1665                 setUseTempCoords(true);
1666             }
1667 
1668             if (!DESTRUCTIVE_REORDER) {
1669                 copySolutionToTempState(solution, dragView);
1670             }
1671             setItemPlacementDirty(true);
1672             animateItemsToSolution(solution, dragView, mode == MODE_ON_DROP);
1673 
1674             if (!DESTRUCTIVE_REORDER
1675                     && (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
1676                 // Since the temp solution didn't update dragView, don't commit it either
1677                 commitTempPlacement(dragView);
1678                 completeAndClearReorderPreviewAnimations();
1679                 setItemPlacementDirty(false);
1680             } else {
1681                 beginOrAdjustReorderPreviewAnimations(solution, dragView,
1682                         ReorderPreviewAnimation.MODE_PREVIEW);
1683             }
1684         }
1685 
1686         if (mode == MODE_ON_DROP && !DESTRUCTIVE_REORDER) {
1687             setUseTempCoords(false);
1688         }
1689 
1690         mShortcutsAndWidgets.requestLayout();
1691     }
1692 
setItemPlacementDirty(boolean dirty)1693     void setItemPlacementDirty(boolean dirty) {
1694         mItemPlacementDirty = dirty;
1695     }
isItemPlacementDirty()1696     boolean isItemPlacementDirty() {
1697         return mItemPlacementDirty;
1698     }
1699 
1700     /**
1701      * Find a starting cell position that will fit the given bounds nearest the requested
1702      * cell location. Uses Euclidean distance to score multiple vacant areas.
1703      *
1704      * @param pixelX The X location at which you want to search for a vacant area.
1705      * @param pixelY The Y location at which you want to search for a vacant area.
1706      * @param spanX Horizontal span of the object.
1707      * @param spanY Vertical span of the object.
1708      * @param result Previously returned value to possibly recycle.
1709      * @return The X, Y cell of a vacant area that can contain this object,
1710      *         nearest the requested location.
1711      */
findNearestAreaIgnoreOccupied(int pixelX, int pixelY, int spanX, int spanY, int[] result)1712     public int[] findNearestAreaIgnoreOccupied(int pixelX, int pixelY, int spanX, int spanY,
1713             int[] result) {
1714         return findNearestArea(pixelX, pixelY, spanX, spanY, spanX, spanY, true, result, null);
1715     }
1716 
existsEmptyCell()1717     boolean existsEmptyCell() {
1718         return findCellForSpan(null, 1, 1);
1719     }
1720 
1721     /**
1722      * Finds the upper-left coordinate of the first rectangle in the grid that can
1723      * hold a cell of the specified dimensions. If intersectX and intersectY are not -1,
1724      * then this method will only return coordinates for rectangles that contain the cell
1725      * (intersectX, intersectY)
1726      *
1727      * @param cellXY The array that will contain the position of a vacant cell if such a cell
1728      *               can be found.
1729      * @param spanX The horizontal span of the cell we want to find.
1730      * @param spanY The vertical span of the cell we want to find.
1731      *
1732      * @return True if a vacant cell of the specified dimension was found, false otherwise.
1733      */
findCellForSpan(int[] cellXY, int spanX, int spanY)1734     public boolean findCellForSpan(int[] cellXY, int spanX, int spanY) {
1735         if (cellXY == null) {
1736             cellXY = new int[2];
1737         }
1738         return mOccupied.findVacantCell(cellXY, spanX, spanY);
1739     }
1740 
1741     /**
1742      * A drag event has begun over this layout.
1743      * It may have begun over this layout (in which case onDragChild is called first),
1744      * or it may have begun on another layout.
1745      */
onDragEnter()1746     void onDragEnter() {
1747         mDragging = true;
1748         mPreviousSolution = null;
1749     }
1750 
1751     /**
1752      * Called when drag has left this CellLayout or has been completed (successfully or not)
1753      */
onDragExit()1754     void onDragExit() {
1755         // This can actually be called when we aren't in a drag, e.g. when adding a new
1756         // item to this layout via the customize drawer.
1757         // Guard against that case.
1758         if (mDragging) {
1759             mDragging = false;
1760         }
1761 
1762         // Invalidate the drag data
1763         mPreviousSolution = null;
1764         mDragCell[0] = mDragCell[1] = -1;
1765         mDragCellSpan[0] = mDragCellSpan[1] = -1;
1766         mDragOutlineAnims[mDragOutlineCurrent].animateOut();
1767         mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length;
1768         revertTempState();
1769         setIsDragOverlapping(false);
1770     }
1771 
1772     /**
1773      * Mark a child as having been dropped.
1774      * At the beginning of the drag operation, the child may have been on another
1775      * screen, but it is re-parented before this method is called.
1776      *
1777      * @param child The child that is being dropped
1778      */
onDropChild(View child)1779     void onDropChild(View child) {
1780         if (child != null) {
1781             CellLayoutLayoutParams
1782                     lp = (CellLayoutLayoutParams) child.getLayoutParams();
1783             lp.dropped = true;
1784             child.requestLayout();
1785             markCellsAsOccupiedForView(child);
1786         }
1787     }
1788 
1789     /**
1790      * Computes a bounding rectangle for a range of cells
1791      *
1792      * @param cellX X coordinate of upper left corner expressed as a cell position
1793      * @param cellY Y coordinate of upper left corner expressed as a cell position
1794      * @param cellHSpan Width in cells
1795      * @param cellVSpan Height in cells
1796      * @param resultRect Rect into which to put the results
1797      */
cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect)1798     public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect) {
1799         final int cellWidth = mCellWidth;
1800         final int cellHeight = mCellHeight;
1801 
1802         // We observe a shift of 1 pixel on the x coordinate compared to the actual cell coordinates
1803         final int hStartPadding = getPaddingLeft()
1804                 + (int) Math.ceil(getUnusedHorizontalSpace() / 2f);
1805         final int vStartPadding = getPaddingTop();
1806 
1807         int x = hStartPadding + (cellX * mBorderSpace.x) + (cellX * cellWidth);
1808         int y = vStartPadding + (cellY * mBorderSpace.y) + (cellY * cellHeight);
1809 
1810         int width = cellHSpan * cellWidth + ((cellHSpan - 1) * mBorderSpace.x);
1811         int height = cellVSpan * cellHeight + ((cellVSpan - 1) * mBorderSpace.y);
1812 
1813         resultRect.set(x, y, x + width, y + height);
1814     }
1815 
markCellsAsOccupiedForView(View view)1816     public void markCellsAsOccupiedForView(View view) {
1817         if (view instanceof LauncherAppWidgetHostView
1818                 && view.getTag() instanceof LauncherAppWidgetInfo) {
1819             LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag();
1820             CellPos pos = mActivity.getCellPosMapper().mapModelToPresenter(info);
1821             mOccupied.markCells(pos.cellX, pos.cellY, info.spanX, info.spanY, true);
1822             return;
1823         }
1824         if (view == null || view.getParent() != mShortcutsAndWidgets) return;
1825         CellLayoutLayoutParams
1826                 lp = (CellLayoutLayoutParams) view.getLayoutParams();
1827         mOccupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, true);
1828     }
1829 
markCellsAsUnoccupiedForView(View view)1830     public void markCellsAsUnoccupiedForView(View view) {
1831         if (view instanceof LauncherAppWidgetHostView
1832                 && view.getTag() instanceof LauncherAppWidgetInfo) {
1833             LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag();
1834             CellPos pos = mActivity.getCellPosMapper().mapModelToPresenter(info);
1835             mOccupied.markCells(pos.cellX, pos.cellY, info.spanX, info.spanY, false);
1836             return;
1837         }
1838         if (view == null || view.getParent() != mShortcutsAndWidgets) return;
1839         CellLayoutLayoutParams
1840                 lp = (CellLayoutLayoutParams) view.getLayoutParams();
1841         mOccupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, false);
1842     }
1843 
getDesiredWidth()1844     public int getDesiredWidth() {
1845         return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth)
1846                 + ((mCountX - 1) * mBorderSpace.x);
1847     }
1848 
getDesiredHeight()1849     public int getDesiredHeight()  {
1850         return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight)
1851                 + ((mCountY - 1) * mBorderSpace.y);
1852     }
1853 
isOccupied(int x, int y)1854     public boolean isOccupied(int x, int y) {
1855         if (x >= 0 && x < mCountX && y >= 0 && y < mCountY) {
1856             return mOccupied.cells[x][y];
1857         }
1858         if (BuildConfig.IS_STUDIO_BUILD) {
1859             throw new RuntimeException("Position exceeds the bound of this CellLayout");
1860         }
1861         return true;
1862     }
1863 
1864     @Override
generateLayoutParams(AttributeSet attrs)1865     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
1866         return new CellLayoutLayoutParams(getContext(), attrs);
1867     }
1868 
1869     @Override
checkLayoutParams(ViewGroup.LayoutParams p)1870     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
1871         return p instanceof CellLayoutLayoutParams;
1872     }
1873 
1874     @Override
generateLayoutParams(ViewGroup.LayoutParams p)1875     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
1876         return new CellLayoutLayoutParams(p);
1877     }
1878 
1879     /**
1880      * Returns whether an item can be placed in this CellLayout (after rearranging and/or resizing
1881      * if necessary).
1882      */
hasReorderSolution(ItemInfo itemInfo)1883     public boolean hasReorderSolution(ItemInfo itemInfo) {
1884         int[] cellPoint = new int[2];
1885         // Check for a solution starting at every cell.
1886         for (int cellX = 0; cellX < getCountX(); cellX++) {
1887             for (int cellY = 0; cellY < getCountY(); cellY++) {
1888                 cellToPoint(cellX, cellY, cellPoint);
1889                 if (findReorderSolution(cellPoint[0], cellPoint[1], itemInfo.minSpanX,
1890                         itemInfo.minSpanY, itemInfo.spanX, itemInfo.spanY, mDirectionVector, null,
1891                         true).isSolution) {
1892                     return true;
1893                 }
1894             }
1895         }
1896         return false;
1897     }
1898 
1899     /**
1900      * Finds solution to accept hotseat migration to cell layout. commits solution if commitConfig
1901      */
makeSpaceForHotseatMigration(boolean commitConfig)1902     public boolean makeSpaceForHotseatMigration(boolean commitConfig) {
1903         int[] cellPoint = new int[2];
1904         int[] directionVector = new int[]{0, -1};
1905         cellToPoint(0, mCountY, cellPoint);
1906         ItemConfiguration configuration = findReorderSolution(
1907                 cellPoint[0] /* pixelX */,
1908                 cellPoint[1] /* pixelY */,
1909                 mCountX /* minSpanX */,
1910                 1 /* minSpanY */,
1911                 mCountX /* spanX */,
1912                 1 /* spanY */,
1913                 directionVector /* direction */,
1914                 null /* dragView */,
1915                 false /* decX */
1916         );
1917         if (configuration.isSolution) {
1918             if (commitConfig) {
1919                 copySolutionToTempState(configuration, null);
1920                 commitTempPlacement(null);
1921                 // undo marking cells occupied since there is actually nothing being placed yet.
1922                 mOccupied.markCells(0, mCountY - 1, mCountX, 1, false);
1923             }
1924             return true;
1925         }
1926         return false;
1927     }
1928 
1929     /**
1930      * returns a copy of cell layout's grid occupancy
1931      */
cloneGridOccupancy()1932     public GridOccupancy cloneGridOccupancy() {
1933         GridOccupancy occupancy = new GridOccupancy(mCountX, mCountY);
1934         mOccupied.copyTo(occupancy);
1935         return occupancy;
1936     }
1937 
isRegionVacant(int x, int y, int spanX, int spanY)1938     public boolean isRegionVacant(int x, int y, int spanX, int spanY) {
1939         return mOccupied.isRegionVacant(x, y, spanX, spanY);
1940     }
1941 
setSpaceBetweenCellLayoutsPx(@x int spaceBetweenCellLayoutsPx)1942     public void setSpaceBetweenCellLayoutsPx(@Px int spaceBetweenCellLayoutsPx) {
1943         mSpaceBetweenCellLayoutsPx = spaceBetweenCellLayoutsPx;
1944     }
1945 }
1946