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