Adding a scrollable folder content implementation

> Size is restricted to 3x3 for now
> Drag-drop across page s not implemented yet
> A-Z sorting is not implemented yet

Change-Id: I84328caa6ad910d1edeeac6f3a7fb61b7292ea7e
diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java
index c1aa19a..9f8d499 100644
--- a/src/com/android/launcher3/AppsCustomizePagedView.java
+++ b/src/com/android/launcher3/AppsCustomizePagedView.java
@@ -36,7 +36,6 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Gravity;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -45,6 +44,7 @@
 import android.widget.Toast;
 
 import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.FocusHelper.PagedViewKeyListener;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 
 import java.util.ArrayList;
@@ -139,7 +139,7 @@
  * The Apps/Customize page that displays all the applications, widgets, and shortcuts.
  */
 public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements
-        View.OnClickListener, View.OnKeyListener, DragSource,
+        View.OnClickListener, DragSource,
         PagedViewWidget.ShortPressListener, LauncherTransitionable {
     static final String TAG = "AppsCustomizePagedView";
 
@@ -182,6 +182,8 @@
     ArrayList<AppsCustomizeAsyncTask> mRunningTasks;
     private static final int sPageSleepDelay = 200;
 
+    private final PagedViewKeyListener mKeyListener = new PagedViewKeyListener();
+
     private Runnable mInflateWidgetRunnable = null;
     private Runnable mBindWidgetRunnable = null;
     static final int WIDGET_NO_CLEANUP_REQUIRED = -1;
@@ -449,10 +451,6 @@
         mWidgetInstructionToast.show();
     }
 
-    public boolean onKey(View v, int keyCode, KeyEvent event) {
-        return FocusHelper.handleAppsCustomizeKeyEvent(v,  keyCode, event);
-    }
-
     /*
      * PagedViewWithDraggableItems implementation
      */
@@ -959,7 +957,7 @@
             icon.setOnClickListener(mLauncher);
             icon.setOnLongClickListener(this);
             icon.setOnTouchListener(this);
-            icon.setOnKeyListener(this);
+            icon.setOnKeyListener(mKeyListener);
             icon.setOnFocusChangeListener(layout.mFocusHandlerView);
 
             int index = i - startIndex;
@@ -1141,7 +1139,7 @@
             widget.setOnClickListener(this);
             widget.setOnLongClickListener(this);
             widget.setOnTouchListener(this);
-            widget.setOnKeyListener(this);
+            widget.setOnKeyListener(mKeyListener);
 
             // Layout each widget
             int ix = i % mWidgetCountX;
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
index 737f6cc..b090a7c 100644
--- a/src/com/android/launcher3/FocusHelper.java
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -23,6 +23,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.android.launcher3.FocusHelper.PagedViewKeyListener;
 import com.android.launcher3.util.FocusLogic;
 
 /**
@@ -65,9 +66,32 @@
     //
 
     /**
+     * A keyboard listener for scrollable folders
+     */
+    public static class PagedFolderKeyEventListener extends PagedViewKeyListener {
+
+        private final Folder mFolder;
+
+        public PagedFolderKeyEventListener(Folder folder) {
+            mFolder = folder;
+        }
+
+        @Override
+        public void handleNoopKey(int keyCode, View v) {
+            if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+                mFolder.mFolderName.requestFocus();
+                playSoundEffect(keyCode, v);
+            }
+        }
+    }
+
+    /**
      * Handles key events in the all apps screen.
      */
