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