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