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