-    static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) {
+    public static class PagedViewKeyListener implements View.OnKeyListener {
+
+    @Override
+    public boolean onKey(View v, int keyCode, KeyEvent e) {
         boolean consume = FocusLogic.shouldConsume(keyCode);
         if (e.getAction() == KeyEvent.ACTION_UP) {
             return consume;
@@ -87,15 +111,14 @@
             parentLayout = (ViewGroup) itemContainer.getParent();
             countX = ((CellLayout) parentLayout).getCountX();
             countY = ((CellLayout) parentLayout).getCountY();
-        } else if (v.getParent() instanceof ViewGroup) {
-            //TODO(hyunyoungs): figure out when this needs to be called.
-            itemContainer = parentLayout = (ViewGroup) v.getParent();
-            countX = ((PagedViewGridLayout) parentLayout).getCellCountX();
-            countY = ((PagedViewGridLayout) parentLayout).getCellCountY();
         } else {
-            throw new IllegalStateException(
-                    "Parent of the focused item inside all apps screen is not a supported type.");
+            if (LauncherAppState.isDogfoodBuild()) {
+                throw new IllegalStateException("Parent of the focused item is not supported.");
+            } else {
+                return false;
+            }
         }
+
         final int iconIndex = itemContainer.indexOfChild(v);
         final PagedView container = (PagedView) parentLayout.getParent();
         final int pageIndex = container.indexToPage(container.indexOfChild(parentLayout));
@@ -109,6 +132,7 @@
         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
                 iconIndex, pageIndex, pageCount);
         if (newIconIndex == FocusLogic.NOOP) {
+            handleNoopKey(keyCode, v);
             return consume;
         }
         switch (newIconIndex) {
@@ -163,10 +187,15 @@
         if (child != null) {
             child.requestFocus();
             playSoundEffect(keyCode, v);
+        } else {
+            handleNoopKey(keyCode, v);
         }
         return consume;
     }
 
+    public void handleNoopKey(int keyCode, View v) { }
+    }
+
     /**
      * Handles key events in the workspace hot seat (bottom of the screen).
      * <p>Currently we don't special case for the phone UI in different orientations, even though
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index 4414672..0a65579 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -66,6 +66,7 @@
      * results in CellLayout being measured as UNSPECIFIED, which it does not support.
      */
     private static final int MIN_CONTENT_DIMEN = 5;
+    private static final boolean ALLOW_FOLDER_SCROLL = true;
 
     static final int STATE_NONE = -1;
     static final int STATE_SMALL = 0;
@@ -100,8 +101,8 @@
     private View mContentWrapper;
     FolderEditText mFolderName;
 
-    private View mBottomContent;
-    private int mBottomContentHeight;
+    private View mFooter;
+    private int mFooterHeight;
 
     // Cell ranks used for drag and drop
     private int mTargetRank, mPrevTargetRank, mEmptyCellRank;
@@ -175,13 +176,12 @@
         mFolderName.setInputType(mFolderName.getInputType() |
                 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
 
-        // We only have the folder name at the bottom for now
-        mBottomContent = mFolderName;
-        // We find out how tall the bottom content wants to be (it is set to wrap_content), so that
+        mFooter = ALLOW_FOLDER_SCROLL ? findViewById(R.id.folder_footer) : mFolderName;
+        // We find out how tall footer wants to be (it is set to wrap_content), so that
         // we can allocate the appropriate amount of space for it.
         int measureSpec = MeasureSpec.UNSPECIFIED;
-        mBottomContent.measure(measureSpec, measureSpec);
-        mBottomContentHeight = mBottomContent.getMeasuredHeight();
+        mFooter.measure(measureSpec, measureSpec);
+        mFooterHeight = mFooter.getMeasuredHeight();
     }
 
     private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
@@ -225,7 +225,7 @@
             mEmptyCellRank = item.rank;
             mCurrentDragView = v;
 
-            mContent.removeView(mCurrentDragView);
+            mContent.removeItem(mCurrentDragView);
             mInfo.remove(mCurrentDragInfo);
             mDragInProgress = true;
             mItemAddedBackToSelfViaIcon = false;
@@ -359,7 +359,8 @@
      * @return A new UserFolder.
      */
     static Folder fromXml(Context context) {
-        return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null);
+        return (Folder) LayoutInflater.from(context).inflate(
+                ALLOW_FOLDER_SCROLL ? R.layout.user_folder_scroll : R.layout.user_folder, null);
     }
 
     /**
@@ -434,8 +435,8 @@
             iconsAlpha.setStartDelay(mMaterialExpandStagger);
             iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
 
-            mBottomContent.setAlpha(0f);
-            Animator textAlpha = LauncherAnimUtils.ofFloat(mBottomContent, "alpha", 0f, 1f);
+            mFooter.setAlpha(0f);
+            Animator textAlpha = LauncherAnimUtils.ofFloat(mFooter, "alpha", 0f, 1f);
             textAlpha.setDuration(mMaterialExpandDuration);
             textAlpha.setStartDelay(mMaterialExpandStagger);
             textAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
@@ -795,7 +796,7 @@
                 CellLayout.LANDSCAPE : CellLayout.PORTRAIT);
         int maxContentAreaHeight = grid.availableHeightPx -
                 workspacePadding.top - workspacePadding.bottom -
-                mBottomContentHeight;
+                mFooterHeight;
         int height = Math.min(maxContentAreaHeight,
                 mContent.getDesiredHeight());
         return Math.max(height, MIN_CONTENT_DIMEN);
@@ -810,7 +811,7 @@
     }
 
     private int getFolderHeight(int contentAreaHeight) {
-        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mBottomContentHeight;
+        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
     }
 
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -822,10 +823,8 @@
 
         mContent.setFixedSize(contentWidth, contentHeight);
         mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec);
-
-        // Move the bottom content below mContent
-        mBottomContent.measure(contentAreaWidthSpec,
-                MeasureSpec.makeMeasureSpec(mBottomContentHeight, MeasureSpec.EXACTLY));
+        mFooter.measure(contentAreaWidthSpec,
+                MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY));
 
         int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
         int folderHeight = getFolderHeight(contentHeight);
@@ -929,7 +928,7 @@
 
     // This method keeps track of the last item in the folder for the purposes
     // of keyboard focus
-    private void updateTextViewFocus() {
+    public void updateTextViewFocus() {
         View lastChild = mContent.getLastItem();
         if (lastChild != null) {
             mFolderName.setNextFocusDownId(lastChild.getId());
@@ -1028,7 +1027,7 @@
         // the work associated with removing the item, so we don't have to do anything here.
         if (item == mCurrentDragInfo) return;
         View v = getViewForInfo(item);
-        mContent.removeView(v);
+        mContent.removeItem(v);
         if (mState == STATE_ANIMATING) {
             mRearrangeOnClose = true;
         } else {
@@ -1090,7 +1089,7 @@
     public static interface FolderContent {
         void setFolder(Folder f);
 
-        void removeView(View v);
+        void removeItem(View v);
 
         boolean isFull();
         int getItemCount();
diff --git a/src/com/android/launcher3/FolderCellLayout.java b/src/com/android/launcher3/FolderCellLayout.java
index b354ec7..1566912 100644
--- a/src/com/android/launcher3/FolderCellLayout.java
+++ b/src/com/android/launcher3/FolderCellLayout.java
@@ -1,3 +1,19 @@
+/**
+ * Copyright (C) 2015 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.
+ */
+
 package com.android.launcher3;
 
 import android.content.Context;
