Initial implementation of folder reordering

Change-Id: I9f700e71f46e3b91afa96742d9e3890d519c391d
diff --git a/res/layout-land/user_folder.xml b/res/layout-land/user_folder.xml
deleted file mode 100644
index 87499e7..0000000
--- a/res/layout-land/user_folder.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-  
-          http://www.apache.org/licenses/LICENSE-2.0
-  
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<com.android.launcher2.Folder
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher"
-    android:orientation="vertical"
-    android:background="@drawable/portal_container_holo">
-
-    <com.android.launcher2.CellLayout
-        android:id="@id/folder_content"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-
-        android:cacheColorHint="#ff333333"
-
-        android:hapticFeedbackEnabled="false"
-        launcher:widthGap="@dimen/workspace_width_gap"
-        launcher:heightGap="@dimen/workspace_height_gap"
-        launcher:cellWidth="@dimen/workspace_cell_width"
-        launcher:cellHeight="@dimen/workspace_cell_height"
-        launcher:xAxisStartPadding="0dip"
-        launcher:xAxisEndPadding="0dip"
-        launcher:yAxisStartPadding="8dip"
-        launcher:yAxisEndPadding="8dip"/>
-
-</com.android.launcher2.Folder>
diff --git a/res/layout-port/user_folder.xml b/res/layout/user_folder.xml
similarity index 99%
rename from res/layout-port/user_folder.xml
rename to res/layout/user_folder.xml
index 0d383c1..5ef959d 100644
--- a/res/layout-port/user_folder.xml
+++ b/res/layout/user_folder.xml
@@ -4,9 +4,9 @@
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
-  
+
           http://www.apache.org/licenses/LICENSE-2.0
-  
+
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -36,5 +36,4 @@
         launcher:xAxisEndPadding="0dip"
         launcher:yAxisStartPadding="8dip"
         launcher:yAxisEndPadding="8dip"/>
-
 </com.android.launcher2.Folder>
diff --git a/src/com/android/launcher2/CellLayout.java b/src/com/android/launcher2/CellLayout.java
index e5aa7a9..a9ba88d 100644
--- a/src/com/android/launcher2/CellLayout.java
+++ b/src/com/android/launcher2/CellLayout.java
@@ -20,6 +20,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -48,6 +49,7 @@
 import com.android.launcher.R;
 
 import java.util.Arrays;
+import java.util.HashMap;
 
 public class CellLayout extends ViewGroup {
     static final String TAG = "CellLayout";
@@ -118,6 +120,9 @@
     private InterruptibleInOutAnimator mCrosshairsAnimator = null;
     private float mCrosshairsVisibility = 0.0f;
 
+    private HashMap<CellLayout.LayoutParams, ObjectAnimator> mReorderAnimators = new
+            HashMap<CellLayout.LayoutParams, ObjectAnimator>();
+
     // When a drag operation is in progress, holds the nearest cell to the touch point
     private final int[] mDragCell = new int[2];
 
@@ -966,6 +971,64 @@
         return mChildren.getChildAt(x, y);
     }
 
