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