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