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