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