+    public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration) {
+        CellLayoutChildren clc = getChildrenLayout();
+        if (clc.indexOfChild(child) != -1 && !mOccupied[cellX][cellY]) {
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            final ItemInfo info = (ItemInfo) child.getTag();
+
+            // We cancel any existing animations
+            if (mReorderAnimators.containsKey(lp)) {
+                mReorderAnimators.get(lp).cancel();
+                mReorderAnimators.remove(lp);
+            }
+
+            int oldX = lp.x;
+            int oldY = lp.y;
+            mOccupied[lp.cellX][lp.cellY] = false;
+            mOccupied[cellX][cellY] = true;
+
+            lp.isLockedToGrid = true;
+            lp.cellX = info.cellX = cellX;
+            lp.cellY = info.cellY = cellY;
+            clc.setupLp(lp);
+            lp.isLockedToGrid = false;
+            int newX = lp.x;
+            int newY = lp.y;
+
+            PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", oldX, newX);
+            PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", oldY, newY);
+            ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(lp, x, y);
+            oa.setDuration(duration);
+            mReorderAnimators.put(lp, oa);
+            oa.addUpdateListener(new AnimatorUpdateListener() {
+                public void onAnimationUpdate(ValueAnimator animation) {
+                    child.requestLayout();
+                }
+            });
+            oa.addListener(new AnimatorListenerAdapter() {
+                boolean cancelled = false;
+                public void onAnimationEnd(Animator animation) {
+                    // If the animation was cancelled, it means that another animation
+                    // has interrupted this one, and we don't want to lock the item into
+                    // place just yet.
+                    if (!cancelled) {
+                        lp.isLockedToGrid = true;
+                    }
+                    if (mReorderAnimators.containsKey(lp)) {
+                        mReorderAnimators.remove(lp);
+                    }
+                }
+                public void onAnimationCancel(Animator animation) {
+                    cancelled = true;
+                }
+            });
+            oa.start();
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Estimate where the top left cell of the dragged item will land if it is dropped.
      *
diff --git a/src/com/android/launcher2/CellLayoutChildren.java b/src/com/android/launcher2/CellLayoutChildren.java
index 59db9c9..6e78885 100644
--- a/src/com/android/launcher2/CellLayoutChildren.java
+++ b/src/com/android/launcher2/CellLayoutChildren.java
@@ -79,6 +79,10 @@
         setMeasuredDimension(widthSpecSize, heightSpecSize);
     }
 
+    public void setupLp(CellLayout.LayoutParams lp) {
+        lp.setup(mCellWidth, mCellHeight, mWidthGap, mHeightGap);
+    }
+
     public void measureChild(View child) {
         final int cellWidth = mCellWidth;
         final int cellHeight = mCellHeight;
diff --git a/src/com/android/launcher2/DragController.java b/src/com/android/launcher2/DragController.java
index 8ad5c7c..5b1b20a 100644
--- a/src/com/android/launcher2/DragController.java
+++ b/src/com/android/launcher2/DragController.java
@@ -284,6 +284,7 @@
 
         mDragging = true;
 
+        mDragObject.dragComplete = false;
         mDragObject.xOffset = mMotionDownX - (screenX + dragRegionLeft);
         mDragObject.yOffset = mMotionDownY - (screenY + dragRegionTop);
         mDragObject.dragSource = source;
@@ -373,6 +374,7 @@
     public void cancelDrag() {
         if (mDragging) {
             // Should we also be calling onDragExit() here?
+            mDragObject.dragComplete = true;
             mDragObject.dragSource.onDropCompleted(null, mDragObject, false);
         }
         endDrag();
@@ -565,6 +567,7 @@
         mDragObject.y = coordinates[1];
         boolean accepted = false;
         if (dropTarget != null) {
+            mDragObject.dragComplete = true;
             dropTarget.onDragExit(mDragObject);
             if (dropTarget.acceptDrop(mDragObject)) {
                 dropTarget.onDrop(mDragObject);
diff --git a/src/com/android/launcher2/DropTarget.java b/src/com/android/launcher2/DropTarget.java
index 9c0faf3..6e18479 100644
--- a/src/com/android/launcher2/DropTarget.java
+++ b/src/com/android/launcher2/DropTarget.java
@@ -34,6 +34,12 @@
         /** Y offset from the upper-left corner of the cell to where we touched.  */
         public int yOffset = -1;
 
+        /** This indicates whether a drag is in final stages, either drop or cancel. It
+         * differentiates onDragExit, since this is called when the drag is ending, above
+         * the current drag target, or when the drag moves off the current drag object.
+         */
+        public boolean dragComplete = false;
+
         /** The view that moves around while you drag.  */
         public DragView dragView = null;
 
diff --git a/src/com/android/launcher2/Folder.java b/src/com/android/launcher2/Folder.java
index d6be307..d81183c 100644
--- a/src/com/android/launcher2/Folder.java
+++ b/src/com/android/launcher2/Folder.java
@@ -23,6 +23,7 @@
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.content.Context;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
@@ -54,11 +55,6 @@
     protected Launcher mLauncher;
 
     protected FolderInfo mInfo;
-    
-    /**
-     * Which item is being dragged
-     */
-    protected ShortcutInfo mDragItem;
 
     private static final String TAG = "Launcher.Folder";
 
@@ -75,6 +71,8 @@
     private int[] mDragItemPosition = new int[2];
     private static final int FULL_GROW = 0;
     private static final int PARTIAL_GROW = 1;
+    private static final int REORDER_ANIMATION_DURATION = 230;
+    private static final int ON_EXIT_CLOSE_DELAY = 800;
     private int mMode = PARTIAL_GROW;
     private boolean mRearrangeOnClose = false;
     private FolderIcon mFolderIcon;
@@ -84,7 +82,14 @@
     private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
     private Drawable mIconDrawable;
     boolean mItemsInvalidated = false;
-    ShortcutInfo mCurrentDragInfo;
+    private ShortcutInfo mCurrentDragInfo;
+    private View mCurrentDragView;
+    boolean mSuppressOnAdd = false;
+    private int[] mTargetCell = new int[2];
+    private int[] mPreviousTargetCell = new int[2];
+    private int[] mEmptyCell = new int[2];
+    private Alarm mReorderAlarm = new Alarm();
+    private Alarm mOnExitAlarm = new Alarm();
 
     /**
      * Used to inflate the Workspace from XML.
@@ -136,8 +141,6 @@
     public boolean onLongClick(View v) {
         Object tag = v.getTag();
         if (tag instanceof ShortcutInfo) {
-            mLauncher.closeFolder(this);
-
             ShortcutInfo item = (ShortcutInfo) tag;
             if (!v.isInTouchMode()) {
                 return false;
@@ -150,10 +153,11 @@
             mIconDrawable = ((TextView) v).getCompoundDrawables()[1];
 
             mCurrentDragInfo = item;
-            mItemsInvalidated = true;
-            mInfo.itemsChanged();
-
-            mDragItem = item;
+            mEmptyCell[0] = item.cellX;
+            mEmptyCell[1] = item.cellY;
+            mCurrentDragView = v;
+            mContent.removeView(mCurrentDragView);
+            mInfo.remove(item);
         } else {
             mLauncher.closeFolder(this);
             mLauncher.showRenameDialog(mInfo);
@@ -182,8 +186,6 @@
 
         mDragController.startDrag(view, this, app, DragController.DRAG_ACTION_COPY);
         mLauncher.closeFolder(this);
-        mDragItem = app;
-
         return true;
     }
 
@@ -382,25 +384,6 @@
                     !isFull());
     }
 
-    public void onDrop(DragObject d) {
-        ShortcutInfo item;
-        if (d.dragInfo instanceof ApplicationInfo) {
-            // Came from all apps -- make a copy
-            item = ((ApplicationInfo) d.dragInfo).makeShortcut();
-            item.spanX = 1;
-            item.spanY = 1;
-        } else {
-            item = (ShortcutInfo) d.dragInfo;
-        }
-
-        // Dragged from self onto self
-        if (item == mCurrentDragInfo) {
-            mInfo.remove(item);
-        }
-
-        mInfo.add(item);
-    }
-
     protected boolean findAndSetEmptyCells(ShortcutInfo item) {
         int[] emptyCell = new int[2];
         if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) {
@@ -430,46 +413,135 @@
     }
 
     public void onDragEnter(DragObject d) {
+        mPreviousTargetCell[0] = -1;
+        mPreviousTargetCell[1] = -1;
         mContent.onDragEnter();
+        mOnExitAlarm.cancelAlarm();
+    }
+
+    OnAlarmListener mReorderAlarmListener = new OnAlarmListener() {
+        public void onAlarm(Alarm alarm) {
+            realTimeReorder(mEmptyCell, mTargetCell);
+        }
+    };
+
+    boolean readingOrderGreaterThan(int[] v1, int[] v2) {
+        if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private void realTimeReorder(int[] empty, int[] target) {
+        boolean wrap;
+        int startX;
+        int endX;
+        int startY;
+        if (readingOrderGreaterThan(target, empty)) {
+            wrap = empty[0] >= mContent.getCountX() - 1;
+            startY = wrap ? empty[1] + 1 : empty[1];
+            for (int y = startY; y <= target[1]; y++) {
+                startX = y == empty[1] ? empty[0] + 1 : 0;
+                endX = y < target[1] ? mContent.getCountX() - 1 : target[0];
+                for (int x = startX; x <= endX; x++) {
+                    View v = mContent.getChildAt(x,y);
+                    if (mContent.animateChildToPosition(v, empty[0], empty[1],
+                            REORDER_ANIMATION_DURATION)) {
+                        empty[0] = x;
+                        empty[1] = y;
+                    }
+                }
+            }
+        } else {
+            wrap = empty[0] == 0;
+            startY = wrap ? empty[1] - 1 : empty[1];
+            for (int y = startY; y >= target[1]; y--) {
+                startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1;
+                endX = y > target[1] ? 0 : target[0];
+                for (int x = startX; x >= endX; x--) {
+                    View v = mContent.getChildAt(x,y);
+                    if (mContent.animateChildToPosition(v, empty[0], empty[1],
+                            REORDER_ANIMATION_DURATION)) {
+                        empty[0] = x;
+                        empty[1] = y;
+                    }
+                }
+            }
+        }
     }
 
     public void onDragOver(DragObject d) {
-        float[] r = mapPointFromScreenToContent(d.x, d.y, null);
-        mContent.visualizeDropLocation(null, null, (int) r[0], (int) r[1], 1, 1);
+        float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null);
+        mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1], 1, 1, mTargetCell);
+
+        if (mTargetCell[0] != mPreviousTargetCell[0] || mTargetCell[1] != mPreviousTargetCell[1]) {
+            mReorderAlarm.cancelAlarm();
+            mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
+            mReorderAlarm.setAlarm(150);
+            mPreviousTargetCell[0] = mTargetCell[0];
+            mPreviousTargetCell[1] = mTargetCell[1];
+        }
     }
 
+    // This is used to compute the visual center of the dragView. The idea is that
+    // the visual center represents the user's interpretation of where the item is, and hence
+    // is the appropriate point to use when determining drop location.
+    private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset,
+            DragView dragView, float[] recycle) {
+        float res[];
+        if (recycle == null) {
+            res = new float[2];
+        } else {
+            res = recycle;
+        }
+
+        // These represent the visual top and left of drag view if a dragRect was provided.
+        // If a dragRect was not provided, then they correspond to the actual view left and
+        // top, as the dragRect is in that case taken to be the entire dragView.
+        // R.dimen.dragViewOffsetY.
+        int left = x - xOffset;
+        int top = y - yOffset;
+
+        // In order to find the visual center, we shift by half the dragRect
+        res[0] = left + dragView.getDragRegion().width() / 2;
+        res[1] = top + dragView.getDragRegion().height() / 2;
+
+        return res;
+    }
+
+    OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
+        public void onAlarm(Alarm alarm) {
+            mLauncher.closeFolder(Folder.this);
+            mCurrentDragInfo = null;
+            mCurrentDragView = null;
+            mSuppressOnAdd = false;
+            mRearrangeOnClose = true;
+        }
+    };
+
     public void onDragExit(DragObject d) {
+        // We only close the folder if this is a true drag exit, ie. not because a drop
+        // has occurred above the folder.
+        if (!d.dragComplete) {
+            mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener);
+            mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
+        }
+        mReorderAlarm.cancelAlarm();
         mContent.onDragExit();
     }
 
-    public float[] mapPointFromScreenToContent(int x, int y, float[] r) {
-        if (r == null) {
-            r = new float[2];
-        }
-
-        int[] screenLocation = new int[2];
-        mContent.getLocationOnScreen(screenLocation);
-
-        r[0] = x - screenLocation[0];
-        r[1] = y - screenLocation[1];
-        return r;
-    }
-
     public void onDropCompleted(View target, DragObject d, boolean success) {
+        mCurrentDragInfo = null;
+        mCurrentDragView = null;
+        mSuppressOnAdd = false;
         if (!success) {
             if (d.dragView != null) {
                 if (target instanceof CellLayout) {
                     // TODO: we should animate the item back to the folder in this case
                 }
             }
-            mCurrentDragInfo = null;
-            mItemsInvalidated = true;
-            mInfo.itemsChanged();
-        } else {
-            if (target != this) {
-                mInfo.remove(mCurrentDragInfo);
-                mCurrentDragInfo = null;
-            }
+            // TODO: if the drag fails, we need to re-add the item
         }
     }
 
@@ -594,18 +666,6 @@
         mItemsInvalidated = true;
     }
 
-    public void onAdd(ShortcutInfo item) {
-        mItemsInvalidated = true;
-        if (!findAndSetEmptyCells(item)) {
-            // The current layout is full, can we expand it?
-            setupContentForNumItems(getItemCount() + 1);
-            findAndSetEmptyCells(item);
-        }
-        createAndAddShortcut(item);
-        LauncherModel.addOrMoveItemInDatabase(
-                mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
-    }
-
     public int getItemCount() {
         return mContent.getChildrenLayout().getChildCount();
     }
@@ -621,8 +681,46 @@
         }
     }
 
+    public void onDrop(DragObject d) {
+        ShortcutInfo item;
+        if (d.dragInfo instanceof ApplicationInfo) {
+            // Came from all apps -- make a copy
+            item = ((ApplicationInfo) d.dragInfo).makeShortcut();
+            item.spanX = 1;
+            item.spanY = 1;
+        } else {
+            item = (ShortcutInfo) d.dragInfo;
+        }
+        // Dragged from self onto self
+        if (item == mCurrentDragInfo) {
+            ShortcutInfo si = (ShortcutInfo) mCurrentDragView.getTag();
+            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mCurrentDragView.getLayoutParams();
+            si.cellX = lp.cellX = mEmptyCell[0];
+            si.cellX = lp.cellY = mEmptyCell[1];
+            mContent.addViewToCellLayout(mCurrentDragView, -1, (int)item.id, lp, true);
+            mSuppressOnAdd = true;
+            mItemsInvalidated = true;
+            setupContentDimension(getItemCount());
+        }
+        mInfo.add(item);
+    }
+
+    public void onAdd(ShortcutInfo item) {
+        mItemsInvalidated = true;
+        if (mSuppressOnAdd) return;
+        if (!findAndSetEmptyCells(item)) {
+            // The current layout is full, can we expand it?
+            setupContentForNumItems(getItemCount() + 1);
+            findAndSetEmptyCells(item);
+        }
+        createAndAddShortcut(item);
+        LauncherModel.addOrMoveItemInDatabase(
+                mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
+    }
+
     public void onRemove(ShortcutInfo item) {
         mItemsInvalidated = true;
+        if (item == mCurrentDragInfo) return;
         View v = mContent.getChildAt(mDragItemPosition[0], mDragItemPosition[1]);
         mContent.removeView(v);
         if (mState == STATE_ANIMATING) {
diff --git a/src/com/android/launcher2/Workspace.java b/src/com/android/launcher2/Workspace.java
index ff2dede..1e01254 100644
--- a/src/com/android/launcher2/Workspace.java
+++ b/src/com/android/launcher2/Workspace.java
@@ -2555,42 +2555,6 @@
     }
 
     public DropTarget getDropTargetDelegate(DragObject d) {
-
-        if (mIsSmall || mIsInUnshrinkAnimation) {
-            // If we're shrunken, don't let anyone drag on folders/etc that are on the mini-screens
-            return null;
-        }
-        // We may need to delegate the drag to a child view. If a 1x1 item
-        // would land in a cell occupied by a DragTarget (e.g. a Folder),
-        // then drag events should be handled by that child.
-
-        ItemInfo item = (ItemInfo) d.dragInfo;
-        CellLayout currentLayout = getCurrentDropLayout();
-
-        int dragPointX, dragPointY;
-        if (item.spanX == 1 && item.spanY == 1) {
-            // For a 1x1, calculate the drop cell exactly as in onDragOver
-            dragPointX = d.x - d.xOffset;
-            dragPointY = d.y - d.yOffset;
-        } else {
-            // Otherwise, use the exact drag coordinates
-            dragPointX = d.x;
-            dragPointY = d.y;
-        }
-        dragPointX += mScrollX - currentLayout.getLeft();
-        dragPointY += mScrollY - currentLayout.getTop();
-
-        // If we are dragging over a cell that contains a DropTarget that will
-        // accept the drop, delegate to that DropTarget.
-        final int[] cellXY = mTempCell;
-        currentLayout.estimateDropCell(dragPointX, dragPointY, item.spanX, item.spanY, cellXY);
-        View child = currentLayout.getChildAt(cellXY[0], cellXY[1]);
-        if (child instanceof DropTarget) {
-            DropTarget target = (DropTarget)child;
-            if (target.acceptDrop(d)) {
-                return target;
-            }
-        }
         return null;
     }
 
@@ -3559,5 +3523,4 @@
     @Override
     public void syncPageItems(int page) {
     }
-
 }