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
17package com.android.launcher3;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.TimeInterpolator;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.content.Context;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.Bitmap;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Paint;
31import android.graphics.Point;
32import android.graphics.Rect;
33import android.graphics.drawable.ColorDrawable;
34import android.graphics.drawable.Drawable;
35import android.os.Parcelable;
36import android.support.annotation.IntDef;
37import android.support.v4.view.ViewCompat;
38import android.util.AttributeSet;
39import android.util.Log;
40import android.util.SparseArray;
41import android.view.MotionEvent;
42import android.view.View;
43import android.view.ViewDebug;
44import android.view.ViewGroup;
45import android.view.accessibility.AccessibilityEvent;
46import android.view.animation.DecelerateInterpolator;
47
48import com.android.launcher3.BubbleTextView.BubbleTextShadowHandler;
49import com.android.launcher3.LauncherSettings.Favorites;
50import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
51import com.android.launcher3.accessibility.FolderAccessibilityHelper;
52import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
53import com.android.launcher3.anim.PropertyListBuilder;
54import com.android.launcher3.config.ProviderConfig;
55import com.android.launcher3.folder.FolderIcon;
56import com.android.launcher3.graphics.DragPreviewProvider;
57import com.android.launcher3.util.CellAndSpan;
58import com.android.launcher3.util.GridOccupancy;
59import com.android.launcher3.util.ParcelableSparseArray;
60import com.android.launcher3.util.Thunk;
61
62import java.lang.annotation.Retention;
63import java.lang.annotation.RetentionPolicy;
64import java.util.ArrayList;
65import java.util.Arrays;
66import java.util.Collections;
67import java.util.Comparator;
68import java.util.HashMap;
69import java.util.Stack;
70
71public 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
185    public CellLayout(Context context) {
186        this(context, null);
187    }
188
189    public CellLayout(Context context, AttributeSet attrs) {
190        this(context, attrs, 0);
191    }
192
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
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
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
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
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
358    public void enableHardwareLayer(boolean hasLayer) {
359        mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint);
360    }
361
362    public void buildHardwareLayer() {
363        mShortcutsAndWidgets.buildLayer();
364    }
365
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
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.
383    public void setInvertIfRtl(boolean invert) {
384        mShortcutsAndWidgets.setInvertIfRtl(invert);
385    }
386
387    public void setDropPending(boolean pending) {
388        mDropPending = pending;
389    }
390
391    public boolean isDropPending() {
392        return mDropPending;
393    }
394
395    @Override
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
409    void disableDragTarget() {
410        mIsDragTarget = false;
411    }
412
413    public boolean isDragTarget() {
414        return mIsDragTarget;
415    }
416
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
426    public void disableJailContent() {
427        mJailContent = false;
428    }
429
430    @Override
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
442    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
443        super.dispatchRestoreInstanceState(mJailContent ? getJailedArray(container) : container);
444    }
445
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
452    public boolean getIsDragOverlapping() {
453        return mIsDragOverlapping;
454    }
455
456    @Override
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
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
536    public void addFolderBackground(FolderIcon.PreviewBackground bg) {
537        mFolderBackgrounds.add(bg);
538    }
539    public void removeFolderBackground(FolderIcon.PreviewBackground bg) {
540        mFolderBackgrounds.remove(bg);
541    }
542
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
556    public void clearFolderLeaveBehind() {
557        mFolderLeaveBehind.delegateCellX = -1;
558        mFolderLeaveBehind.delegateCellY = -1;
559        invalidate();
560    }
561
562    @Override
563    public boolean shouldDelayChildPressedState() {
564        return false;
565    }
566
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
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
591    public void setOnInterceptTouchListener(View.OnTouchListener listener) {
592        mInterceptTouchListener = listener;
593    }
594
595    public int getCountX() {
596        return mCountX;
597    }
598
599    public int getCountY() {
600        return mCountY;
601    }
602
603    public boolean acceptsWidget() {
604        return mContainerType == WORKSPACE;
605    }
606
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
642    public void removeAllViews() {
643        mOccupied.clear();
644        mShortcutsAndWidgets.removeAllViews();
645    }
646
647    @Override
648    public void removeAllViewsInLayout() {
649        if (mShortcutsAndWidgets.getChildCount() > 0) {
650            mOccupied.clear();
651            mShortcutsAndWidgets.removeAllViewsInLayout();
652        }
653    }
654
655    @Override
656    public void removeView(View view) {
657        markCellsAsUnoccupiedForView(view);
658        mShortcutsAndWidgets.removeView(view);
659    }
660
661    @Override
662    public void removeViewAt(int index) {
663        markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index));
664        mShortcutsAndWidgets.removeViewAt(index);
665    }
666
667    @Override
668    public void removeViewInLayout(View view) {
669        markCellsAsUnoccupiedForView(view);
670        mShortcutsAndWidgets.removeViewInLayout(view);
671    }
672
673    @Override
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
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     */
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     */
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     */
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     */
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     */
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     */
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
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
784    public int getCellWidth() {
785        return mCellWidth;
786    }
787
788    int getCellHeight() {
789        return mCellHeight;
790    }
791
792    public void setFixedSize(int width, int height) {
793        mFixedWidth = width;
794        mFixedHeight = height;
795    }
796
797    @Override
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
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     */
879    public int getUnusedHorizontalSpace() {
880        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth);
881    }
882
883    public float getBackgroundAlpha() {
884        return mBackgroundAlpha;
885    }
886
887    public void setBackgroundAlpha(float alpha) {
888        if (mBackgroundAlpha != alpha) {
889            mBackgroundAlpha = alpha;
890            mBackground.setAlpha((int) (mBackgroundAlpha * 255));
891        }
892    }
893
894    @Override
895    protected boolean verifyDrawable(Drawable who) {
896        return super.verifyDrawable(who) || (mIsDragTarget && who == mBackground);
897    }
898
899    public void setShortcutAndWidgetAlpha(float alpha) {
900        mShortcutsAndWidgets.setAlpha(alpha);
901    }
902
903    public ShortcutAndWidgetContainer getShortcutsAndWidgets() {
904        return mShortcutsAndWidgets;
905    }
906
907    public View getChildAt(int x, int y) {
908        return mShortcutsAndWidgets.getChildAt(x, y);
909    }
910
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
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
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
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     */
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>();
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
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     */
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     */
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
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")
1364        public ViewCluster(ArrayList<View> views, ItemConfiguration config) {
1365            this.views = (ArrayList<View>) views.clone();
1366            this.config = config;
1367            resetEdges();
1368        }
1369
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
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
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
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
1487        public void addView(View v) {
1488            views.add(v);
1489            resetEdges();
1490        }
1491
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;
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
1519        public void sortConfigurationForEdgePush(int edge) {
1520            comparator.whichEdge = edge;
1521            Collections.sort(config.sortedViews, comparator);
1522        }
1523    }
1524
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
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.
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
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     */
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
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
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
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
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
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
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
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
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
2088        private void cancel() {
2089            if (a != null) {
2090                a.cancel();
2091            }
2092        }
2093
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
2112    private void completeAndClearReorderPreviewAnimations() {
2113        for (ReorderPreviewAnimation a: mShakeAnimators.values()) {
2114            a.completeAnimationImmediately();
2115        }
2116        mShakeAnimators.clear();
2117    }
2118
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
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
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    */
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.
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
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
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
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
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
2412    void setItemPlacementDirty(boolean dirty) {
2413        mItemPlacementDirty = dirty;
2414    }
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
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
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
2440        void add(View v, CellAndSpan cs) {
2441            map.put(v, cs);
2442            savedMap.put(v, new CellAndSpan());
2443            sortedViews.add(v);
2444        }
2445
2446        int area() {
2447            return spanX * spanY;
2448        }
2449
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     */
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
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     */
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     */
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     */
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     */
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     */
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
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
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
2585    public int getDesiredWidth() {
2586        return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth);
2587    }
2588
2589    public int getDesiredHeight()  {
2590        return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight);
2591    }
2592
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
2602    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
2603        return new CellLayout.LayoutParams(getContext(), attrs);
2604    }
2605
2606    @Override
2607    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
2608        return p instanceof CellLayout.LayoutParams;
2609    }
2610
2611    @Override
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
2682        public LayoutParams(Context c, AttributeSet attrs) {
2683            super(c, attrs);
2684            cellHSpan = 1;
2685            cellVSpan = 1;
2686        }
2687
2688        public LayoutParams(ViewGroup.LayoutParams source) {
2689            super(source);
2690            cellHSpan = 1;
2691            cellVSpan = 1;
2692        }
2693
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
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
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         */
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
2740        public String toString() {
2741            return "(" + this.cellX + ", " + this.cellY + ")";
2742        }
2743
2744        public void setWidth(int width) {
2745            this.width = width;
2746        }
2747
2748        public int getWidth() {
2749            return width;
2750        }
2751
2752        public void setHeight(int height) {
2753            this.height = height;
2754        }
2755
2756        public int getHeight() {
2757            return height;
2758        }
2759
2760        public void setX(int x) {
2761            this.x = x;
2762        }
2763
2764        public int getX() {
2765            return x;
2766        }
2767
2768        public void setY(int y) {
2769            this.y = y;
2770        }
2771
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
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
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     */
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
2825    public boolean isRegionVacant(int x, int y, int spanX, int spanY) {
2826        return mOccupied.isRegionVacant(x, y, spanX, spanY);
2827    }
2828}
2829