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