• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.animation.ValueAnimator.areAnimatorsEnabled;
20 
21 import static com.android.app.animation.Interpolators.DECELERATE_1_5;
22 import static com.android.launcher3.LauncherState.EDIT_MODE;
23 import static com.android.launcher3.dragndrop.DraggableView.DRAGGABLE_ICON;
24 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
25 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET;
26 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_PREVIEW_OFFSET;
27 
28 import android.animation.Animator;
29 import android.animation.AnimatorListenerAdapter;
30 import android.animation.ObjectAnimator;
31 import android.animation.TimeInterpolator;
32 import android.animation.ValueAnimator;
33 import android.animation.ValueAnimator.AnimatorUpdateListener;
34 import android.annotation.SuppressLint;
35 import android.content.Context;
36 import android.content.res.Resources;
37 import android.content.res.TypedArray;
38 import android.graphics.Canvas;
39 import android.graphics.Color;
40 import android.graphics.Paint;
41 import android.graphics.Point;
42 import android.graphics.PointF;
43 import android.graphics.Rect;
44 import android.graphics.RectF;
45 import android.graphics.drawable.Drawable;
46 import android.os.Parcelable;
47 import android.util.ArrayMap;
48 import android.util.AttributeSet;
49 import android.util.FloatProperty;
50 import android.util.Log;
51 import android.util.Property;
52 import android.util.SparseArray;
53 import android.view.MotionEvent;
54 import android.view.View;
55 import android.view.ViewDebug;
56 import android.view.ViewGroup;
57 import android.view.accessibility.AccessibilityEvent;
58 
59 import androidx.annotation.IntDef;
60 import androidx.core.graphics.ColorUtils;
61 import androidx.core.view.ViewCompat;
62 
63 import com.android.app.animation.Interpolators;
64 import com.android.launcher3.LauncherSettings.Favorites;
65 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
66 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
67 import com.android.launcher3.celllayout.CellPosMapper.CellPos;
68 import com.android.launcher3.celllayout.ReorderAlgorithm;
69 import com.android.launcher3.config.FeatureFlags;
70 import com.android.launcher3.dragndrop.DraggableView;
71 import com.android.launcher3.folder.PreviewBackground;
72 import com.android.launcher3.model.data.ItemInfo;
73 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
74 import com.android.launcher3.util.CellAndSpan;
75 import com.android.launcher3.util.GridOccupancy;
76 import com.android.launcher3.util.MultiTranslateDelegate;
77 import com.android.launcher3.util.ParcelableSparseArray;
78 import com.android.launcher3.util.Themes;
79 import com.android.launcher3.util.Thunk;
80 import com.android.launcher3.views.ActivityContext;
81 import com.android.launcher3.widget.LauncherAppWidgetHostView;
82 
83 import java.lang.annotation.Retention;
84 import java.lang.annotation.RetentionPolicy;
85 import java.util.ArrayList;
86 import java.util.Arrays;
87 import java.util.Collections;
88 import java.util.Comparator;
89 import java.util.List;
90 import java.util.Stack;
91 
92 public class CellLayout extends ViewGroup {
93     private static final String TAG = "CellLayout";
94     private static final boolean LOGD = false;
95 
96     /** The color of the "leave-behind" shape when a folder is opened from Hotseat. */
97     private static final int FOLDER_LEAVE_BEHIND_COLOR = Color.argb(160, 245, 245, 245);
98 
99     protected final ActivityContext mActivity;
100     @ViewDebug.ExportedProperty(category = "launcher")
101     @Thunk int mCellWidth;
102     @ViewDebug.ExportedProperty(category = "launcher")
103     @Thunk int mCellHeight;
104     private int mFixedCellWidth;
105     private int mFixedCellHeight;
106     @ViewDebug.ExportedProperty(category = "launcher")
107     protected Point mBorderSpace;
108 
109     @ViewDebug.ExportedProperty(category = "launcher")
110     protected int mCountX;
111     @ViewDebug.ExportedProperty(category = "launcher")
112     protected int mCountY;
113 
114     private boolean mDropPending = false;
115 
116     // These are temporary variables to prevent having to allocate a new object just to
117     // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
118     @Thunk final int[] mTmpPoint = new int[2];
119     @Thunk final int[] mTempLocation = new int[2];
120     final PointF mTmpPointF = new PointF();
121 
122     protected GridOccupancy mOccupied;
123     public GridOccupancy mTmpOccupied;
124 
125     private OnTouchListener mInterceptTouchListener;
126 
127     private final ArrayList<DelegatedCellDrawing> mDelegatedCellDrawings = new ArrayList<>();
128     final PreviewBackground mFolderLeaveBehind = new PreviewBackground();
129 
130     private static final int[] BACKGROUND_STATE_ACTIVE = new int[] { android.R.attr.state_active };
131     private static final int[] BACKGROUND_STATE_DEFAULT = EMPTY_STATE_SET;
132     protected final Drawable mBackground;
133 
134     // These values allow a fixed measurement to be set on the CellLayout.
135     private int mFixedWidth = -1;
136     private int mFixedHeight = -1;
137 
138     // If we're actively dragging something over this screen, mIsDragOverlapping is true
139     private boolean mIsDragOverlapping = false;
140 
141     // These arrays are used to implement the drag visualization on x-large screens.
142     // They are used as circular arrays, indexed by mDragOutlineCurrent.
143     @Thunk final CellLayoutLayoutParams[] mDragOutlines = new CellLayoutLayoutParams[4];
144     @Thunk final float[] mDragOutlineAlphas = new float[mDragOutlines.length];
145     private final InterruptibleInOutAnimator[] mDragOutlineAnims =
146             new InterruptibleInOutAnimator[mDragOutlines.length];
147 
148     // Used as an index into the above 3 arrays; indicates which is the most current value.
149     private int mDragOutlineCurrent = 0;
150     private final Paint mDragOutlinePaint = new Paint();
151 
152     @Thunk final ArrayMap<CellLayoutLayoutParams, Animator> mReorderAnimators = new ArrayMap<>();
153     @Thunk final ArrayMap<Reorderable, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>();
154 
155     private boolean mItemPlacementDirty = false;
156 
157     // Used to visualize the grid and drop locations
158     private boolean mVisualizeCells = false;
159     private boolean mVisualizeDropLocation = true;
160     private RectF mVisualizeGridRect = new RectF();
161     private Paint mVisualizeGridPaint = new Paint();
162     private int mGridVisualizationRoundingRadius;
163     private float mGridAlpha = 0f;
164     private int mGridColor = 0;
165     protected float mSpringLoadedProgress = 0f;
166     private float mScrollProgress = 0f;
167 
168     // When a drag operation is in progress, holds the nearest cell to the touch point
169     private final int[] mDragCell = new int[2];
170     private final int[] mDragCellSpan = new int[2];
171 
172     private boolean mDragging = false;
173 
174     private final TimeInterpolator mEaseOutInterpolator;
175     protected final ShortcutAndWidgetContainer mShortcutsAndWidgets;
176 
177     @Retention(RetentionPolicy.SOURCE)
178     @IntDef({WORKSPACE, HOTSEAT, FOLDER})
179     public @interface ContainerType{}
180     public static final int WORKSPACE = 0;
181     public static final int HOTSEAT = 1;
182     public static final int FOLDER = 2;
183 
184     @ContainerType private final int mContainerType;
185 
186     private final float mChildScale = 1f;
187 
188     public static final int MODE_SHOW_REORDER_HINT = 0;
189     public static final int MODE_DRAG_OVER = 1;
190     public static final int MODE_ON_DROP = 2;
191     public static final int MODE_ON_DROP_EXTERNAL = 3;
192     public static final int MODE_ACCEPT_DROP = 4;
193     private static final boolean DESTRUCTIVE_REORDER = false;
194     private static final boolean DEBUG_VISUALIZE_OCCUPIED = false;
195 
196     private static final float REORDER_PREVIEW_MAGNITUDE = 0.12f;
197     private static final int REORDER_ANIMATION_DURATION = 150;
198     @Thunk final float mReorderPreviewAnimationMagnitude;
199 
200     private final ArrayList<View> mIntersectingViews = new ArrayList<>();
201     private final Rect mOccupiedRect = new Rect();
202     public final int[] mDirectionVector = new int[2];
203 
204     ItemConfiguration mPreviousSolution = null;
205     private static final int INVALID_DIRECTION = -100;
206 
207     private final Rect mTempRect = new Rect();
208     private final RectF mTempRectF = new RectF();
209     private final float[] mTmpFloatArray = new float[4];
210 
211     private static final Paint sPaint = new Paint();
212 
213     // Related to accessible drag and drop
214     DragAndDropAccessibilityDelegate mTouchHelper;
215 
216 
217     public static final FloatProperty<CellLayout> SPRING_LOADED_PROGRESS =
218             new FloatProperty<CellLayout>("spring_loaded_progress") {
219                 @Override
220                 public Float get(CellLayout cl) {
221                     return cl.getSpringLoadedProgress();
222                 }
223 
224                 @Override
225                 public void setValue(CellLayout cl, float progress) {
226                     cl.setSpringLoadedProgress(progress);
227                 }
228             };
229 
CellLayout(Context context)230     public CellLayout(Context context) {
231         this(context, null);
232     }
233 
CellLayout(Context context, AttributeSet attrs)234     public CellLayout(Context context, AttributeSet attrs) {
235         this(context, attrs, 0);
236     }
237 
CellLayout(Context context, AttributeSet attrs, int defStyle)238     public CellLayout(Context context, AttributeSet attrs, int defStyle) {
239         super(context, attrs, defStyle);
240         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0);
241         mContainerType = a.getInteger(R.styleable.CellLayout_containerType, WORKSPACE);
242         a.recycle();
243 
244         // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show
245         // the user where a dragged item will land when dropped.
246         setWillNotDraw(false);
247         setClipToPadding(false);
248         setClipChildren(false);
249         mActivity = ActivityContext.lookupContext(context);
250         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
251 
252         resetCellSizeInternal(deviceProfile);
253 
254         mCountX = deviceProfile.inv.numColumns;
255         mCountY = deviceProfile.inv.numRows;
256         mOccupied =  new GridOccupancy(mCountX, mCountY);
257         mTmpOccupied = new GridOccupancy(mCountX, mCountY);
258 
259         mFolderLeaveBehind.mDelegateCellX = -1;
260         mFolderLeaveBehind.mDelegateCellY = -1;
261 
262         setAlwaysDrawnWithCacheEnabled(false);
263 
264         Resources res = getResources();
265 
266         mBackground = getContext().getDrawable(R.drawable.bg_celllayout);
267         mBackground.setCallback(this);
268         mBackground.setAlpha(0);
269 
270         mGridColor = Themes.getAttrColor(getContext(), R.attr.workspaceAccentColor);
271         mGridVisualizationRoundingRadius =
272                 res.getDimensionPixelSize(R.dimen.grid_visualization_rounding_radius);
273         mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * deviceProfile.iconSizePx);
274 
275         // Initialize the data structures used for the drag visualization.
276         mEaseOutInterpolator = Interpolators.DECELERATE_QUINT; // Quint ease out
277         mDragCell[0] = mDragCell[1] = -1;
278         mDragCellSpan[0] = mDragCellSpan[1] = -1;
279         for (int i = 0; i < mDragOutlines.length; i++) {
280             mDragOutlines[i] = new CellLayoutLayoutParams(0, 0, 0, 0);
281         }
282         mDragOutlinePaint.setColor(Themes.getAttrColor(context, R.attr.workspaceTextColor));
283 
284         // When dragging things around the home screens, we show a green outline of
285         // where the item will land. The outlines gradually fade out, leaving a trail
286         // behind the drag path.
287         // Set up all the animations that are used to implement this fading.
288         final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime);
289         final float fromAlphaValue = 0;
290         final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha);
291 
292         Arrays.fill(mDragOutlineAlphas, fromAlphaValue);
293 
294         for (int i = 0; i < mDragOutlineAnims.length; i++) {
295             final InterruptibleInOutAnimator anim =
296                     new InterruptibleInOutAnimator(duration, fromAlphaValue, toAlphaValue);
297             anim.getAnimator().setInterpolator(mEaseOutInterpolator);
298             final int thisIndex = i;
299             anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() {
300                 public void onAnimationUpdate(ValueAnimator animation) {
301                     // If an animation is started and then stopped very quickly, we can still
302                     // get spurious updates we've cleared the tag. Guard against this.
303                     mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue();
304                     CellLayout.this.invalidate();
305                 }
306             });
307             // The animation holds a reference to the drag outline bitmap as long is it's
308             // running. This way the bitmap can be GCed when the animations are complete.
309             mDragOutlineAnims[i] = anim;
310         }
311 
312         mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context, mContainerType);
313         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
314                 mBorderSpace);
315         addView(mShortcutsAndWidgets);
316     }
317 
318     /**
319      * Sets or clears a delegate used for accessible drag and drop
320      */
setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate)321     public void setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate) {
322         setOnClickListener(delegate);
323         ViewCompat.setAccessibilityDelegate(this, delegate);
324 
325         mTouchHelper = delegate;
326         int accessibilityFlag = mTouchHelper != null
327                 ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO;
328         setImportantForAccessibility(accessibilityFlag);
329         getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
330 
331         // ExploreByTouchHelper sets focusability. Clear it when the delegate is cleared.
332         setFocusable(delegate != null);
333         // Invalidate the accessibility hierarchy
334         if (getParent() != null) {
335             getParent().notifySubtreeAccessibilityStateChanged(
336                     this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
337         }
338     }
339 
340     /**
341      * Returns the currently set accessibility delegate
342      */
getDragAndDropAccessibilityDelegate()343     public DragAndDropAccessibilityDelegate getDragAndDropAccessibilityDelegate() {
344         return mTouchHelper;
345     }
346 
347     @Override
dispatchHoverEvent(MotionEvent event)348     public boolean dispatchHoverEvent(MotionEvent event) {
349         // Always attempt to dispatch hover events to accessibility first.
350         if (mTouchHelper != null && mTouchHelper.dispatchHoverEvent(event)) {
351             return true;
352         }
353         return super.dispatchHoverEvent(event);
354     }
355 
356     @Override
onInterceptTouchEvent(MotionEvent ev)357     public boolean onInterceptTouchEvent(MotionEvent ev) {
358         return mTouchHelper != null
359                 || (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev));
360     }
361 
enableHardwareLayer(boolean hasLayer)362     public void enableHardwareLayer(boolean hasLayer) {
363         mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint);
364     }
365 
isHardwareLayerEnabled()366     public boolean isHardwareLayerEnabled() {
367         return mShortcutsAndWidgets.getLayerType() == LAYER_TYPE_HARDWARE;
368     }
369 
370     /**
371      * Change sizes of cells
372      *
373      * @param width  the new width of the cells
374      * @param height the new height of the cells
375      */
setCellDimensions(int width, int height)376     public void setCellDimensions(int width, int height) {
377         mFixedCellWidth = mCellWidth = width;
378         mFixedCellHeight = mCellHeight = height;
379         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
380                 mBorderSpace);
381     }
382 
resetCellSizeInternal(DeviceProfile deviceProfile)383     private void resetCellSizeInternal(DeviceProfile deviceProfile) {
384         switch (mContainerType) {
385             case FOLDER:
386                 mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx);
387                 break;
388             case HOTSEAT:
389                 mBorderSpace = new Point(deviceProfile.hotseatBorderSpace,
390                         deviceProfile.hotseatBorderSpace);
391                 break;
392             case WORKSPACE:
393             default:
394                 mBorderSpace = new Point(deviceProfile.cellLayoutBorderSpacePx);
395                 break;
396         }
397 
398         mCellWidth = mCellHeight = -1;
399         mFixedCellWidth = mFixedCellHeight = -1;
400     }
401 
402     /**
403      * Reset the cell sizes and border space
404      */
resetCellSize(DeviceProfile deviceProfile)405     public void resetCellSize(DeviceProfile deviceProfile) {
406         resetCellSizeInternal(deviceProfile);
407         requestLayout();
408     }
409 
setGridSize(int x, int y)410     public void setGridSize(int x, int y) {
411         mCountX = x;
412         mCountY = y;
413         mOccupied = new GridOccupancy(mCountX, mCountY);
414         mTmpOccupied = new GridOccupancy(mCountX, mCountY);
415         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
416                 mBorderSpace);
417         requestLayout();
418     }
419 
420     // Set whether or not to invert the layout horizontally if the layout is in RTL mode.
setInvertIfRtl(boolean invert)421     public void setInvertIfRtl(boolean invert) {
422         mShortcutsAndWidgets.setInvertIfRtl(invert);
423     }
424 
setDropPending(boolean pending)425     public void setDropPending(boolean pending) {
426         mDropPending = pending;
427     }
428 
isDropPending()429     public boolean isDropPending() {
430         return mDropPending;
431     }
432 
setIsDragOverlapping(boolean isDragOverlapping)433     void setIsDragOverlapping(boolean isDragOverlapping) {
434         if (mIsDragOverlapping != isDragOverlapping) {
435             mIsDragOverlapping = isDragOverlapping;
436             mBackground.setState(mIsDragOverlapping
437                     ? BACKGROUND_STATE_ACTIVE : BACKGROUND_STATE_DEFAULT);
438             invalidate();
439         }
440     }
441 
442     @Override
dispatchSaveInstanceState(SparseArray<Parcelable> container)443     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
444         ParcelableSparseArray jail = getJailedArray(container);
445         super.dispatchSaveInstanceState(jail);
446         container.put(R.id.cell_layout_jail_id, jail);
447     }
448 
449     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)450     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
451         super.dispatchRestoreInstanceState(getJailedArray(container));
452     }
453 
454     /**
455      * Wrap the SparseArray in another Parcelable so that the item ids do not conflict with our
456      * our internal resource ids
457      */
getJailedArray(SparseArray<Parcelable> container)458     private ParcelableSparseArray getJailedArray(SparseArray<Parcelable> container) {
459         final Parcelable parcelable = container.get(R.id.cell_layout_jail_id);
460         return parcelable instanceof ParcelableSparseArray ?
461                 (ParcelableSparseArray) parcelable : new ParcelableSparseArray();
462     }
463 
getIsDragOverlapping()464     public boolean getIsDragOverlapping() {
465         return mIsDragOverlapping;
466     }
467 
468     @Override
onDraw(Canvas canvas)469     protected void onDraw(Canvas canvas) {
470         // When we're large, we are either drawn in a "hover" state (ie when dragging an item to
471         // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f)
472         // When we're small, we are either drawn normally or in the "accepts drops" state (during
473         // a drag). However, we also drag the mini hover background *over* one of those two
474         // backgrounds
475         if (mBackground.getAlpha() > 0) {
476             mBackground.draw(canvas);
477         }
478 
479         if (DEBUG_VISUALIZE_OCCUPIED) {
480             Rect cellBounds = new Rect();
481             // Will contain the bounds of the cell including spacing between cells.
482             Rect cellBoundsWithSpacing = new Rect();
483             int[] targetCell = new int[2];
484             int[] cellCenter = new int[2];
485             Paint debugPaint = new Paint();
486             debugPaint.setStrokeWidth(Utilities.dpToPx(1));
487             for (int x = 0; x < mCountX; x++) {
488                 for (int y = 0; y < mCountY; y++) {
489                     if (!mOccupied.cells[x][y]) {
490                         continue;
491                     }
492                     targetCell[0] = x;
493                     targetCell[1] = y;
494 
495                     boolean canCreateFolder = canCreateFolder(getChildAt(x, y));
496                     cellToRect(x, y, 1, 1, cellBounds);
497                     cellBoundsWithSpacing.set(cellBounds);
498                     cellBoundsWithSpacing.inset(-mBorderSpace.x / 2, -mBorderSpace.y / 2);
499                     getWorkspaceCellVisualCenter(x, y, cellCenter);
500 
501                     canvas.save();
502                     canvas.clipRect(cellBoundsWithSpacing);
503 
504                     // Draw reorder drag target.
505                     debugPaint.setColor(Color.RED);
506                     canvas.drawCircle(cellCenter[0], cellCenter[1],
507                             getReorderRadius(targetCell, 1, 1), debugPaint);
508 
509                     // Draw folder creation drag target.
510                     if (canCreateFolder) {
511                         debugPaint.setColor(Color.GREEN);
512                         canvas.drawCircle(cellCenter[0], cellCenter[1],
513                                 getFolderCreationRadius(targetCell), debugPaint);
514                     }
515 
516                     canvas.restore();
517                 }
518             }
519         }
520 
521         for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
522             DelegatedCellDrawing cellDrawing = mDelegatedCellDrawings.get(i);
523             cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation);
524             canvas.save();
525             canvas.translate(mTempLocation[0], mTempLocation[1]);
526             cellDrawing.drawUnderItem(canvas);
527             canvas.restore();
528         }
529 
530         if (mFolderLeaveBehind.mDelegateCellX >= 0 && mFolderLeaveBehind.mDelegateCellY >= 0) {
531             cellToPoint(mFolderLeaveBehind.mDelegateCellX,
532                     mFolderLeaveBehind.mDelegateCellY, mTempLocation);
533             canvas.save();
534             canvas.translate(mTempLocation[0], mTempLocation[1]);
535             mFolderLeaveBehind.drawLeaveBehind(canvas, FOLDER_LEAVE_BEHIND_COLOR);
536             canvas.restore();
537         }
538 
539         if (mVisualizeCells || mVisualizeDropLocation) {
540             visualizeGrid(canvas);
541         }
542     }
543 
544     /**
545      * Returns whether dropping an icon on the given View can create (or add to) a folder.
546      */
canCreateFolder(View child)547     private boolean canCreateFolder(View child) {
548         return child instanceof DraggableView
549                 && ((DraggableView) child).getViewType() == DRAGGABLE_ICON;
550     }
551 
552     /**
553      * Indicates the progress of the Workspace entering the SpringLoaded state; allows the
554      * CellLayout to update various visuals for this state.
555      *
556      * @param progress
557      */
setSpringLoadedProgress(float progress)558     public void setSpringLoadedProgress(float progress) {
559         if (Float.compare(progress, mSpringLoadedProgress) != 0) {
560             mSpringLoadedProgress = progress;
561             updateBgAlpha();
562             setGridAlpha(progress);
563         }
564     }
565 
566     /**
567      * See setSpringLoadedProgress
568      * @return progress
569      */
getSpringLoadedProgress()570     public float getSpringLoadedProgress() {
571         return mSpringLoadedProgress;
572     }
573 
updateBgAlpha()574     protected void updateBgAlpha() {
575         if (!getWorkspace().mLauncher.isInState(EDIT_MODE)) {
576             mBackground.setAlpha((int) (mSpringLoadedProgress * 255));
577         }
578     }
579 
580     /**
581      * Set the progress of this page's scroll
582      *
583      * @param progress 0 if the screen is centered, +/-1 if it is to the right / left respectively
584      */
setScrollProgress(float progress)585     public void setScrollProgress(float progress) {
586         if (Float.compare(Math.abs(progress), mScrollProgress) != 0) {
587             mScrollProgress = Math.abs(progress);
588             updateBgAlpha();
589         }
590     }
591 
setGridAlpha(float gridAlpha)592     private void setGridAlpha(float gridAlpha) {
593         if (Float.compare(gridAlpha, mGridAlpha) != 0) {
594             mGridAlpha = gridAlpha;
595             invalidate();
596         }
597     }
598 
visualizeGrid(Canvas canvas)599     protected void visualizeGrid(Canvas canvas) {
600         DeviceProfile dp = mActivity.getDeviceProfile();
601         int paddingX = Math.min((mCellWidth - dp.iconSizePx) / 2, dp.gridVisualizationPaddingX);
602         int paddingY = Math.min((mCellHeight - dp.iconSizePx) / 2, dp.gridVisualizationPaddingY);
603         mVisualizeGridRect.set(paddingX, paddingY,
604                 mCellWidth - paddingX,
605                 mCellHeight - paddingY);
606 
607         mVisualizeGridPaint.setStrokeWidth(8);
608         int paintAlpha = (int) (120 * mGridAlpha);
609         mVisualizeGridPaint.setColor(ColorUtils.setAlphaComponent(mGridColor, paintAlpha));
610 
611         if (mVisualizeCells) {
612             for (int i = 0; i < mCountX; i++) {
613                 for (int j = 0; j < mCountY; j++) {
614                     int transX = i * mCellWidth + (i * mBorderSpace.x) + getPaddingLeft()
615                             + paddingX;
616                     int transY = j * mCellHeight + (j * mBorderSpace.y) + getPaddingTop()
617                             + paddingY;
618 
619                     mVisualizeGridRect.offsetTo(transX, transY);
620                     mVisualizeGridPaint.setStyle(Paint.Style.FILL);
621                     canvas.drawRoundRect(mVisualizeGridRect, mGridVisualizationRoundingRadius,
622                             mGridVisualizationRoundingRadius, mVisualizeGridPaint);
623                 }
624             }
625         }
626 
627         if (mVisualizeDropLocation) {
628             for (int i = 0; i < mDragOutlines.length; i++) {
629                 final float alpha = mDragOutlineAlphas[i];
630                 if (alpha <= 0) continue;
631 
632                 mVisualizeGridPaint.setAlpha(255);
633                 int x = mDragOutlines[i].getCellX();
634                 int y = mDragOutlines[i].getCellY();
635                 int spanX = mDragOutlines[i].cellHSpan;
636                 int spanY = mDragOutlines[i].cellVSpan;
637 
638                 // TODO b/194414754 clean this up, reconcile with cellToRect
639                 mVisualizeGridRect.set(paddingX, paddingY,
640                         mCellWidth * spanX + mBorderSpace.x * (spanX - 1) - paddingX,
641                         mCellHeight * spanY + mBorderSpace.y * (spanY - 1) - paddingY);
642 
643                 int transX = x * mCellWidth + (x * mBorderSpace.x)
644                         + getPaddingLeft() + paddingX;
645                 int transY = y * mCellHeight + (y * mBorderSpace.y)
646                         + getPaddingTop() + paddingY;
647 
648                 mVisualizeGridRect.offsetTo(transX, transY);
649 
650                 mVisualizeGridPaint.setStyle(Paint.Style.STROKE);
651                 mVisualizeGridPaint.setColor(Color.argb((int) (alpha),
652                         Color.red(mGridColor), Color.green(mGridColor), Color.blue(mGridColor)));
653 
654                 canvas.drawRoundRect(mVisualizeGridRect, mGridVisualizationRoundingRadius,
655                         mGridVisualizationRoundingRadius, mVisualizeGridPaint);
656             }
657         }
658     }
659 
660     @Override
dispatchDraw(Canvas canvas)661     protected void dispatchDraw(Canvas canvas) {
662         super.dispatchDraw(canvas);
663 
664         for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
665             DelegatedCellDrawing bg = mDelegatedCellDrawings.get(i);
666             cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation);
667             canvas.save();
668             canvas.translate(mTempLocation[0], mTempLocation[1]);
669             bg.drawOverItem(canvas);
670             canvas.restore();
671         }
672     }
673 
674     /**
675      * Add Delegated cell drawing
676      */
addDelegatedCellDrawing(DelegatedCellDrawing bg)677     public void addDelegatedCellDrawing(DelegatedCellDrawing bg) {
678         mDelegatedCellDrawings.add(bg);
679     }
680 
681     /**
682      * Remove item from DelegatedCellDrawings
683      */
removeDelegatedCellDrawing(DelegatedCellDrawing bg)684     public void removeDelegatedCellDrawing(DelegatedCellDrawing bg) {
685         mDelegatedCellDrawings.remove(bg);
686     }
687 
setFolderLeaveBehindCell(int x, int y)688     public void setFolderLeaveBehindCell(int x, int y) {
689         View child = getChildAt(x, y);
690         mFolderLeaveBehind.setup(getContext(), mActivity, null,
691                 child.getMeasuredWidth(), child.getPaddingTop());
692 
693         mFolderLeaveBehind.mDelegateCellX = x;
694         mFolderLeaveBehind.mDelegateCellY = y;
695         invalidate();
696     }
697 
clearFolderLeaveBehind()698     public void clearFolderLeaveBehind() {
699         mFolderLeaveBehind.mDelegateCellX = -1;
700         mFolderLeaveBehind.mDelegateCellY = -1;
701         invalidate();
702     }
703 
704     @Override
shouldDelayChildPressedState()705     public boolean shouldDelayChildPressedState() {
706         return false;
707     }
708 
restoreInstanceState(SparseArray<Parcelable> states)709     public void restoreInstanceState(SparseArray<Parcelable> states) {
710         try {
711             dispatchRestoreInstanceState(states);
712         } catch (IllegalArgumentException ex) {
713             if (FeatureFlags.IS_STUDIO_BUILD) {
714                 throw ex;
715             }
716             // Mismatched viewId / viewType preventing restore. Skip restore on production builds.
717             Log.e(TAG, "Ignoring an error while restoring a view instance state", ex);
718         }
719     }
720 
721     @Override
cancelLongPress()722     public void cancelLongPress() {
723         super.cancelLongPress();
724 
725         // Cancel long press for all children
726         final int count = getChildCount();
727         for (int i = 0; i < count; i++) {
728             final View child = getChildAt(i);
729             child.cancelLongPress();
730         }
731     }
732 
setOnInterceptTouchListener(View.OnTouchListener listener)733     public void setOnInterceptTouchListener(View.OnTouchListener listener) {
734         mInterceptTouchListener = listener;
735     }
736 
getCountX()737     public int getCountX() {
738         return mCountX;
739     }
740 
getCountY()741     public int getCountY() {
742         return mCountY;
743     }
744 
acceptsWidget()745     public boolean acceptsWidget() {
746         return mContainerType == WORKSPACE;
747     }
748 
749     /**
750      * Adds the given view to the CellLayout
751      *
752      * @param child view to add.
753      * @param index index of the CellLayout children where to add the view.
754      * @param childId id of the view.
755      * @param params represent the logic of the view on the CellLayout.
756      * @param markCells if the occupied cells should be marked or not
757      * @return if adding the view was successful
758      */
addViewToCellLayout(View child, int index, int childId, CellLayoutLayoutParams params, boolean markCells)759     public boolean addViewToCellLayout(View child, int index, int childId,
760             CellLayoutLayoutParams params, boolean markCells) {
761         final CellLayoutLayoutParams lp = params;
762 
763         // Hotseat icons - remove text
764         if (child instanceof BubbleTextView) {
765             BubbleTextView bubbleChild = (BubbleTextView) child;
766             bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
767         }
768 
769         child.setScaleX(mChildScale);
770         child.setScaleY(mChildScale);
771 
772         // Generate an id for each view, this assumes we have at most 256x256 cells
773         // per workspace screen
774         if (lp.getCellX() >= 0 && lp.getCellX() <= mCountX - 1
775                 && lp.getCellY() >= 0 && lp.getCellY() <= mCountY - 1) {
776             // If the horizontal or vertical span is set to -1, it is taken to
777             // mean that it spans the extent of the CellLayout
778             if (lp.cellHSpan < 0) lp.cellHSpan = mCountX;
779             if (lp.cellVSpan < 0) lp.cellVSpan = mCountY;
780 
781             child.setId(childId);
782             if (LOGD) {
783                 Log.d(TAG, "Adding view to ShortcutsAndWidgetsContainer: " + child);
784             }
785             mShortcutsAndWidgets.addView(child, index, lp);
786 
787             if (markCells) markCellsAsOccupiedForView(child);
788 
789             return true;
790         }
791         return false;
792     }
793 
794     @Override
removeAllViews()795     public void removeAllViews() {
796         mOccupied.clear();
797         mShortcutsAndWidgets.removeAllViews();
798     }
799 
800     @Override
removeAllViewsInLayout()801     public void removeAllViewsInLayout() {
802         if (mShortcutsAndWidgets.getChildCount() > 0) {
803             mOccupied.clear();
804             mShortcutsAndWidgets.removeAllViewsInLayout();
805         }
806     }
807 
808     @Override
removeView(View view)809     public void removeView(View view) {
810         markCellsAsUnoccupiedForView(view);
811         mShortcutsAndWidgets.removeView(view);
812     }
813 
814     @Override
removeViewAt(int index)815     public void removeViewAt(int index) {
816         markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index));
817         mShortcutsAndWidgets.removeViewAt(index);
818     }
819 
820     @Override
removeViewInLayout(View view)821     public void removeViewInLayout(View view) {
822         markCellsAsUnoccupiedForView(view);
823         mShortcutsAndWidgets.removeViewInLayout(view);
824     }
825 
826     @Override
removeViews(int start, int count)827     public void removeViews(int start, int count) {
828         for (int i = start; i < start + count; i++) {
829             markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i));
830         }
831         mShortcutsAndWidgets.removeViews(start, count);
832     }
833 
834     @Override
removeViewsInLayout(int start, int count)835     public void removeViewsInLayout(int start, int count) {
836         for (int i = start; i < start + count; i++) {
837             markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i));
838         }
839         mShortcutsAndWidgets.removeViewsInLayout(start, count);
840     }
841 
842     /**
843      * Given a point, return the cell that strictly encloses that point
844      * @param x X coordinate of the point
845      * @param y Y coordinate of the point
846      * @param result Array of 2 ints to hold the x and y coordinate of the cell
847      */
pointToCellExact(int x, int y, int[] result)848     public void pointToCellExact(int x, int y, int[] result) {
849         final int hStartPadding = getPaddingLeft();
850         final int vStartPadding = getPaddingTop();
851 
852         result[0] = (x - hStartPadding) / (mCellWidth + mBorderSpace.x);
853         result[1] = (y - vStartPadding) / (mCellHeight + mBorderSpace.y);
854 
855         final int xAxis = mCountX;
856         final int yAxis = mCountY;
857 
858         if (result[0] < 0) result[0] = 0;
859         if (result[0] >= xAxis) result[0] = xAxis - 1;
860         if (result[1] < 0) result[1] = 0;
861         if (result[1] >= yAxis) result[1] = yAxis - 1;
862     }
863 
864     /**
865      * Given a cell coordinate, return the point that represents the upper left corner of that cell
866      *
867      * @param cellX X coordinate of the cell
868      * @param cellY Y coordinate of the cell
869      *
870      * @param result Array of 2 ints to hold the x and y coordinate of the point
871      */
cellToPoint(int cellX, int cellY, int[] result)872     void cellToPoint(int cellX, int cellY, int[] result) {
873         cellToRect(cellX, cellY, 1, 1, mTempRect);
874         result[0] = mTempRect.left;
875         result[1] = mTempRect.top;
876     }
877 
878     /**
879      * Given a cell coordinate, return the point that represents the center of the cell
880      *
881      * @param cellX X coordinate of the cell
882      * @param cellY Y coordinate of the cell
883      *
884      * @param result Array of 2 ints to hold the x and y coordinate of the point
885      */
cellToCenterPoint(int cellX, int cellY, int[] result)886     void cellToCenterPoint(int cellX, int cellY, int[] result) {
887         regionToCenterPoint(cellX, cellY, 1, 1, result);
888     }
889 
890     /**
891      * Given a cell coordinate and span return the point that represents the center of the region
892      *
893      * @param cellX X coordinate of the cell
894      * @param cellY Y coordinate of the cell
895      *
896      * @param result Array of 2 ints to hold the x and y coordinate of the point
897      */
regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result)898     public void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
899         cellToRect(cellX, cellY, spanX, spanY, mTempRect);
900         result[0] = mTempRect.centerX();
901         result[1] = mTempRect.centerY();
902     }
903 
904     /**
905      * Returns the distance between the given coordinate and the visual center of the given cell.
906      */
getDistanceFromWorkspaceCellVisualCenter(float x, float y, int[] cell)907     public float getDistanceFromWorkspaceCellVisualCenter(float x, float y, int[] cell) {
908         getWorkspaceCellVisualCenter(cell[0], cell[1], mTmpPoint);
909         return (float) Math.hypot(x - mTmpPoint[0], y - mTmpPoint[1]);
910     }
911 
getWorkspaceCellVisualCenter(int cellX, int cellY, int[] outPoint)912     private void getWorkspaceCellVisualCenter(int cellX, int cellY, int[] outPoint) {
913         View child = getChildAt(cellX, cellY);
914         if (child instanceof DraggableView) {
915             DraggableView draggableChild = (DraggableView) child;
916             if (draggableChild.getViewType() == DRAGGABLE_ICON) {
917                 cellToPoint(cellX, cellY, outPoint);
918                 draggableChild.getWorkspaceVisualDragBounds(mTempRect);
919                 mTempRect.offset(outPoint[0], outPoint[1]);
920                 outPoint[0] = mTempRect.centerX();
921                 outPoint[1] = mTempRect.centerY();
922                 return;
923             }
924         }
925         cellToCenterPoint(cellX, cellY, outPoint);
926     }
927 
928     /**
929      * Returns the max distance from the center of a cell that can accept a drop to create a folder.
930      */
getFolderCreationRadius(int[] targetCell)931     public float getFolderCreationRadius(int[] targetCell) {
932         DeviceProfile grid = mActivity.getDeviceProfile();
933         float iconVisibleRadius = ICON_VISIBLE_AREA_FACTOR * grid.iconSizePx / 2;
934         // Halfway between reorder radius and icon.
935         return (getReorderRadius(targetCell, 1, 1) + iconVisibleRadius) / 2;
936     }
937 
938     /**
939      * Returns the max distance from the center of a cell that will start to reorder on drag over.
940      */
getReorderRadius(int[] targetCell, int spanX, int spanY)941     public float getReorderRadius(int[] targetCell, int spanX, int spanY) {
942         int[] centerPoint = mTmpPoint;
943         getWorkspaceCellVisualCenter(targetCell[0], targetCell[1], centerPoint);
944 
945         Rect cellBoundsWithSpacing = mTempRect;
946         cellToRect(targetCell[0], targetCell[1], spanX, spanY, cellBoundsWithSpacing);
947         cellBoundsWithSpacing.inset(-mBorderSpace.x / 2, -mBorderSpace.y / 2);
948 
949         if (canCreateFolder(getChildAt(targetCell[0], targetCell[1])) && spanX == 1 && spanY == 1) {
950             // Take only the circle in the smaller dimension, to ensure we don't start reordering
951             // too soon before accepting a folder drop.
952             int minRadius = centerPoint[0] - cellBoundsWithSpacing.left;
953             minRadius = Math.min(minRadius, centerPoint[1] - cellBoundsWithSpacing.top);
954             minRadius = Math.min(minRadius, cellBoundsWithSpacing.right - centerPoint[0]);
955             minRadius = Math.min(minRadius, cellBoundsWithSpacing.bottom - centerPoint[1]);
956             return minRadius;
957         }
958         // Take up the entire cell, including space between this cell and the adjacent ones.
959         // Multiply by span to scale radius
960         return (float) Math.hypot(spanX * cellBoundsWithSpacing.width() / 2f,
961                 spanY * cellBoundsWithSpacing.height() / 2f);
962     }
963 
getCellWidth()964     public int getCellWidth() {
965         return mCellWidth;
966     }
967 
getCellHeight()968     public int getCellHeight() {
969         return mCellHeight;
970     }
971 
setFixedSize(int width, int height)972     public void setFixedSize(int width, int height) {
973         mFixedWidth = width;
974         mFixedHeight = height;
975     }
976 
977     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)978     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
979         int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
980         int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
981         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
982         int heightSize =  MeasureSpec.getSize(heightMeasureSpec);
983         int childWidthSize = widthSize - (getPaddingLeft() + getPaddingRight());
984         int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom());
985 
986         if (mFixedCellWidth < 0 || mFixedCellHeight < 0) {
987             int cw = DeviceProfile.calculateCellWidth(childWidthSize, mBorderSpace.x,
988                     mCountX);
989             int ch = DeviceProfile.calculateCellHeight(childHeightSize, mBorderSpace.y,
990                     mCountY);
991             if (cw != mCellWidth || ch != mCellHeight) {
992                 mCellWidth = cw;
993                 mCellHeight = ch;
994                 mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
995                         mBorderSpace);
996             }
997         }
998 
999         int newWidth = childWidthSize;
1000         int newHeight = childHeightSize;
1001         if (mFixedWidth > 0 && mFixedHeight > 0) {
1002             newWidth = mFixedWidth;
1003             newHeight = mFixedHeight;
1004         } else if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
1005             throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions");
1006         }
1007 
1008         mShortcutsAndWidgets.measure(
1009                 MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY),
1010                 MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY));
1011 
1012         int maxWidth = mShortcutsAndWidgets.getMeasuredWidth();
1013         int maxHeight = mShortcutsAndWidgets.getMeasuredHeight();
1014         if (mFixedWidth > 0 && mFixedHeight > 0) {
1015             setMeasuredDimension(maxWidth, maxHeight);
1016         } else {
1017             setMeasuredDimension(widthSize, heightSize);
1018         }
1019     }
1020 
1021     @Override
onLayout(boolean changed, int l, int t, int r, int b)1022     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1023         int left = getPaddingLeft();
1024         left += (int) Math.ceil(getUnusedHorizontalSpace() / 2f);
1025         int right = r - l - getPaddingRight();
1026         right -= (int) Math.ceil(getUnusedHorizontalSpace() / 2f);
1027 
1028         int top = getPaddingTop();
1029         int bottom = b - t - getPaddingBottom();
1030 
1031         // Expand the background drawing bounds by the padding baked into the background drawable
1032         mBackground.getPadding(mTempRect);
1033         mBackground.setBounds(
1034                 left - mTempRect.left - getPaddingLeft(),
1035                 top - mTempRect.top - getPaddingTop(),
1036                 right + mTempRect.right + getPaddingRight(),
1037                 bottom + mTempRect.bottom + getPaddingBottom());
1038 
1039         mShortcutsAndWidgets.layout(left, top, right, bottom);
1040     }
1041 
1042     /**
1043      * Returns the amount of space left over after subtracting padding and cells. This space will be
1044      * very small, a few pixels at most, and is a result of rounding down when calculating the cell
1045      * width in {@link DeviceProfile#calculateCellWidth(int, int, int)}.
1046      */
getUnusedHorizontalSpace()1047     public int getUnusedHorizontalSpace() {
1048         return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth)
1049                 - ((mCountX - 1) * mBorderSpace.x);
1050     }
1051 
1052     @Override
verifyDrawable(Drawable who)1053     protected boolean verifyDrawable(Drawable who) {
1054         return super.verifyDrawable(who) || (who == mBackground);
1055     }
1056 
getShortcutsAndWidgets()1057     public ShortcutAndWidgetContainer getShortcutsAndWidgets() {
1058         return mShortcutsAndWidgets;
1059     }
1060 
getChildAt(int cellX, int cellY)1061     public View getChildAt(int cellX, int cellY) {
1062         return mShortcutsAndWidgets.getChildAt(cellX, cellY);
1063     }
1064 
animateChildToPosition(final View child, int cellX, int cellY, int duration, int delay, boolean permanent, boolean adjustOccupied)1065     public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
1066             int delay, boolean permanent, boolean adjustOccupied) {
1067         ShortcutAndWidgetContainer clc = getShortcutsAndWidgets();
1068 
1069         if (clc.indexOfChild(child) != -1 && (child instanceof Reorderable)) {
1070             final CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1071             final ItemInfo info = (ItemInfo) child.getTag();
1072             final Reorderable item = (Reorderable) child;
1073 
1074             // We cancel any existing animations
1075             if (mReorderAnimators.containsKey(lp)) {
1076                 mReorderAnimators.get(lp).cancel();
1077                 mReorderAnimators.remove(lp);
1078             }
1079 
1080 
1081             if (adjustOccupied) {
1082                 GridOccupancy occupied = permanent ? mOccupied : mTmpOccupied;
1083                 occupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, false);
1084                 occupied.markCells(cellX, cellY, lp.cellHSpan, lp.cellVSpan, true);
1085             }
1086 
1087             // Compute the new x and y position based on the new cellX and cellY
1088             // We leverage the actual layout logic in the layout params and hence need to modify
1089             // state and revert that state.
1090             final int oldX = lp.x;
1091             final int oldY = lp.y;
1092             lp.isLockedToGrid = true;
1093             if (permanent) {
1094                 lp.setCellX(cellX);
1095                 lp.setCellY(cellY);
1096             } else {
1097                 lp.setTmpCellX(cellX);
1098                 lp.setTmpCellY(cellY);
1099             }
1100             clc.setupLp(child);
1101             final int newX = lp.x;
1102             final int newY = lp.y;
1103             lp.x = oldX;
1104             lp.y = oldY;
1105             lp.isLockedToGrid = false;
1106             // End compute new x and y
1107 
1108             MultiTranslateDelegate mtd = item.getTranslateDelegate();
1109             float initPreviewOffsetX = mtd.getTranslationX(INDEX_REORDER_PREVIEW_OFFSET).getValue();
1110             float initPreviewOffsetY = mtd.getTranslationY(INDEX_REORDER_PREVIEW_OFFSET).getValue();
1111             final float finalPreviewOffsetX = newX - oldX;
1112             final float finalPreviewOffsetY = newY - oldY;
1113 
1114             // Exit early if we're not actually moving the view
1115             if (finalPreviewOffsetX == 0 && finalPreviewOffsetY == 0
1116                     && initPreviewOffsetX == 0 && initPreviewOffsetY == 0) {
1117                 lp.isLockedToGrid = true;
1118                 return true;
1119             }
1120 
1121             ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
1122             va.setDuration(duration);
1123             mReorderAnimators.put(lp, va);
1124 
1125             va.addUpdateListener(new AnimatorUpdateListener() {
1126                 @Override
1127                 public void onAnimationUpdate(ValueAnimator animation) {
1128                     float r = (Float) animation.getAnimatedValue();
1129                     float x = (1 - r) * initPreviewOffsetX + r * finalPreviewOffsetX;
1130                     float y = (1 - r) * initPreviewOffsetY + r * finalPreviewOffsetY;
1131                     item.getTranslateDelegate().setTranslation(INDEX_REORDER_PREVIEW_OFFSET, x, y);
1132                 }
1133             });
1134             va.addListener(new AnimatorListenerAdapter() {
1135                 boolean cancelled = false;
1136                 public void onAnimationEnd(Animator animation) {
1137                     // If the animation was cancelled, it means that another animation
1138                     // has interrupted this one, and we don't want to lock the item into
1139                     // place just yet.
1140                     if (!cancelled) {
1141                         lp.isLockedToGrid = true;
1142                         item.getTranslateDelegate()
1143                                 .setTranslation(INDEX_REORDER_PREVIEW_OFFSET, 0, 0);
1144                         child.requestLayout();
1145                     }
1146                     if (mReorderAnimators.containsKey(lp)) {
1147                         mReorderAnimators.remove(lp);
1148                     }
1149                 }
1150                 public void onAnimationCancel(Animator animation) {
1151                     cancelled = true;
1152                 }
1153             });
1154             va.setStartDelay(delay);
1155             va.start();
1156             return true;
1157         }
1158         return false;
1159     }
1160 
visualizeDropLocation(int cellX, int cellY, int spanX, int spanY, DropTarget.DragObject dragObject)1161     void visualizeDropLocation(int cellX, int cellY, int spanX, int spanY,
1162             DropTarget.DragObject dragObject) {
1163         if (mDragCell[0] != cellX || mDragCell[1] != cellY || mDragCellSpan[0] != spanX
1164                 || mDragCellSpan[1] != spanY) {
1165             mDragCell[0] = cellX;
1166             mDragCell[1] = cellY;
1167             mDragCellSpan[0] = spanX;
1168             mDragCellSpan[1] = spanY;
1169 
1170             // Apply color extraction on a widget when dragging.
1171             applyColorExtractionOnWidget(dragObject, mDragCell, spanX, spanY);
1172 
1173             final int oldIndex = mDragOutlineCurrent;
1174             mDragOutlineAnims[oldIndex].animateOut();
1175             mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
1176 
1177             CellLayoutLayoutParams cell = mDragOutlines[mDragOutlineCurrent];
1178             cell.setCellX(cellX);
1179             cell.setCellY(cellY);
1180             cell.cellHSpan = spanX;
1181             cell.cellVSpan = spanY;
1182 
1183             mDragOutlineAnims[mDragOutlineCurrent].animateIn();
1184             invalidate();
1185 
1186             if (dragObject.stateAnnouncer != null) {
1187                 dragObject.stateAnnouncer.announce(getItemMoveDescription(cellX, cellY));
1188             }
1189 
1190         }
1191     }
1192 
1193     /** Applies the local color extraction to a dragging widget object. */
applyColorExtractionOnWidget(DropTarget.DragObject dragObject, int[] targetCell, int spanX, int spanY)1194     private void applyColorExtractionOnWidget(DropTarget.DragObject dragObject, int[] targetCell,
1195             int spanX, int spanY) {
1196         // Apply local extracted color if the DragView is an AppWidgetHostViewDrawable.
1197         View view = dragObject.dragView.getContentView();
1198         if (view instanceof LauncherAppWidgetHostView) {
1199             int screenId = getWorkspace().getIdForScreen(this);
1200             cellToRect(targetCell[0], targetCell[1], spanX, spanY, mTempRect);
1201 
1202             ((LauncherAppWidgetHostView) view).handleDrag(mTempRect, this, screenId);
1203         }
1204     }
1205 
1206     @SuppressLint("StringFormatMatches")
getItemMoveDescription(int cellX, int cellY)1207     public String getItemMoveDescription(int cellX, int cellY) {
1208         if (mContainerType == HOTSEAT) {
1209             return getContext().getString(R.string.move_to_hotseat_position,
1210                     Math.max(cellX, cellY) + 1);
1211         } else {
1212             Workspace<?> workspace = getWorkspace();
1213             int row = cellY + 1;
1214             int col = workspace.mIsRtl ? mCountX - cellX : cellX + 1;
1215             int panelCount = workspace.getPanelCount();
1216             int screenId = workspace.getIdForScreen(this);
1217             int pageIndex = workspace.getPageIndexForScreenId(screenId);
1218             if (panelCount > 1) {
1219                 // Increment the column if the target is on the right side of a two panel home
1220                 col += (pageIndex % panelCount) * mCountX;
1221             }
1222             return getContext().getString(R.string.move_to_empty_cell_description, row, col,
1223                     workspace.getPageDescription(pageIndex));
1224         }
1225     }
1226 
getWorkspace()1227     private Workspace<?> getWorkspace() {
1228         return Launcher.cast(mActivity).getWorkspace();
1229     }
1230 
clearDragOutlines()1231     public void clearDragOutlines() {
1232         final int oldIndex = mDragOutlineCurrent;
1233         mDragOutlineAnims[oldIndex].animateOut();
1234         mDragCell[0] = mDragCell[1] = -1;
1235     }
1236 
1237     /**
1238      * Find a vacant area that will fit the given bounds nearest the requested
1239      * cell location. Uses Euclidean distance to score multiple vacant areas.
1240      *
1241      * @param pixelX The X location at which you want to search for a vacant area.
1242      * @param pixelY The Y location at which you want to search for a vacant area.
1243      * @param minSpanX The minimum horizontal span required
1244      * @param minSpanY The minimum vertical span required
1245      * @param spanX Horizontal span of the object.
1246      * @param spanY Vertical span of the object.
1247      * @param result Array in which to place the result, or null (in which case a new array will
1248      *        be allocated)
1249      * @return The X, Y cell of a vacant area that can contain this object,
1250      *         nearest the requested location.
1251      */
findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] result, int[] resultSpan)1252     public int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY,
1253             int spanX, int spanY, int[] result, int[] resultSpan) {
1254         return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, false,
1255                 result, resultSpan);
1256     }
1257 
1258     /**
1259      * Find a vacant area that will fit the given bounds nearest the requested
1260      * cell location. Uses Euclidean distance to score multiple vacant areas.
1261      * @param relativeXPos The X location relative to the Cell layout at which you want to search
1262      *                     for a vacant area.
1263      * @param relativeYPos The Y location relative to the Cell layout at which you want to search
1264      *                     for a vacant area.
1265      * @param minSpanX The minimum horizontal span required
1266      * @param minSpanY The minimum vertical span required
1267      * @param spanX Horizontal span of the object.
1268      * @param spanY Vertical span of the object.
1269      * @param ignoreOccupied If true, the result can be an occupied cell
1270      * @param result Array in which to place the result, or null (in which case a new array will
1271      *        be allocated)
1272      * @return The X, Y cell of a vacant area that can contain this object,
1273      *         nearest the requested location.
1274      */
findNearestArea(int relativeXPos, int relativeYPos, int minSpanX, int minSpanY, int spanX, int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan)1275     protected int[] findNearestArea(int relativeXPos, int relativeYPos, int minSpanX, int minSpanY,
1276             int spanX, int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan) {
1277         // For items with a spanX / spanY > 1, the passed in point (relativeXPos, relativeYPos)
1278         // corresponds to the center of the item, but we are searching based on the top-left cell,
1279         // so we translate the point over to correspond to the top-left.
1280         relativeXPos = (int) (relativeXPos - (mCellWidth + mBorderSpace.x) * (spanX - 1) / 2f);
1281         relativeYPos = (int) (relativeYPos - (mCellHeight + mBorderSpace.y) * (spanY - 1) / 2f);
1282 
1283         // Keep track of best-scoring drop area
1284         final int[] bestXY = result != null ? result : new int[2];
1285         double bestDistance = Double.MAX_VALUE;
1286         final Rect bestRect = new Rect(-1, -1, -1, -1);
1287         final Stack<Rect> validRegions = new Stack<>();
1288 
1289         final int countX = mCountX;
1290         final int countY = mCountY;
1291 
1292         if (minSpanX <= 0 || minSpanY <= 0 || spanX <= 0 || spanY <= 0 ||
1293                 spanX < minSpanX || spanY < minSpanY) {
1294             return bestXY;
1295         }
1296 
1297         for (int y = 0; y < countY - (minSpanY - 1); y++) {
1298             inner:
1299             for (int x = 0; x < countX - (minSpanX - 1); x++) {
1300                 int ySize = -1;
1301                 int xSize = -1;
1302                 if (!ignoreOccupied) {
1303                     // First, let's see if this thing fits anywhere
1304                     for (int i = 0; i < minSpanX; i++) {
1305                         for (int j = 0; j < minSpanY; j++) {
1306                             if (mOccupied.cells[x + i][y + j]) {
1307                                 continue inner;
1308                             }
1309                         }
1310                     }
1311                     xSize = minSpanX;
1312                     ySize = minSpanY;
1313 
1314                     // We know that the item will fit at _some_ acceptable size, now let's see
1315                     // how big we can make it. We'll alternate between incrementing x and y spans
1316                     // until we hit a limit.
1317                     boolean incX = true;
1318                     boolean hitMaxX = xSize >= spanX;
1319                     boolean hitMaxY = ySize >= spanY;
1320                     while (!(hitMaxX && hitMaxY)) {
1321                         if (incX && !hitMaxX) {
1322                             for (int j = 0; j < ySize; j++) {
1323                                 if (x + xSize > countX -1 || mOccupied.cells[x + xSize][y + j]) {
1324                                     // We can't move out horizontally
1325                                     hitMaxX = true;
1326                                 }
1327                             }
1328                             if (!hitMaxX) {
1329                                 xSize++;
1330                             }
1331                         } else if (!hitMaxY) {
1332                             for (int i = 0; i < xSize; i++) {
1333                                 if (y + ySize > countY - 1 || mOccupied.cells[x + i][y + ySize]) {
1334                                     // We can't move out vertically
1335                                     hitMaxY = true;
1336                                 }
1337                             }
1338                             if (!hitMaxY) {
1339                                 ySize++;
1340                             }
1341                         }
1342                         hitMaxX |= xSize >= spanX;
1343                         hitMaxY |= ySize >= spanY;
1344                         incX = !incX;
1345                     }
1346                 }
1347                 final int[] cellXY = mTmpPoint;
1348                 cellToCenterPoint(x, y, cellXY);
1349 
1350                 // We verify that the current rect is not a sub-rect of any of our previous
1351                 // candidates. In this case, the current rect is disqualified in favour of the
1352                 // containing rect.
1353                 Rect currentRect = new Rect(x, y, x + xSize, y + ySize);
1354                 boolean contained = false;
1355                 for (Rect r : validRegions) {
1356                     if (r.contains(currentRect)) {
1357                         contained = true;
1358                         break;
1359                     }
1360                 }
1361                 validRegions.push(currentRect);
1362                 double distance = Math.hypot(cellXY[0] - relativeXPos,  cellXY[1] - relativeYPos);
1363 
1364                 if ((distance <= bestDistance && !contained) ||
1365                         currentRect.contains(bestRect)) {
1366                     bestDistance = distance;
1367                     bestXY[0] = x;
1368                     bestXY[1] = y;
1369                     if (resultSpan != null) {
1370                         resultSpan[0] = xSize;
1371                         resultSpan[1] = ySize;
1372                     }
1373                     bestRect.set(currentRect);
1374                 }
1375             }
1376         }
1377 
1378         // Return -1, -1 if no suitable location found
1379         if (bestDistance == Double.MAX_VALUE) {
1380             bestXY[0] = -1;
1381             bestXY[1] = -1;
1382         }
1383         return bestXY;
1384     }
1385 
getOccupied()1386     public GridOccupancy getOccupied() {
1387         return mOccupied;
1388     }
1389 
copySolutionToTempState(ItemConfiguration solution, View dragView)1390     private void copySolutionToTempState(ItemConfiguration solution, View dragView) {
1391         mTmpOccupied.clear();
1392 
1393         int childCount = mShortcutsAndWidgets.getChildCount();
1394         for (int i = 0; i < childCount; i++) {
1395             View child = mShortcutsAndWidgets.getChildAt(i);
1396             if (child == dragView) continue;
1397             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1398             CellAndSpan c = solution.map.get(child);
1399             if (c != null) {
1400                 lp.setTmpCellX(c.cellX);
1401                 lp.setTmpCellY(c.cellY);
1402                 lp.cellHSpan = c.spanX;
1403                 lp.cellVSpan = c.spanY;
1404                 mTmpOccupied.markCells(c, true);
1405             }
1406         }
1407         mTmpOccupied.markCells(solution, true);
1408     }
1409 
animateItemsToSolution(ItemConfiguration solution, View dragView, boolean commitDragView)1410     private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean
1411             commitDragView) {
1412 
1413         GridOccupancy occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied;
1414         occupied.clear();
1415 
1416         int childCount = mShortcutsAndWidgets.getChildCount();
1417         for (int i = 0; i < childCount; i++) {
1418             View child = mShortcutsAndWidgets.getChildAt(i);
1419             if (child == dragView) continue;
1420             CellAndSpan c = solution.map.get(child);
1421             if (c != null) {
1422                 animateChildToPosition(child, c.cellX, c.cellY, REORDER_ANIMATION_DURATION, 0,
1423                         DESTRUCTIVE_REORDER, false);
1424                 occupied.markCells(c, true);
1425             }
1426         }
1427         if (commitDragView) {
1428             occupied.markCells(solution, true);
1429         }
1430     }
1431 
1432 
1433     // This method starts or changes the reorder preview animations
beginOrAdjustReorderPreviewAnimations(ItemConfiguration solution, View dragView, int mode)1434     private void beginOrAdjustReorderPreviewAnimations(ItemConfiguration solution,
1435             View dragView, int mode) {
1436         int childCount = mShortcutsAndWidgets.getChildCount();
1437         for (int i = 0; i < childCount; i++) {
1438             View child = mShortcutsAndWidgets.getChildAt(i);
1439             if (child == dragView) continue;
1440             CellAndSpan c = solution.map.get(child);
1441             boolean skip = mode == ReorderPreviewAnimation.MODE_HINT && solution.intersectingViews
1442                     != null && !solution.intersectingViews.contains(child);
1443 
1444 
1445             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1446             if (c != null && !skip && (child instanceof Reorderable)) {
1447                 ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child,
1448                         mode, lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY);
1449                 rha.animate();
1450             }
1451         }
1452     }
1453 
1454     private static final Property<ReorderPreviewAnimation, Float> ANIMATION_PROGRESS =
1455             new Property<ReorderPreviewAnimation, Float>(float.class, "animationProgress") {
1456                 @Override
1457                 public Float get(ReorderPreviewAnimation anim) {
1458                     return anim.animationProgress;
1459                 }
1460 
1461                 @Override
1462                 public void set(ReorderPreviewAnimation anim, Float progress) {
1463                     anim.setAnimationProgress(progress);
1464                 }
1465             };
1466 
1467     // Class which represents the reorder preview animations. These animations show that an item is
1468     // in a temporary state, and hint at where the item will return to.
1469     class ReorderPreviewAnimation<T extends View & Reorderable> {
1470         final T child;
1471         float finalDeltaX;
1472         float finalDeltaY;
1473         float initDeltaX;
1474         float initDeltaY;
1475         final float finalScale;
1476         float initScale;
1477         final int mode;
1478         boolean repeating = false;
1479         private static final int PREVIEW_DURATION = 300;
1480         private static final int HINT_DURATION = Workspace.REORDER_TIMEOUT;
1481 
1482         private static final float CHILD_DIVIDEND = 4.0f;
1483 
1484         public static final int MODE_HINT = 0;
1485         public static final int MODE_PREVIEW = 1;
1486 
1487         float animationProgress = 0;
1488         ValueAnimator a;
1489 
ReorderPreviewAnimation(View childView, int mode, int cellX0, int cellY0, int cellX1, int cellY1, int spanX, int spanY)1490         ReorderPreviewAnimation(View childView, int mode, int cellX0, int cellY0,
1491                 int cellX1, int cellY1, int spanX, int spanY) {
1492             regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint);
1493             final int x0 = mTmpPoint[0];
1494             final int y0 = mTmpPoint[1];
1495             regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint);
1496             final int x1 = mTmpPoint[0];
1497             final int y1 = mTmpPoint[1];
1498             final int dX = x1 - x0;
1499             final int dY = y1 - y0;
1500 
1501             this.child = (T) childView;
1502             this.mode = mode;
1503             finalDeltaX = 0;
1504             finalDeltaY = 0;
1505 
1506             MultiTranslateDelegate mtd = child.getTranslateDelegate();
1507             initDeltaX = mtd.getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).getValue();
1508             initDeltaY = mtd.getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).getValue();
1509             initScale = child.getReorderBounceScale();
1510             finalScale = mChildScale - (CHILD_DIVIDEND / child.getWidth()) * initScale;
1511 
1512             int dir = mode == MODE_HINT ? -1 : 1;
1513             if (dX == dY && dX == 0) {
1514             } else {
1515                 if (dY == 0) {
1516                     finalDeltaX = -dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
1517                 } else if (dX == 0) {
1518                     finalDeltaY = -dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
1519                 } else {
1520                     double angle = Math.atan( (float) (dY) / dX);
1521                     finalDeltaX = (int) (-dir * Math.signum(dX)
1522                             * Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
1523                     finalDeltaY = (int) (-dir * Math.signum(dY)
1524                             * Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
1525                 }
1526             }
1527         }
1528 
setInitialAnimationValuesToBaseline()1529         void setInitialAnimationValuesToBaseline() {
1530             initScale = mChildScale;
1531             initDeltaX = 0;
1532             initDeltaY = 0;
1533         }
1534 
animate()1535         void animate() {
1536             boolean noMovement = (finalDeltaX == 0) && (finalDeltaY == 0);
1537 
1538             if (mShakeAnimators.containsKey(child)) {
1539                 ReorderPreviewAnimation oldAnimation = mShakeAnimators.get(child);
1540                 mShakeAnimators.remove(child);
1541 
1542                 if (noMovement) {
1543                     // A previous animation for this item exists, and no new animation will exist.
1544                     // Finish the old animation smoothly.
1545                     oldAnimation.finishAnimation();
1546                     return;
1547                 } else {
1548                     // A previous animation for this item exists, and a new one will exist. Stop
1549                     // the old animation in its tracks, and proceed with the new one.
1550                     oldAnimation.cancel();
1551                 }
1552             }
1553             if (noMovement) {
1554                 return;
1555             }
1556 
1557             ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS, 0, 1);
1558             a = va;
1559 
1560             // Animations are disabled in power save mode, causing the repeated animation to jump
1561             // spastically between beginning and end states. Since this looks bad, we don't repeat
1562             // the animation in power save mode.
1563             if (areAnimatorsEnabled()) {
1564                 va.setRepeatMode(ValueAnimator.REVERSE);
1565                 va.setRepeatCount(ValueAnimator.INFINITE);
1566             }
1567 
1568             va.setDuration(mode == MODE_HINT ? HINT_DURATION : PREVIEW_DURATION);
1569             va.setStartDelay((int) (Math.random() * 60));
1570             va.addListener(new AnimatorListenerAdapter() {
1571                 public void onAnimationRepeat(Animator animation) {
1572                     // We make sure to end only after a full period
1573                     setInitialAnimationValuesToBaseline();
1574                     repeating = true;
1575                 }
1576             });
1577             mShakeAnimators.put(child, this);
1578             va.start();
1579         }
1580 
setAnimationProgress(float progress)1581         private void setAnimationProgress(float progress) {
1582             animationProgress = progress;
1583             float r1 = (mode == MODE_HINT && repeating) ? 1.0f : animationProgress;
1584             float x = r1 * finalDeltaX + (1 - r1) * initDeltaX;
1585             float y = r1 * finalDeltaY + (1 - r1) * initDeltaY;
1586             child.getTranslateDelegate().setTranslation(INDEX_REORDER_BOUNCE_OFFSET, x, y);
1587             float s = animationProgress * finalScale + (1 - animationProgress) * initScale;
1588             child.setReorderBounceScale(s);
1589         }
1590 
cancel()1591         private void cancel() {
1592             if (a != null) {
1593                 a.cancel();
1594             }
1595         }
1596 
1597         /**
1598          * Smoothly returns the item to its baseline position / scale
1599          */
finishAnimation()1600         @Thunk void finishAnimation() {
1601             if (a != null) {
1602                 a.cancel();
1603             }
1604 
1605             setInitialAnimationValuesToBaseline();
1606             ValueAnimator va = ObjectAnimator.ofFloat(this, ANIMATION_PROGRESS,
1607                     animationProgress, 0);
1608             a = va;
1609             a.setInterpolator(DECELERATE_1_5);
1610             a.setDuration(REORDER_ANIMATION_DURATION);
1611             a.start();
1612         }
1613     }
1614 
completeAndClearReorderPreviewAnimations()1615     private void completeAndClearReorderPreviewAnimations() {
1616         for (ReorderPreviewAnimation a: mShakeAnimators.values()) {
1617             a.finishAnimation();
1618         }
1619         mShakeAnimators.clear();
1620     }
1621 
commitTempPlacement(View dragView)1622     private void commitTempPlacement(View dragView) {
1623         mTmpOccupied.copyTo(mOccupied);
1624 
1625         int screenId = getWorkspace().getIdForScreen(this);
1626         int container = Favorites.CONTAINER_DESKTOP;
1627 
1628         if (mContainerType == HOTSEAT) {
1629             screenId = -1;
1630             container = Favorites.CONTAINER_HOTSEAT;
1631         }
1632 
1633         int childCount = mShortcutsAndWidgets.getChildCount();
1634         for (int i = 0; i < childCount; i++) {
1635             View child = mShortcutsAndWidgets.getChildAt(i);
1636             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
1637             ItemInfo info = (ItemInfo) child.getTag();
1638             // We do a null check here because the item info can be null in the case of the
1639             // AllApps button in the hotseat.
1640             if (info != null && child != dragView) {
1641                 CellPos presenterPos = mActivity.getCellPosMapper().mapModelToPresenter(info);
1642                 final boolean requiresDbUpdate = (presenterPos.cellX != lp.getTmpCellX()
1643                         || presenterPos.cellY != lp.getTmpCellY() || info.spanX != lp.cellHSpan
1644                         || info.spanY != lp.cellVSpan || presenterPos.screenId != screenId);
1645 
1646                 lp.setCellX(lp.getTmpCellX());
1647                 lp.setCellY(lp.getTmpCellY());
1648                 if (requiresDbUpdate) {
1649                     Launcher.cast(mActivity).getModelWriter().modifyItemInDatabase(info, container,
1650                             screenId, lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan);
1651                 }
1652             }
1653         }
1654     }
1655 
setUseTempCoords(boolean useTempCoords)1656     private void setUseTempCoords(boolean useTempCoords) {
1657         int childCount = mShortcutsAndWidgets.getChildCount();
1658         for (int i = 0; i < childCount; i++) {
1659             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mShortcutsAndWidgets.getChildAt(
1660                     i).getLayoutParams();
1661             lp.useTmpCoords = useTempCoords;
1662         }
1663     }
1664 
1665     // For a given cell and span, fetch the set of views intersecting the region.
getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY, View dragView, Rect boundingRect, ArrayList<View> intersectingViews)1666     public void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY,
1667             View dragView, Rect boundingRect, ArrayList<View> intersectingViews) {
1668         if (boundingRect != null) {
1669             boundingRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
1670         }
1671         intersectingViews.clear();
1672         Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
1673         Rect r1 = new Rect();
1674         final int count = mShortcutsAndWidgets.getChildCount();
1675         for (int i = 0; i < count; i++) {
1676             View child = mShortcutsAndWidgets.getChildAt(i);
1677             if (child == dragView) continue;
1678             CellLayoutLayoutParams
1679                     lp = (CellLayoutLayoutParams) child.getLayoutParams();
1680             r1.set(lp.getCellX(), lp.getCellY(), lp.getCellX() + lp.cellHSpan,
1681                     lp.getCellY() + lp.cellVSpan);
1682             if (Rect.intersects(r0, r1)) {
1683                 mIntersectingViews.add(child);
1684                 if (boundingRect != null) {
1685                     boundingRect.union(r1);
1686                 }
1687             }
1688         }
1689     }
1690 
isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY, View dragView, int[] result)1691     public boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY,
1692             View dragView, int[] result) {
1693         result = findNearestAreaIgnoreOccupied(pixelX, pixelY, spanX, spanY, result);
1694         getViewsIntersectingRegion(result[0], result[1], spanX, spanY, dragView, null,
1695                 mIntersectingViews);
1696         return !mIntersectingViews.isEmpty();
1697     }
1698 
revertTempState()1699     void revertTempState() {
1700         completeAndClearReorderPreviewAnimations();
1701         if (isItemPlacementDirty() && !DESTRUCTIVE_REORDER) {
1702             final int count = mShortcutsAndWidgets.getChildCount();
1703             for (int i = 0; i < count; i++) {
1704                 View child = mShortcutsAndWidgets.getChildAt(i);
1705                 CellLayoutLayoutParams
1706                         lp = (CellLayoutLayoutParams) child.getLayoutParams();
1707                 if (lp.getTmpCellX() != lp.getCellX() || lp.getTmpCellY() != lp.getCellY()) {
1708                     lp.setTmpCellX(lp.getCellX());
1709                     lp.setTmpCellY(lp.getCellY());
1710                     animateChildToPosition(child, lp.getCellX(), lp.getCellY(),
1711                             REORDER_ANIMATION_DURATION, 0, false, false);
1712                 }
1713             }
1714             setItemPlacementDirty(false);
1715         }
1716     }
1717 
createAreaForResize(int cellX, int cellY, int spanX, int spanY, View dragView, int[] direction, boolean commit)1718     boolean createAreaForResize(int cellX, int cellY, int spanX, int spanY,
1719             View dragView, int[] direction, boolean commit) {
1720         int[] pixelXY = new int[2];
1721         regionToCenterPoint(cellX, cellY, spanX, spanY, pixelXY);
1722 
1723         // First we determine if things have moved enough to cause a different layout
1724         ItemConfiguration swapSolution = findReorderSolution(pixelXY[0], pixelXY[1], spanX, spanY,
1725                 spanX,  spanY, direction, dragView,  true,  new ItemConfiguration());
1726 
1727         setUseTempCoords(true);
1728         if (swapSolution != null && swapSolution.isSolution) {
1729             // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
1730             // committing anything or animating anything as we just want to determine if a solution
1731             // exists
1732             copySolutionToTempState(swapSolution, dragView);
1733             setItemPlacementDirty(true);
1734             animateItemsToSolution(swapSolution, dragView, commit);
1735 
1736             if (commit) {
1737                 commitTempPlacement(null);
1738                 completeAndClearReorderPreviewAnimations();
1739                 setItemPlacementDirty(false);
1740             } else {
1741                 beginOrAdjustReorderPreviewAnimations(swapSolution, dragView,
1742                         ReorderPreviewAnimation.MODE_PREVIEW);
1743             }
1744             mShortcutsAndWidgets.requestLayout();
1745         }
1746         return swapSolution.isSolution;
1747     }
1748 
1749     /**
1750      * Find a vacant area that will fit the given bounds nearest the requested
1751      * cell location, and will also weigh in a suggested direction vector of the
1752      * desired location. This method computers distance based on unit grid distances,
1753      * not pixel distances.
1754      *
1755      * @param cellX The X cell nearest to which you want to search for a vacant area.
1756      * @param cellY The Y cell nearest which you want to search for a vacant area.
1757      * @param spanX Horizontal span of the object.
1758      * @param spanY Vertical span of the object.
1759      * @param direction The favored direction in which the views should move from x, y
1760      * @param occupied The array which represents which cells in the CellLayout are occupied
1761      * @param blockOccupied The array which represents which cells in the specified block (cellX,
1762      *        cellY, spanX, spanY) are occupied. This is used when try to move a group of views.
1763      * @param result Array in which to place the result, or null (in which case a new array will
1764      *        be allocated)
1765      * @return The X, Y cell of a vacant area that can contain this object,
1766      *         nearest the requested location.
1767      */
findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction, boolean[][] occupied, boolean blockOccupied[][], int[] result)1768     private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction,
1769             boolean[][] occupied, boolean blockOccupied[][], int[] result) {
1770         // Keep track of best-scoring drop area
1771         final int[] bestXY = result != null ? result : new int[2];
1772         float bestDistance = Float.MAX_VALUE;
1773         int bestDirectionScore = Integer.MIN_VALUE;
1774 
1775         final int countX = mCountX;
1776         final int countY = mCountY;
1777 
1778         for (int y = 0; y < countY - (spanY - 1); y++) {
1779             inner:
1780             for (int x = 0; x < countX - (spanX - 1); x++) {
1781                 // First, let's see if this thing fits anywhere
1782                 for (int i = 0; i < spanX; i++) {
1783                     for (int j = 0; j < spanY; j++) {
1784                         if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) {
1785                             continue inner;
1786                         }
1787                     }
1788                 }
1789 
1790                 float distance = (float) Math.hypot(x - cellX, y - cellY);
1791                 int[] curDirection = mTmpPoint;
1792                 computeDirectionVector(x - cellX, y - cellY, curDirection);
1793                 // The direction score is just the dot product of the two candidate direction
1794                 // and that passed in.
1795                 int curDirectionScore = direction[0] * curDirection[0] +
1796                         direction[1] * curDirection[1];
1797                 if (Float.compare(distance,  bestDistance) < 0 ||
1798                         (Float.compare(distance, bestDistance) == 0
1799                                 && curDirectionScore > bestDirectionScore)) {
1800                     bestDistance = distance;
1801                     bestDirectionScore = curDirectionScore;
1802                     bestXY[0] = x;
1803                     bestXY[1] = y;
1804                 }
1805             }
1806         }
1807 
1808         // Return -1, -1 if no suitable location found
1809         if (bestDistance == Float.MAX_VALUE) {
1810             bestXY[0] = -1;
1811             bestXY[1] = -1;
1812         }
1813         return bestXY;
1814     }
1815 
addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop, int[] direction, ItemConfiguration currentState)1816     private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop,
1817             int[] direction, ItemConfiguration currentState) {
1818         CellAndSpan c = currentState.map.get(v);
1819         boolean success = false;
1820         mTmpOccupied.markCells(c, false);
1821         mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
1822 
1823         findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction,
1824                 mTmpOccupied.cells, null, mTempLocation);
1825 
1826         if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
1827             c.cellX = mTempLocation[0];
1828             c.cellY = mTempLocation[1];
1829             success = true;
1830         }
1831         mTmpOccupied.markCells(c, true);
1832         return success;
1833     }
1834 
pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop, int[] direction, View dragView, ItemConfiguration currentState)1835     private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
1836             int[] direction, View dragView, ItemConfiguration currentState) {
1837 
1838         ViewCluster cluster = new ViewCluster(views, currentState);
1839         Rect clusterRect = cluster.getBoundingRect();
1840         int whichEdge;
1841         int pushDistance;
1842         boolean fail = false;
1843 
1844         // Determine the edge of the cluster that will be leading the push and how far
1845         // the cluster must be shifted.
1846         if (direction[0] < 0) {
1847             whichEdge = ViewCluster.LEFT;
1848             pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left;
1849         } else if (direction[0] > 0) {
1850             whichEdge = ViewCluster.RIGHT;
1851             pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left;
1852         } else if (direction[1] < 0) {
1853             whichEdge = ViewCluster.TOP;
1854             pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top;
1855         } else {
1856             whichEdge = ViewCluster.BOTTOM;
1857             pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top;
1858         }
1859 
1860         // Break early for invalid push distance.
1861         if (pushDistance <= 0) {
1862             return false;
1863         }
1864 
1865         // Mark the occupied state as false for the group of views we want to move.
1866         for (View v: views) {
1867             CellAndSpan c = currentState.map.get(v);
1868             mTmpOccupied.markCells(c, false);
1869         }
1870 
1871         // We save the current configuration -- if we fail to find a solution we will revert
1872         // to the initial state. The process of finding a solution modifies the configuration
1873         // in place, hence the need for revert in the failure case.
1874         currentState.save();
1875 
1876         // The pushing algorithm is simplified by considering the views in the order in which
1877         // they would be pushed by the cluster. For example, if the cluster is leading with its
1878         // left edge, we consider sort the views by their right edge, from right to left.
1879         cluster.sortConfigurationForEdgePush(whichEdge);
1880 
1881         while (pushDistance > 0 && !fail) {
1882             for (View v: currentState.sortedViews) {
1883                 // For each view that isn't in the cluster, we see if the leading edge of the
1884                 // cluster is contacting the edge of that view. If so, we add that view to the
1885                 // cluster.
1886                 if (!cluster.views.contains(v) && v != dragView) {
1887                     if (cluster.isViewTouchingEdge(v, whichEdge)) {
1888                         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) v.getLayoutParams();
1889                         if (!lp.canReorder) {
1890                             // The push solution includes the all apps button, this is not viable.
1891                             fail = true;
1892                             break;
1893                         }
1894                         cluster.addView(v);
1895                         CellAndSpan c = currentState.map.get(v);
1896 
1897                         // Adding view to cluster, mark it as not occupied.
1898                         mTmpOccupied.markCells(c, false);
1899                     }
1900                 }
1901             }
1902             pushDistance--;
1903 
1904             // The cluster has been completed, now we move the whole thing over in the appropriate
1905             // direction.
1906             cluster.shift(whichEdge, 1);
1907         }
1908 
1909         boolean foundSolution = false;
1910         clusterRect = cluster.getBoundingRect();
1911 
1912         // Due to the nature of the algorithm, the only check required to verify a valid solution
1913         // is to ensure that completed shifted cluster lies completely within the cell layout.
1914         if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 &&
1915                 clusterRect.bottom <= mCountY) {
1916             foundSolution = true;
1917         } else {
1918             currentState.restore();
1919         }
1920 
1921         // In either case, we set the occupied array as marked for the location of the views
1922         for (View v: cluster.views) {
1923             CellAndSpan c = currentState.map.get(v);
1924             mTmpOccupied.markCells(c, true);
1925         }
1926 
1927         return foundSolution;
1928     }
1929 
1930     /**
1931      * This helper class defines a cluster of views. It helps with defining complex edges
1932      * of the cluster and determining how those edges interact with other views. The edges
1933      * essentially define a fine-grained boundary around the cluster of views -- like a more
1934      * precise version of a bounding box.
1935      */
1936     private class ViewCluster {
1937         final static int LEFT = 1 << 0;
1938         final static int TOP = 1 << 1;
1939         final static int RIGHT = 1 << 2;
1940         final static int BOTTOM = 1 << 3;
1941 
1942         final ArrayList<View> views;
1943         final ItemConfiguration config;
1944         final Rect boundingRect = new Rect();
1945 
1946         final int[] leftEdge = new int[mCountY];
1947         final int[] rightEdge = new int[mCountY];
1948         final int[] topEdge = new int[mCountX];
1949         final int[] bottomEdge = new int[mCountX];
1950         int dirtyEdges;
1951         boolean boundingRectDirty;
1952 
1953         @SuppressWarnings("unchecked")
ViewCluster(ArrayList<View> views, ItemConfiguration config)1954         public ViewCluster(ArrayList<View> views, ItemConfiguration config) {
1955             this.views = (ArrayList<View>) views.clone();
1956             this.config = config;
1957             resetEdges();
1958         }
1959 
resetEdges()1960         void resetEdges() {
1961             for (int i = 0; i < mCountX; i++) {
1962                 topEdge[i] = -1;
1963                 bottomEdge[i] = -1;
1964             }
1965             for (int i = 0; i < mCountY; i++) {
1966                 leftEdge[i] = -1;
1967                 rightEdge[i] = -1;
1968             }
1969             dirtyEdges = LEFT | TOP | RIGHT | BOTTOM;
1970             boundingRectDirty = true;
1971         }
1972 
computeEdge(int which)1973         void computeEdge(int which) {
1974             int count = views.size();
1975             for (int i = 0; i < count; i++) {
1976                 CellAndSpan cs = config.map.get(views.get(i));
1977                 switch (which) {
1978                     case LEFT:
1979                         int left = cs.cellX;
1980                         for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
1981                             if (left < leftEdge[j] || leftEdge[j] < 0) {
1982                                 leftEdge[j] = left;
1983                             }
1984                         }
1985                         break;
1986                     case RIGHT:
1987                         int right = cs.cellX + cs.spanX;
1988                         for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
1989                             if (right > rightEdge[j]) {
1990                                 rightEdge[j] = right;
1991                             }
1992                         }
1993                         break;
1994                     case TOP:
1995                         int top = cs.cellY;
1996                         for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
1997                             if (top < topEdge[j] || topEdge[j] < 0) {
1998                                 topEdge[j] = top;
1999                             }
2000                         }
2001                         break;
2002                     case BOTTOM:
2003                         int bottom = cs.cellY + cs.spanY;
2004                         for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
2005                             if (bottom > bottomEdge[j]) {
2006                                 bottomEdge[j] = bottom;
2007                             }
2008                         }
2009                         break;
2010                 }
2011             }
2012         }
2013 
isViewTouchingEdge(View v, int whichEdge)2014         boolean isViewTouchingEdge(View v, int whichEdge) {
2015             CellAndSpan cs = config.map.get(v);
2016 
2017             if ((dirtyEdges & whichEdge) == whichEdge) {
2018                 computeEdge(whichEdge);
2019                 dirtyEdges &= ~whichEdge;
2020             }
2021 
2022             switch (whichEdge) {
2023                 case LEFT:
2024                     for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
2025                         if (leftEdge[i] == cs.cellX + cs.spanX) {
2026                             return true;
2027                         }
2028                     }
2029                     break;
2030                 case RIGHT:
2031                     for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
2032                         if (rightEdge[i] == cs.cellX) {
2033                             return true;
2034                         }
2035                     }
2036                     break;
2037                 case TOP:
2038                     for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
2039                         if (topEdge[i] == cs.cellY + cs.spanY) {
2040                             return true;
2041                         }
2042                     }
2043                     break;
2044                 case BOTTOM:
2045                     for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
2046                         if (bottomEdge[i] == cs.cellY) {
2047                             return true;
2048                         }
2049                     }
2050                     break;
2051             }
2052             return false;
2053         }
2054 
shift(int whichEdge, int delta)2055         void shift(int whichEdge, int delta) {
2056             for (View v: views) {
2057                 CellAndSpan c = config.map.get(v);
2058                 switch (whichEdge) {
2059                     case LEFT:
2060                         c.cellX -= delta;
2061                         break;
2062                     case RIGHT:
2063                         c.cellX += delta;
2064                         break;
2065                     case TOP:
2066                         c.cellY -= delta;
2067                         break;
2068                     case BOTTOM:
2069                     default:
2070                         c.cellY += delta;
2071                         break;
2072                 }
2073             }
2074             resetEdges();
2075         }
2076 
addView(View v)2077         public void addView(View v) {
2078             views.add(v);
2079             resetEdges();
2080         }
2081 
getBoundingRect()2082         public Rect getBoundingRect() {
2083             if (boundingRectDirty) {
2084                 config.getBoundingRectForViews(views, boundingRect);
2085             }
2086             return boundingRect;
2087         }
2088 
2089         final PositionComparator comparator = new PositionComparator();
2090         class PositionComparator implements Comparator<View> {
2091             int whichEdge = 0;
compare(View left, View right)2092             public int compare(View left, View right) {
2093                 CellAndSpan l = config.map.get(left);
2094                 CellAndSpan r = config.map.get(right);
2095                 switch (whichEdge) {
2096                     case LEFT:
2097                         return (r.cellX + r.spanX) - (l.cellX + l.spanX);
2098                     case RIGHT:
2099                         return l.cellX - r.cellX;
2100                     case TOP:
2101                         return (r.cellY + r.spanY) - (l.cellY + l.spanY);
2102                     case BOTTOM:
2103                     default:
2104                         return l.cellY - r.cellY;
2105                 }
2106             }
2107         }
2108 
sortConfigurationForEdgePush(int edge)2109         public void sortConfigurationForEdgePush(int edge) {
2110             comparator.whichEdge = edge;
2111             Collections.sort(config.sortedViews, comparator);
2112         }
2113     }
2114 
2115     // This method tries to find a reordering solution which satisfies the push mechanic by trying
2116     // to push items in each of the cardinal directions, in an order based on the direction vector
2117     // passed.
attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied, int[] direction, View ignoreView, ItemConfiguration solution)2118     private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied,
2119             int[] direction, View ignoreView, ItemConfiguration solution) {
2120         if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
2121             // If the direction vector has two non-zero components, we try pushing
2122             // separately in each of the components.
2123             int temp = direction[1];
2124             direction[1] = 0;
2125 
2126             if (pushViewsToTempLocation(intersectingViews, occupied, direction,
2127                     ignoreView, solution)) {
2128                 return true;
2129             }
2130             direction[1] = temp;
2131             temp = direction[0];
2132             direction[0] = 0;
2133 
2134             if (pushViewsToTempLocation(intersectingViews, occupied, direction,
2135                     ignoreView, solution)) {
2136                 return true;
2137             }
2138             // Revert the direction
2139             direction[0] = temp;
2140 
2141             // Now we try pushing in each component of the opposite direction
2142             direction[0] *= -1;
2143             direction[1] *= -1;
2144             temp = direction[1];
2145             direction[1] = 0;
2146             if (pushViewsToTempLocation(intersectingViews, occupied, direction,
2147                     ignoreView, solution)) {
2148                 return true;
2149             }
2150 
2151             direction[1] = temp;
2152             temp = direction[0];
2153             direction[0] = 0;
2154             if (pushViewsToTempLocation(intersectingViews, occupied, direction,
2155                     ignoreView, solution)) {
2156                 return true;
2157             }
2158             // revert the direction
2159             direction[0] = temp;
2160             direction[0] *= -1;
2161             direction[1] *= -1;
2162 
2163         } else {
2164             // If the direction vector has a single non-zero component, we push first in the
2165             // direction of the vector
2166             if (pushViewsToTempLocation(intersectingViews, occupied, direction,
2167                     ignoreView, solution)) {
2168                 return true;
2169             }
2170             // Then we try the opposite direction
2171             direction[0] *= -1;
2172             direction[1] *= -1;
2173             if (pushViewsToTempLocation(intersectingViews, occupied, direction,
2174                     ignoreView, solution)) {
2175                 return true;
2176             }
2177             // Switch the direction back
2178             direction[0] *= -1;
2179             direction[1] *= -1;
2180 
2181             // If we have failed to find a push solution with the above, then we try
2182             // to find a solution by pushing along the perpendicular axis.
2183 
2184             // Swap the components
2185             int temp = direction[1];
2186             direction[1] = direction[0];
2187             direction[0] = temp;
2188             if (pushViewsToTempLocation(intersectingViews, occupied, direction,
2189                     ignoreView, solution)) {
2190                 return true;
2191             }
2192 
2193             // Then we try the opposite direction
2194             direction[0] *= -1;
2195             direction[1] *= -1;
2196             if (pushViewsToTempLocation(intersectingViews, occupied, direction,
2197                     ignoreView, solution)) {
2198                 return true;
2199             }
2200             // Switch the direction back
2201             direction[0] *= -1;
2202             direction[1] *= -1;
2203 
2204             // Swap the components back
2205             temp = direction[1];
2206             direction[1] = direction[0];
2207             direction[0] = temp;
2208         }
2209         return false;
2210     }
2211 
2212     /*
2213      * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between
2214      * the provided point and the provided cell
2215      */
computeDirectionVector(float deltaX, float deltaY, int[] result)2216     private void computeDirectionVector(float deltaX, float deltaY, int[] result) {
2217         double angle = Math.atan(deltaY / deltaX);
2218 
2219         result[0] = 0;
2220         result[1] = 0;
2221         if (Math.abs(Math.cos(angle)) > 0.5f) {
2222             result[0] = (int) Math.signum(deltaX);
2223         }
2224         if (Math.abs(Math.sin(angle)) > 0.5f) {
2225             result[1] = (int) Math.signum(deltaY);
2226         }
2227     }
2228 
2229     /* This seems like it should be obvious and straight-forward, but when the direction vector
2230     needs to match with the notion of the dragView pushing other views, we have to employ
2231     a slightly more subtle notion of the direction vector. The question is what two points is
2232     the vector between? The center of the dragView and its desired destination? Not quite, as
2233     this doesn't necessarily coincide with the interaction of the dragView and items occupying
2234     those cells. Instead we use some heuristics to often lock the vector to up, down, left
2235     or right, which helps make pushing feel right.
2236     */
getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX, int spanY, View dragView, int[] resultDirection)2237     public void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
2238             int spanY, View dragView, int[] resultDirection) {
2239 
2240         //TODO(adamcohen) b/151776141 use the items visual center for the direction vector
2241         int[] targetDestination = new int[2];
2242 
2243         findNearestAreaIgnoreOccupied(dragViewCenterX, dragViewCenterY, spanX, spanY,
2244                 targetDestination);
2245         Rect dragRect = new Rect();
2246         cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
2247         dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
2248 
2249         Rect dropRegionRect = new Rect();
2250         getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY,
2251                 dragView, dropRegionRect, mIntersectingViews);
2252 
2253         int dropRegionSpanX = dropRegionRect.width();
2254         int dropRegionSpanY = dropRegionRect.height();
2255 
2256         cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
2257                 dropRegionRect.height(), dropRegionRect);
2258 
2259         int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
2260         int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY;
2261 
2262         if (dropRegionSpanX == mCountX || spanX == mCountX) {
2263             deltaX = 0;
2264         }
2265         if (dropRegionSpanY == mCountY || spanY == mCountY) {
2266             deltaY = 0;
2267         }
2268 
2269         if (deltaX == 0 && deltaY == 0) {
2270             // No idea what to do, give a random direction.
2271             resultDirection[0] = 1;
2272             resultDirection[1] = 0;
2273         } else {
2274             computeDirectionVector(deltaX, deltaY, resultDirection);
2275         }
2276     }
2277 
addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop, int[] direction, View dragView, ItemConfiguration currentState)2278     private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
2279             int[] direction, View dragView, ItemConfiguration currentState) {
2280         if (views.size() == 0) return true;
2281 
2282         boolean success = false;
2283         Rect boundingRect = new Rect();
2284         // We construct a rect which represents the entire group of views passed in
2285         currentState.getBoundingRectForViews(views, boundingRect);
2286 
2287         // Mark the occupied state as false for the group of views we want to move.
2288         for (View v: views) {
2289             CellAndSpan c = currentState.map.get(v);
2290             mTmpOccupied.markCells(c, false);
2291         }
2292 
2293         GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(), boundingRect.height());
2294         int top = boundingRect.top;
2295         int left = boundingRect.left;
2296         // We mark more precisely which parts of the bounding rect are truly occupied, allowing
2297         // for interlocking.
2298         for (View v: views) {
2299             CellAndSpan c = currentState.map.get(v);
2300             blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true);
2301         }
2302 
2303         mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
2304 
2305         findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(),
2306                 boundingRect.height(), direction,
2307                 mTmpOccupied.cells, blockOccupied.cells, mTempLocation);
2308 
2309         // If we successfully found a location by pushing the block of views, we commit it
2310         if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
2311             int deltaX = mTempLocation[0] - boundingRect.left;
2312             int deltaY = mTempLocation[1] - boundingRect.top;
2313             for (View v: views) {
2314                 CellAndSpan c = currentState.map.get(v);
2315                 c.cellX += deltaX;
2316                 c.cellY += deltaY;
2317             }
2318             success = true;
2319         }
2320 
2321         // In either case, we set the occupied array as marked for the location of the views
2322         for (View v: views) {
2323             CellAndSpan c = currentState.map.get(v);
2324             mTmpOccupied.markCells(c, true);
2325         }
2326         return success;
2327     }
2328 
rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction, View ignoreView, ItemConfiguration solution)2329     public boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
2330             View ignoreView, ItemConfiguration solution) {
2331         // Return early if get invalid cell positions
2332         if (cellX < 0 || cellY < 0) return false;
2333 
2334         mIntersectingViews.clear();
2335         mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
2336 
2337         // Mark the desired location of the view currently being dragged.
2338         if (ignoreView != null) {
2339             CellAndSpan c = solution.map.get(ignoreView);
2340             if (c != null) {
2341                 c.cellX = cellX;
2342                 c.cellY = cellY;
2343             }
2344         }
2345         Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
2346         Rect r1 = new Rect();
2347         // The views need to be sorted so that the results are deterministic on the views positions
2348         // and not by the views hash which is "random".
2349         // The views are sorted twice, once for the X position and a second time for the Y position
2350         // to ensure same order everytime.
2351         Comparator comparator = Comparator.comparing(view ->
2352                         ((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellX())
2353                 .thenComparing(view ->
2354                         ((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellY());
2355         List<View> views = solution.map.keySet().stream().sorted(comparator).toList();
2356         for (View child : views) {
2357             if (child == ignoreView) continue;
2358             CellAndSpan c = solution.map.get(child);
2359             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
2360             r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
2361             if (Rect.intersects(r0, r1)) {
2362                 if (!lp.canReorder) {
2363                     return false;
2364                 }
2365                 mIntersectingViews.add(child);
2366             }
2367         }
2368 
2369         solution.intersectingViews = new ArrayList<>(mIntersectingViews);
2370 
2371         // First we try to find a solution which respects the push mechanic. That is,
2372         // we try to find a solution such that no displaced item travels through another item
2373         // without also displacing that item.
2374         if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView,
2375                 solution)) {
2376             return true;
2377         }
2378 
2379         // Next we try moving the views as a block, but without requiring the push mechanic.
2380         if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView,
2381                 solution)) {
2382             return true;
2383         }
2384 
2385         // Ok, they couldn't move as a block, let's move them individually
2386         for (View v : mIntersectingViews) {
2387             if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) {
2388                 return false;
2389             }
2390         }
2391         return true;
2392     }
2393 
createReorderAlgorithm()2394     public ReorderAlgorithm createReorderAlgorithm() {
2395         return new ReorderAlgorithm(this);
2396     }
2397 
findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution)2398     protected ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX,
2399             int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX,
2400             ItemConfiguration solution) {
2401         return createReorderAlgorithm().findReorderSolution(pixelX, pixelY, minSpanX, minSpanY,
2402                 spanX, spanY, direction, dragView, decX, solution);
2403     }
2404 
copyCurrentStateToSolution(ItemConfiguration solution, boolean temp)2405     public void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
2406         int childCount = mShortcutsAndWidgets.getChildCount();
2407         for (int i = 0; i < childCount; i++) {
2408             View child = mShortcutsAndWidgets.getChildAt(i);
2409             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
2410             CellAndSpan c;
2411             if (temp) {
2412                 c = new CellAndSpan(lp.getTmpCellX(), lp.getTmpCellY(), lp.cellHSpan, lp.cellVSpan);
2413             } else {
2414                 c = new CellAndSpan(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan);
2415             }
2416             solution.add(child, c);
2417         }
2418     }
2419 
2420     /**
2421      * When the user drags an Item in the workspace sometimes we need to move the items already in
2422      * the workspace to make space for the new item, this function return a solution for that
2423      * reorder.
2424      *
2425      * @param pixelX X coordinate in the screen of the dragView in pixels
2426      * @param pixelY Y coordinate in the screen of the dragView in pixels
2427      * @param minSpanX minimum horizontal span the item can be shrunk to
2428      * @param minSpanY minimum vertical span the item can be shrunk to
2429      * @param spanX occupied horizontal span
2430      * @param spanY occupied vertical span
2431      * @param dragView the view of the item being draged
2432      * @return returns a solution for the given parameters, the solution contains all the icons and
2433      *         the locations they should be in the given solution.
2434      */
calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, View dragView)2435     public ItemConfiguration calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY,
2436             int spanX, int spanY, View dragView) {
2437         return createReorderAlgorithm().calculateReorder(pixelX, pixelY, minSpanX, minSpanY,
2438                 spanX, spanY, dragView);
2439     }
2440 
performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, View dragView, int[] result, int[] resultSpan, int mode)2441     int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
2442             View dragView, int[] result, int[] resultSpan, int mode) {
2443         if (resultSpan == null) {
2444             resultSpan = new int[]{-1, -1};
2445         }
2446         if (result == null) {
2447             result = new int[]{-1, -1};
2448         }
2449 
2450         ItemConfiguration finalSolution = null;
2451         // We want the solution to match the animation of the preview and to match the drop so we
2452         // only recalculate in mode MODE_SHOW_REORDER_HINT because that the first one to run in the
2453         // reorder cycle.
2454         if (mode == MODE_SHOW_REORDER_HINT || mPreviousSolution == null) {
2455             finalSolution = calculateReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
2456                     dragView);
2457             mPreviousSolution = finalSolution;
2458         } else {
2459             finalSolution = mPreviousSolution;
2460             // We reset this vector after drop
2461             if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
2462                 mPreviousSolution = null;
2463             }
2464         }
2465 
2466         if (finalSolution == null || !finalSolution.isSolution) {
2467             result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
2468         } else {
2469             result[0] = finalSolution.cellX;
2470             result[1] = finalSolution.cellY;
2471             resultSpan[0] = finalSolution.spanX;
2472             resultSpan[1] = finalSolution.spanY;
2473             performReorder(finalSolution, dragView, mode);
2474         }
2475         return result;
2476     }
2477 
2478     /**
2479      * Animates and submits in the DB the given ItemConfiguration depending of the mode.
2480      *
2481      * @param solution represents widgets on the screen which the Workspace will animate to and
2482      * would be submitted to the database.
2483      * @param dragView view which is being dragged over the workspace that trigger the reorder
2484      * @param mode depending on the mode different animations would be played and depending on the
2485      *             mode the solution would be submitted or not the database.
2486      *             The possible modes are {@link MODE_SHOW_REORDER_HINT}, {@link MODE_DRAG_OVER},
2487      *             {@link MODE_ON_DROP}, {@link MODE_ON_DROP_EXTERNAL}, {@link  MODE_ACCEPT_DROP}
2488      *             defined in {@link CellLayout}.
2489      */
performReorder(ItemConfiguration solution, View dragView, int mode)2490     public void performReorder(ItemConfiguration solution, View dragView, int mode) {
2491         if (mode == MODE_SHOW_REORDER_HINT) {
2492             beginOrAdjustReorderPreviewAnimations(solution, dragView,
2493                     ReorderPreviewAnimation.MODE_HINT);
2494             return;
2495         }
2496         // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
2497         // committing anything or animating anything as we just want to determine if a solution
2498         // exists
2499         if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
2500             if (!DESTRUCTIVE_REORDER) {
2501                 setUseTempCoords(true);
2502             }
2503 
2504             if (!DESTRUCTIVE_REORDER) {
2505                 copySolutionToTempState(solution, dragView);
2506             }
2507             setItemPlacementDirty(true);
2508             animateItemsToSolution(solution, dragView, mode == MODE_ON_DROP);
2509 
2510             if (!DESTRUCTIVE_REORDER
2511                     && (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
2512                 // Since the temp solution didn't update dragView, don't commit it either
2513                 commitTempPlacement(dragView);
2514                 completeAndClearReorderPreviewAnimations();
2515                 setItemPlacementDirty(false);
2516             } else {
2517                 beginOrAdjustReorderPreviewAnimations(solution, dragView,
2518                         ReorderPreviewAnimation.MODE_PREVIEW);
2519             }
2520         }
2521 
2522         if (mode == MODE_ON_DROP && !DESTRUCTIVE_REORDER) {
2523             setUseTempCoords(false);
2524         }
2525 
2526         mShortcutsAndWidgets.requestLayout();
2527     }
2528 
setItemPlacementDirty(boolean dirty)2529     void setItemPlacementDirty(boolean dirty) {
2530         mItemPlacementDirty = dirty;
2531     }
isItemPlacementDirty()2532     boolean isItemPlacementDirty() {
2533         return mItemPlacementDirty;
2534     }
2535 
2536     /**
2537      * Represents the solution to a reorder of items in the Workspace.
2538      */
2539     public static class ItemConfiguration extends CellAndSpan {
2540         public final ArrayMap<View, CellAndSpan> map = new ArrayMap<>();
2541         private final ArrayMap<View, CellAndSpan> savedMap = new ArrayMap<>();
2542         public final ArrayList<View> sortedViews = new ArrayList<>();
2543         public ArrayList<View> intersectingViews;
2544         public boolean isSolution = false;
2545 
save()2546         public void save() {
2547             // Copy current state into savedMap
2548             for (View v: map.keySet()) {
2549                 savedMap.get(v).copyFrom(map.get(v));
2550             }
2551         }
2552 
restore()2553         public void restore() {
2554             // Restore current state from savedMap
2555             for (View v: savedMap.keySet()) {
2556                 map.get(v).copyFrom(savedMap.get(v));
2557             }
2558         }
2559 
add(View v, CellAndSpan cs)2560         public void add(View v, CellAndSpan cs) {
2561             map.put(v, cs);
2562             savedMap.put(v, new CellAndSpan());
2563             sortedViews.add(v);
2564         }
2565 
area()2566         public int area() {
2567             return spanX * spanY;
2568         }
2569 
getBoundingRectForViews(ArrayList<View> views, Rect outRect)2570         public void getBoundingRectForViews(ArrayList<View> views, Rect outRect) {
2571             boolean first = true;
2572             for (View v: views) {
2573                 CellAndSpan c = map.get(v);
2574                 if (first) {
2575                     outRect.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
2576                     first = false;
2577                 } else {
2578                     outRect.union(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
2579                 }
2580             }
2581         }
2582     }
2583 
2584     /**
2585      * Find a starting cell position that will fit the given bounds nearest the requested
2586      * cell location. Uses Euclidean distance to score multiple vacant areas.
2587      *
2588      * @param pixelX The X location at which you want to search for a vacant area.
2589      * @param pixelY The Y location at which you want to search for a vacant area.
2590      * @param spanX Horizontal span of the object.
2591      * @param spanY Vertical span of the object.
2592      * @param result Previously returned value to possibly recycle.
2593      * @return The X, Y cell of a vacant area that can contain this object,
2594      *         nearest the requested location.
2595      */
findNearestAreaIgnoreOccupied(int pixelX, int pixelY, int spanX, int spanY, int[] result)2596     public int[] findNearestAreaIgnoreOccupied(int pixelX, int pixelY, int spanX, int spanY,
2597             int[] result) {
2598         return findNearestArea(pixelX, pixelY, spanX, spanY, spanX, spanY, true, result, null);
2599     }
2600 
existsEmptyCell()2601     boolean existsEmptyCell() {
2602         return findCellForSpan(null, 1, 1);
2603     }
2604 
2605     /**
2606      * Finds the upper-left coordinate of the first rectangle in the grid that can
2607      * hold a cell of the specified dimensions. If intersectX and intersectY are not -1,
2608      * then this method will only return coordinates for rectangles that contain the cell
2609      * (intersectX, intersectY)
2610      *
2611      * @param cellXY The array that will contain the position of a vacant cell if such a cell
2612      *               can be found.
2613      * @param spanX The horizontal span of the cell we want to find.
2614      * @param spanY The vertical span of the cell we want to find.
2615      *
2616      * @return True if a vacant cell of the specified dimension was found, false otherwise.
2617      */
findCellForSpan(int[] cellXY, int spanX, int spanY)2618     public boolean findCellForSpan(int[] cellXY, int spanX, int spanY) {
2619         if (cellXY == null) {
2620             cellXY = new int[2];
2621         }
2622         return mOccupied.findVacantCell(cellXY, spanX, spanY);
2623     }
2624 
2625     /**
2626      * A drag event has begun over this layout.
2627      * It may have begun over this layout (in which case onDragChild is called first),
2628      * or it may have begun on another layout.
2629      */
onDragEnter()2630     void onDragEnter() {
2631         mDragging = true;
2632         mPreviousSolution = null;
2633     }
2634 
2635     /**
2636      * Called when drag has left this CellLayout or has been completed (successfully or not)
2637      */
onDragExit()2638     void onDragExit() {
2639         // This can actually be called when we aren't in a drag, e.g. when adding a new
2640         // item to this layout via the customize drawer.
2641         // Guard against that case.
2642         if (mDragging) {
2643             mDragging = false;
2644         }
2645 
2646         // Invalidate the drag data
2647         mPreviousSolution = null;
2648         mDragCell[0] = mDragCell[1] = -1;
2649         mDragCellSpan[0] = mDragCellSpan[1] = -1;
2650         mDragOutlineAnims[mDragOutlineCurrent].animateOut();
2651         mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length;
2652         revertTempState();
2653         setIsDragOverlapping(false);
2654     }
2655 
2656     /**
2657      * Mark a child as having been dropped.
2658      * At the beginning of the drag operation, the child may have been on another
2659      * screen, but it is re-parented before this method is called.
2660      *
2661      * @param child The child that is being dropped
2662      */
onDropChild(View child)2663     void onDropChild(View child) {
2664         if (child != null) {
2665             CellLayoutLayoutParams
2666                     lp = (CellLayoutLayoutParams) child.getLayoutParams();
2667             lp.dropped = true;
2668             child.requestLayout();
2669             markCellsAsOccupiedForView(child);
2670         }
2671     }
2672 
2673     /**
2674      * Computes a bounding rectangle for a range of cells
2675      *
2676      * @param cellX X coordinate of upper left corner expressed as a cell position
2677      * @param cellY Y coordinate of upper left corner expressed as a cell position
2678      * @param cellHSpan Width in cells
2679      * @param cellVSpan Height in cells
2680      * @param resultRect Rect into which to put the results
2681      */
cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect)2682     public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect) {
2683         final int cellWidth = mCellWidth;
2684         final int cellHeight = mCellHeight;
2685 
2686         // We observe a shift of 1 pixel on the x coordinate compared to the actual cell coordinates
2687         final int hStartPadding = getPaddingLeft()
2688                 + (int) Math.ceil(getUnusedHorizontalSpace() / 2f);
2689         final int vStartPadding = getPaddingTop();
2690 
2691         int x = hStartPadding + (cellX * mBorderSpace.x) + (cellX * cellWidth);
2692         int y = vStartPadding + (cellY * mBorderSpace.y) + (cellY * cellHeight);
2693 
2694         int width = cellHSpan * cellWidth + ((cellHSpan - 1) * mBorderSpace.x);
2695         int height = cellVSpan * cellHeight + ((cellVSpan - 1) * mBorderSpace.y);
2696 
2697         resultRect.set(x, y, x + width, y + height);
2698     }
2699 
markCellsAsOccupiedForView(View view)2700     public void markCellsAsOccupiedForView(View view) {
2701         if (view instanceof LauncherAppWidgetHostView
2702                 && view.getTag() instanceof LauncherAppWidgetInfo) {
2703             LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag();
2704             CellPos pos = mActivity.getCellPosMapper().mapModelToPresenter(info);
2705             mOccupied.markCells(pos.cellX, pos.cellY, info.spanX, info.spanY, true);
2706             return;
2707         }
2708         if (view == null || view.getParent() != mShortcutsAndWidgets) return;
2709         CellLayoutLayoutParams
2710                 lp = (CellLayoutLayoutParams) view.getLayoutParams();
2711         mOccupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, true);
2712     }
2713 
markCellsAsUnoccupiedForView(View view)2714     public void markCellsAsUnoccupiedForView(View view) {
2715         if (view instanceof LauncherAppWidgetHostView
2716                 && view.getTag() instanceof LauncherAppWidgetInfo) {
2717             LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag();
2718             CellPos pos = mActivity.getCellPosMapper().mapModelToPresenter(info);
2719             mOccupied.markCells(pos.cellX, pos.cellY, info.spanX, info.spanY, false);
2720             return;
2721         }
2722         if (view == null || view.getParent() != mShortcutsAndWidgets) return;
2723         CellLayoutLayoutParams
2724                 lp = (CellLayoutLayoutParams) view.getLayoutParams();
2725         mOccupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, false);
2726     }
2727 
getDesiredWidth()2728     public int getDesiredWidth() {
2729         return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth)
2730                 + ((mCountX - 1) * mBorderSpace.x);
2731     }
2732 
getDesiredHeight()2733     public int getDesiredHeight()  {
2734         return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight)
2735                 + ((mCountY - 1) * mBorderSpace.y);
2736     }
2737 
isOccupied(int x, int y)2738     public boolean isOccupied(int x, int y) {
2739         if (x >= 0 && x < mCountX && y >= 0 && y < mCountY) {
2740             return mOccupied.cells[x][y];
2741         }
2742         if (BuildConfig.IS_STUDIO_BUILD) {
2743             throw new RuntimeException("Position exceeds the bound of this CellLayout");
2744         }
2745         return true;
2746     }
2747 
2748     @Override
generateLayoutParams(AttributeSet attrs)2749     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
2750         return new CellLayoutLayoutParams(getContext(), attrs);
2751     }
2752 
2753     @Override
checkLayoutParams(ViewGroup.LayoutParams p)2754     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
2755         return p instanceof CellLayoutLayoutParams;
2756     }
2757 
2758     @Override
generateLayoutParams(ViewGroup.LayoutParams p)2759     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
2760         return new CellLayoutLayoutParams(p);
2761     }
2762 
2763     // This class stores info for two purposes:
2764     // 1. When dragging items (mDragInfo in Workspace), we store the View, its cellX & cellY,
2765     //    its spanX, spanY, and the screen it is on
2766     // 2. When long clicking on an empty cell in a CellLayout, we save information about the
2767     //    cellX and cellY coordinates and which page was clicked. We then set this as a tag on
2768     //    the CellLayout that was long clicked
2769     public static final class CellInfo extends CellAndSpan {
2770         public final View cell;
2771         final int screenId;
2772         final int container;
2773 
CellInfo(View v, ItemInfo info, CellPos cellPos)2774         public CellInfo(View v, ItemInfo info, CellPos cellPos) {
2775             cellX = cellPos.cellX;
2776             cellY = cellPos.cellY;
2777             spanX = info.spanX;
2778             spanY = info.spanY;
2779             cell = v;
2780             screenId = cellPos.screenId;
2781             container = info.container;
2782         }
2783 
2784         @Override
toString()2785         public String toString() {
2786             return "Cell[view=" + (cell == null ? "null" : cell.getClass())
2787                     + ", x=" + cellX + ", y=" + cellY + "]";
2788         }
2789     }
2790 
2791     /**
2792      * A Delegated cell Drawing for drawing on CellLayout
2793      */
2794     public abstract static class DelegatedCellDrawing {
2795         public int mDelegateCellX;
2796         public int mDelegateCellY;
2797 
2798         /**
2799          * Draw under CellLayout
2800          */
drawUnderItem(Canvas canvas)2801         public abstract void drawUnderItem(Canvas canvas);
2802 
2803         /**
2804          * Draw over CellLayout
2805          */
drawOverItem(Canvas canvas)2806         public abstract void drawOverItem(Canvas canvas);
2807     }
2808 
2809     /**
2810      * Returns whether an item can be placed in this CellLayout (after rearranging and/or resizing
2811      * if necessary).
2812      */
hasReorderSolution(ItemInfo itemInfo)2813     public boolean hasReorderSolution(ItemInfo itemInfo) {
2814         int[] cellPoint = new int[2];
2815         // Check for a solution starting at every cell.
2816         for (int cellX = 0; cellX < getCountX(); cellX++) {
2817             for (int cellY = 0; cellY < getCountY(); cellY++) {
2818                 cellToPoint(cellX, cellY, cellPoint);
2819                 if (findReorderSolution(cellPoint[0], cellPoint[1], itemInfo.minSpanX,
2820                         itemInfo.minSpanY, itemInfo.spanX, itemInfo.spanY, mDirectionVector, null,
2821                         true, new ItemConfiguration()).isSolution) {
2822                     return true;
2823                 }
2824             }
2825         }
2826         return false;
2827     }
2828 
2829     /**
2830      * Finds solution to accept hotseat migration to cell layout. commits solution if commitConfig
2831      */
makeSpaceForHotseatMigration(boolean commitConfig)2832     public boolean makeSpaceForHotseatMigration(boolean commitConfig) {
2833         int[] cellPoint = new int[2];
2834         int[] directionVector = new int[]{0, -1};
2835         cellToPoint(0, mCountY, cellPoint);
2836         ItemConfiguration configuration = new ItemConfiguration();
2837         if (findReorderSolution(cellPoint[0], cellPoint[1], mCountX, 1, mCountX, 1,
2838                 directionVector, null, false, configuration).isSolution) {
2839             if (commitConfig) {
2840                 copySolutionToTempState(configuration, null);
2841                 commitTempPlacement(null);
2842                 // undo marking cells occupied since there is actually nothing being placed yet.
2843                 mOccupied.markCells(0, mCountY - 1, mCountX, 1, false);
2844             }
2845             return true;
2846         }
2847         return false;
2848     }
2849 
2850     /**
2851      * returns a copy of cell layout's grid occupancy
2852      */
cloneGridOccupancy()2853     public GridOccupancy cloneGridOccupancy() {
2854         GridOccupancy occupancy = new GridOccupancy(mCountX, mCountY);
2855         mOccupied.copyTo(occupancy);
2856         return occupancy;
2857     }
2858 
isRegionVacant(int x, int y, int spanX, int spanY)2859     public boolean isRegionVacant(int x, int y, int spanX, int spanY) {
2860         return mOccupied.isRegionVacant(x, y, spanX, spanY);
2861     }
2862 }
2863