@@ -138,6 +154,11 @@
         addViewToCellLayout(view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true);
     }
 
+    @Override
+    public void removeItem(View v) {
+        removeView(v);
+    }
+
     /**
      * Updates the item cellX and cellY position
      */
diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java
new file mode 100644
index 0000000..60c94e0
--- /dev/null
+++ b/src/com/android/launcher3/FolderPagedView.java
@@ -0,0 +1,581 @@
+/**
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.launcher3;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
+import com.android.launcher3.PageIndicator.PageMarkerResources;
+import com.android.launcher3.Workspace.ItemOperator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+public class FolderPagedView extends PagedView implements Folder.FolderContent {
+
+    private static final String TAG = "FolderPagedView";
+
+    private static final int REORDER_ANIMATION_DURATION = 230;
+    private static final int[] sTempPosArray = new int[2];
+
+    // TODO: Remove this restriction
+    private static final int MAX_ITEMS_PER_PAGE = 3;
+
+    private final LayoutInflater mInflater;
+    private final IconCache mIconCache;
+    private final HashMap<View, Runnable> mPageChangingViews = new HashMap<>();
+
+    private final CellLayout mFirstPage;
+
+    final int mMaxCountX;
+    final int mMaxCountY;
+    final int mMaxItemsPerPage;
+
+    private int mAllocatedContentSize;
+    private int mGridCountX;
+    private int mGridCountY;
+
+    private Folder mFolder;
+    private FocusIndicatorView mFocusIndicatorView;
+    private PagedFolderKeyEventListener mKeyListener;
+
+    public FolderPagedView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        LauncherAppState app = LauncherAppState.getInstance();
+
+        mFirstPage = newCellLayout();
+        addFullScreenPage(mFirstPage);
+        setCurrentPage(0);
+        setDataIsReady();
+
+        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
+        mMaxCountX = Math.min((int) grid.numColumns, MAX_ITEMS_PER_PAGE);
+        mMaxCountY = Math.min((int) grid.numRows, MAX_ITEMS_PER_PAGE);
+        mMaxItemsPerPage = mMaxCountX * mMaxCountY;
+
+        mInflater = LayoutInflater.from(context);
+        mIconCache = app.getIconCache();
+    }
+
+    @Override
+    public void setFolder(Folder folder) {
+        mFolder = folder;
+        mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator);
+        mKeyListener = new PagedFolderKeyEventListener(folder);
+    }
+
+    /**
+     * Sets up the grid size such that {@param count} items can fit in the grid.
+     * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while
+     * maintaining the restrictions of {@link #mMaxCountX} &amp; {@link #mMaxCountY}.
+     */
+    private void setupContentDimensions(int count) {
+        mAllocatedContentSize = count;
+        boolean done;
+        if (count >= mMaxItemsPerPage) {
+            mGridCountX = mMaxCountX;
+            mGridCountY = mMaxCountY;
+            done = true;
+        } else {
+            mGridCountX = mFirstPage.getCountX();
+            mGridCountY = mFirstPage.getCountY();
+            done = false;
+        }
+
+        while (!done) {
+            int oldCountX = mGridCountX;
+            int oldCountY = mGridCountY;
+            if (mGridCountX * mGridCountY < count) {
+                // Current grid is too small, expand it
+                if ((mGridCountX <= mGridCountY || mGridCountY == mMaxCountY) && mGridCountX < mMaxCountX) {
+                    mGridCountX++;
+                } else if (mGridCountY < mMaxCountY) {
+                    mGridCountY++;
+                }
+                if (mGridCountY == 0) mGridCountY++;
+            } else if ((mGridCountY - 1) * mGridCountX >= count && mGridCountY >= mGridCountX) {
+                mGridCountY = Math.max(0, mGridCountY - 1);
+            } else if ((mGridCountX - 1) * mGridCountY >= count) {
+                mGridCountX = Math.max(0, mGridCountX - 1);
+            }
+            done = mGridCountX == oldCountX && mGridCountY == oldCountY;
+        }
+
+        setGridSize(mGridCountX, mGridCountY);
+    }
+
+    public void setGridSize(int countX, int countY) {
+        mGridCountX = countX;
+        mGridCountY = countY;
+        mFirstPage.setGridSize(mGridCountX, mGridCountY);
+        for (int i = getPageCount() - 1; i > 0; i--) {
+            getPageAt(i).setGridSize(mGridCountX, mGridCountY);
+        }
+    }
+
+    @Override
+    public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) {
+        final int count = items.size();
+
+        if (getPageCount() > 1) {
+            Log.d(TAG, "Binding items to an non-empty view");
+            removeAllViews();
+            addView(mFirstPage);
+            mFirstPage.removeAllViews();
+        }
+
+        setupContentDimensions(count);
+        CellLayout page = mFirstPage;
+        int pagePosition = 0;
+        int rank = 0;
+
+        for (ShortcutInfo item : items) {
+            if (pagePosition >= mMaxItemsPerPage) {
+                // This page is full, add a new page.
+                pagePosition = 0;
+                page = newCellLayout();
+                addFullScreenPage(page);
+            }
+
+            item.cellX = pagePosition % mGridCountX;
+            item.cellY = pagePosition / mGridCountX;
+            item.rank = rank;
+            addNewView(item, page);
+
+            rank++;
+            pagePosition++;
+        }
+        return new ArrayList<ShortcutInfo>();
+    }
+
+    /**
+     * Create space for a new item at the end, and returns the rank for that item.
+     * Also sets the current page to the last page.
+     */
+    @Override
+    public int allocateNewLastItemRank() {
+        int rank = getItemCount();
+        int total = rank + 1;
+        if (rank < mMaxItemsPerPage) {
+            // Rearrange the items as the grid size might change.
+            mFolder.rearrangeChildren(total);
+        } else {
+            setupContentDimensions(total);
+        }
+
+        // Add a new page if last page is full
+        if (getPageAt(getChildCount() - 1).getShortcutsAndWidgets().getChildCount()
+                >= mMaxItemsPerPage) {
+            addFullScreenPage(newCellLayout());
+        }
+        setCurrentPage(getChildCount() - 1);
+        return rank;
+    }
+
+    @Override
+    public View createAndAddViewForRank(ShortcutInfo item, int rank) {
+        int pageNo = updateItemXY(item, rank);
+        CellLayout page = getPageAt(pageNo);
+        return addNewView(item, page);
+    }
+
+    @Override
+    public void addViewForRank(View view, ShortcutInfo item, int rank) {
+        int pageNo = updateItemXY(item, rank);
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
+        lp.cellX = item.cellX;
+        lp.cellY = item.cellY;
+        getPageAt(pageNo).addViewToCellLayout(
+                view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true);
+    }
+
+    /**
+     * Updates the item cellX and cellY position and return the page number for that item.
+     */
+    private int updateItemXY(ShortcutInfo item, int rank) {
+        item.rank = rank;
+
+        int pagePos = item.rank % mMaxItemsPerPage;
+        item.cellX = pagePos % mGridCountX;
+        item.cellY = pagePos / mGridCountX;
+
+        return item.rank / mMaxItemsPerPage;
+    }
+
+    private View addNewView(ShortcutInfo item, CellLayout target) {
+        final BubbleTextView textView = (BubbleTextView) mInflater.inflate(
+                R.layout.folder_application, target.getShortcutsAndWidgets(), false);
+        textView.applyFromShortcutInfo(item, mIconCache, false);
+        textView.setOnClickListener(mFolder);
+        textView.setOnLongClickListener(mFolder);
+        textView.setOnFocusChangeListener(mFocusIndicatorView);
+        textView.setOnKeyListener(mKeyListener);
+
+        CellLayout.LayoutParams lp = new CellLayout.LayoutParams(
+                item.cellX, item.cellY, item.spanX, item.spanY);
+        target.addViewToCellLayout(
+                textView, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true);
+        return textView;
+    }
+
+    @Override
+    public CellLayout getPageAt(int index) {
+        return (CellLayout) getChildAt(index);
+    }
+
+    public void removeCellLayoutView(View view) {
+        for (int i = getChildCount() - 1; i >= 0; i --) {
+            getPageAt(i).removeView(view);
+        }
+    }
+
+    public CellLayout getCurrentCellLayout() {
+        return getPageAt(getNextPage());
+    }
+
+    @Override
+    public void addFullScreenPage(View page) {
+        LayoutParams lp = generateDefaultLayoutParams();
+        lp.isFullScreenPage = true;
+        super.addView(page, -1, lp);
+    }
+
+    private CellLayout newCellLayout() {
+        DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
+
+        CellLayout layout = new CellLayout(getContext());
+        layout.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
+        layout.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
+        layout.setInvertIfRtl(true);
+
+        if (mFirstPage != null) {
+            layout.setGridSize(mFirstPage.getCountX(), mFirstPage.getCountY());
+        }
+
+        return layout;
+    }
+
+    @Override
+    public void setFixedSize(int width, int height) {
+        for (int i = getChildCount() - 1; i >= 0; i --) {
+            ((CellLayout) getChildAt(i)).setFixedSize(width, height);
+        }
+    }
+
+    @Override
+    public void removeItem(View v) {
+        for (int i = getChildCount() - 1; i >= 0; i --) {
+            getPageAt(i).removeView(v);
+        }
+    }
+
+    /**
+     * Updates position and rank of all the children in the view.
+     * It essentially removes all views from all the pages and then adds them again in appropriate
+     * page.
+     *
+     * @param list the ordered list of children.
+     * @param itemCount if greater than the total children count, empty spaces are left
+     * at the end, otherwise it is ignored.
+     *
+     */
+    @Override
+    public void arrangeChildren(ArrayList<View> list, int itemCount) {
+        ArrayList<CellLayout> pages = new ArrayList<CellLayout>();
+        for (int i = 0; i < getChildCount(); i++) {
+            CellLayout page = (CellLayout) getChildAt(i);
+            page.removeAllViews();
+            pages.add(page);
+        }
+        setupContentDimensions(itemCount);
+
+        Iterator<CellLayout> pageItr = pages.iterator();
+        CellLayout currentPage = null;
+
+        int position = 0;
+        int newX, newY, rank;
+
+        rank = 0;
+        for (View v : list) {
+            if (currentPage == null || position >= mMaxItemsPerPage) {
+                // Next page
+                if (pageItr.hasNext()) {
+                    currentPage = pageItr.next();
+                } else {
+                    currentPage = newCellLayout();
+                    addFullScreenPage(currentPage);
+                }
+                position = 0;
+            }
+
+            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
+            newX = position % mGridCountX;
+            newY = position / mGridCountX;
+            ItemInfo info = (ItemInfo) v.getTag();
+            if (info.cellX != newX || info.cellY != newY || info.rank != rank) {
+                info.cellX = newX;
+                info.cellY = newY;
+                info.rank = rank;
+                LauncherModel.addOrMoveItemInDatabase(getContext(), info,
+                        mFolder.mInfo.id, 0, info.cellX, info.cellY);
+            }
+            lp.cellX = info.cellX;
+            lp.cellY = info.cellY;
+            rank ++;
+            position++;
+            currentPage.addViewToCellLayout(
+                    v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true);
+        }
+
+        boolean removed = false;
+        while (pageItr.hasNext()) {
+            CellLayout layout = pageItr.next();
+            if (layout != mFirstPage) {
+                removeView(layout);
+                removed = true;
+            }
+        }
+        if (removed) {
+            setCurrentPage(0);
+        }
+    }
+
+    @Override
+    protected void loadAssociatedPages(int page, boolean immediateAndOnly) { }
+
+    @Override
+    public void syncPages() { }
+
+    @Override
+    public void syncPageItems(int page, boolean immediate) { }
+
+    public int getDesiredWidth() {
+        return mFirstPage.getDesiredWidth();
+    }
+
+    public int getDesiredHeight()  {
+        return mFirstPage.getDesiredHeight();
+    }
+
+    @Override
+    public int getItemCount() {
+        int lastPage = getChildCount() - 1;
+        return getPageAt(lastPage).getShortcutsAndWidgets().getChildCount()
+                + lastPage * mMaxItemsPerPage;
+    }
+
+    @Override
+    public int findNearestArea(int pixelX, int pixelY) {
+        int pageIndex = getNextPage();
+        CellLayout page = getPageAt(pageIndex);
+        page.findNearestArea(pixelX, pixelY, 1, 1, null, false, sTempPosArray);
+        if (mFolder.isLayoutRtl()) {
+            sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1;
+        }
+        return Math.min(mAllocatedContentSize - 1,
+                pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]);
+    }
+
+    @Override
+    protected PageMarkerResources getPageIndicatorMarker(int pageIndex) {
+        return new PageMarkerResources(R.drawable.ic_pageindicator_current_dark, R.drawable.ic_pageindicator_default_dark);
+    }
+
+    @Override
+    public boolean isFull() {
+        return false;
+    }
+
+    @Override
+    public View getLastItem() {
+        if (getChildCount() < 1) {
+            return null;
+        }
+        ShortcutAndWidgetContainer lastContainer = getCurrentCellLayout().getShortcutsAndWidgets();
+        int lastRank = lastContainer.getChildCount() - 1;
+        if (mGridCountX > 0) {
+            return lastContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX);
+        } else {
+            return lastContainer.getChildAt(lastRank);
+        }
+    }
+
+    @Override
+    public View iterateOverItems(ItemOperator op) {
+        for (int k = 0 ; k < getChildCount(); k++) {
+            CellLayout page = getPageAt(k);
+            for (int j = 0; j < page.getCountY(); j++) {
+                for (int i = 0; i < page.getCountX(); i++) {
+                    View v = page.getChildAt(i, j);
+                    if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v, this)) {
+                        return v;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public String getAccessibilityDescription() {
+        return String.format(getContext().getString(R.string.folder_opened),
+                mGridCountX, mGridCountY);
+    }
+
+    @Override
+    public void setFocusOnFirstChild() {
+        View firstChild = getCurrentCellLayout().getChildAt(0, 0);
+        if (firstChild != null) {
+            firstChild.requestFocus();
+        }
+    }
+
+    @Override
+    protected void notifyPageSwitchListener() {
+        super.notifyPageSwitchListener();
+        if (mFolder != null) {
+            mFolder.updateTextViewFocus();
+        }
+    }
+
+    @Override
+    public void realTimeReorder(int empty, int target) {
+        int delay = 0;
+        float delayAmount = 30;
+
+        // Animation only happens on the current page.
+        int pageToAnimate = getNextPage();
+
+        int pageT = target / mMaxItemsPerPage;
+        int pagePosT = target % mMaxItemsPerPage;
+
+        if (pageT != pageToAnimate) {
+            Log.e(TAG, "Cannot animate when the target cell is invisible");
+        }
+        int pagePosE = empty % mMaxItemsPerPage;
+        int pageE = empty / mMaxItemsPerPage;
+
+        int startPos, endPos;
+        int moveStart, moveEnd;
+        int direction;
+
+        if (target == empty) {
+            // No animation
+            return;
+        } else if (target > empty) {
+            // Items will move backwards to make room for the empty cell.
+            direction = 1;
+
+            // If empty cell is in a different page, move them instantly.
+            if (pageE < pageToAnimate) {
+                moveStart = empty;
+                // Instantly move the first item in the current page.
+                moveEnd = pageToAnimate * mMaxItemsPerPage;
+                // Animate the 2nd item in the current page, as the first item was already moved to
+                // the last page.
+                startPos = 0;
+            } else {
+                moveStart = moveEnd = -1;
+                startPos = pagePosE;
+            }
+
+            endPos = pagePosT;
+        } else {
+            // The items will move forward.
+            direction = -1;
+
+            if (pageE > pageToAnimate) {
+                // Move the items immediately.
+                moveStart = empty;
+                // Instantly move the last item in the current page.
+                moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1;
+
+                // Animations start with the second last item in the page
+                startPos = mMaxItemsPerPage - 1;
+            } else {
+                moveStart = moveEnd = -1;
+                startPos = pagePosE;
+            }
+
+            endPos = pagePosT;
+        }
+
+        // Instant moving views.
+        while (moveStart != moveEnd) {
+            int rankToMove = moveStart + direction;
+            int p = rankToMove / mMaxItemsPerPage;
+            int pagePos = rankToMove % mMaxItemsPerPage;
+            int x = pagePos % mGridCountX;
+            int y = pagePos / mGridCountX;
+
+            final CellLayout page = getPageAt(p);
+            final View v = page.getChildAt(x, y);
+            if (v != null) {
+                if (pageToAnimate != p) {
+                    page.removeView(v);
+                    addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart);
+                } else {
+                    // Do a fake animation before removing it.
+                    final int newRank = moveStart;
+                    final float oldTranslateX = v.getTranslationX();
+
+                    Runnable endAction = new Runnable() {
+
+                        @Override
+                        public void run() {
+                            mPageChangingViews.remove(v);
+                            v.setTranslationX(oldTranslateX);
+                            ((CellLayout) v.getParent().getParent()).removeView(v);
+                            addViewForRank(v, (ShortcutInfo) v.getTag(), newRank);
+                        }
+                    };
+                    v.animate()
+                        .translationXBy(direction > 0 ? -v.getWidth() : v.getWidth())
+                        .setDuration(REORDER_ANIMATION_DURATION)
+                        .setStartDelay(0)
+                        .withEndAction(endAction);
+                    mPageChangingViews.put(v, endAction);
+                }
+            }
+            moveStart = rankToMove;
+        }
+
+        if ((endPos - startPos) * direction <= 0) {
+            // No animation
+            return;
+        }
+
+        CellLayout page = getPageAt(pageToAnimate);
+        for (int i = startPos; i != endPos; i += direction) {
+            int nextPos = i + direction;
+            View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX);
+            if (v != null) {
+                ((ItemInfo) v.getTag()).rank -= direction;
+            }
+            if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX,
+                    REORDER_ANIMATION_DURATION, delay, true, true)) {
+                delay += delayAmount;
+                delayAmount *= 0.9;
+            }
+        }
+    }
+}