Refactoring folder content
> Refactoring the CellLayout in folder with a custom view so
that it can be replaced easily with a scrollable paged view.
> Moving some methods from the folder to this new view which
assume a single page layout for the folder
> Changing folder from LinearLayout to FrameLayout to properly
handle focus traversal in case of multi-page folders
Change-Id: I073c00b995488f9f5d8123b00357e094ca2cec7c
diff --git a/src/com/android/launcher3/FolderCellLayout.java b/src/com/android/launcher3/FolderCellLayout.java
new file mode 100644
index 0000000..66f5224
--- /dev/null
+++ b/src/com/android/launcher3/FolderCellLayout.java
@@ -0,0 +1,285 @@
+package com.android.launcher3;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import java.util.ArrayList;
+
+public class FolderCellLayout extends CellLayout {
+
+ private static final int REORDER_ANIMATION_DURATION = 230;
+ private static final int START_VIEW_REORDER_DELAY = 30;
+ private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
+
+ private static final int[] sTempPosArray = new int[2];
+
+ private final FolderKeyEventListener mKeyListener = new FolderKeyEventListener();
+ private final LayoutInflater mInflater;
+ private final IconCache mIconCache;
+
+ private final int mMaxCountX;
+ private final int mMaxCountY;
+ private final int mMaxNumItems;
+
+ // Indicates the last number of items used to set up the grid size
+ private int mAllocatedContentSize;
+
+ private Folder mFolder;
+ private FocusIndicatorView mFocusIndicatorView;
+
+ public FolderCellLayout(Context context) {
+ this(context, null);
+ }
+
+ public FolderCellLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FolderCellLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ LauncherAppState app = LauncherAppState.getInstance();
+ DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
+ mMaxCountX = (int) grid.numColumns;
+ mMaxCountY = (int) grid.numRows;
+ mMaxNumItems = mMaxCountX * mMaxCountY;
+
+ mInflater = LayoutInflater.from(context);
+ mIconCache = app.getIconCache();
+ }
+
+ public void setFolder(Folder folder) {
+ mFolder = folder;
+ mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator);
+ }
+
+ /**
+ * 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} & {@link #mMaxCountY}.
+ */
+ private void setupContentDimensions(int count) {
+ mAllocatedContentSize = count;
+ int countX = getCountX();
+ int countY = getCountY();
+ boolean done = false;
+
+ while (!done) {
+ int oldCountX = countX;
+ int oldCountY = countY;
+ if (countX * countY < count) {
+ // Current grid is too small, expand it
+ if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) {
+ countX++;
+ } else if (countY < mMaxCountY) {
+ countY++;
+ }
+ if (countY == 0) countY++;
+ } else if ((countY - 1) * countX >= count && countY >= countX) {
+ countY = Math.max(0, countY - 1);
+ } else if ((countX - 1) * countY >= count) {
+ countX = Math.max(0, countX - 1);
+ }
+ done = countX == oldCountX && countY == oldCountY;
+ }
+ setGridSize(countX, countY);
+ }
+
+ /**
+ * Binds items to the layout.
+ * @return list of items that could not be bound, probably because we hit the max size limit.
+ */
+ public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) {
+ ArrayList<ShortcutInfo> extra = new ArrayList<ShortcutInfo>();
+ setupContentDimensions(Math.min(items.size(), mMaxNumItems));
+
+ int countX = getCountX();
+ int rank = 0;
+ for (ShortcutInfo item : items) {
+ if (rank >= mMaxNumItems) {
+ extra.add(item);
+ continue;
+ }
+
+ item.rank = rank;
+ item.cellX = rank % countX;
+ item.cellY = rank / countX;
+ addNewView(item);
+ rank++;
+ }
+ return extra;
+ }
+
+ /**
+ * Create space for a new item at the end, and returns the rank for that item.
+ * Resizes the content if necessary.
+ */
+ public int allocateNewLastItemRank() {
+ int rank = getItemCount();
+ mFolder.rearrangeChildren(rank + 1);
+ return rank;
+ }
+
+ /**
+ * Adds the new item to the end of the grid. Resizes the content if necessary.
+ */
+ public View createAndAddShortcutToEnd(ShortcutInfo item) {
+ int rank = allocateNewLastItemRank();
+ return createAndAddViewForRank(item, rank);
+ }
+
+ public View createAndAddViewForRank(ShortcutInfo item, int rank) {
+ updateItemXY(item, rank);
+ return addNewView(item);
+ }
+
+ /**
+ * Adds the {@param view} to the layout based on {@param rank} and updated the position
+ * related attributes. It assumes that {@param item} is already attached to the view.
+ */
+ public void addViewForRank(View view, ShortcutInfo item, int rank) {
+ updateItemXY(item, rank);
+ CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
+ lp.cellX = item.cellX;
+ lp.cellY = item.cellY;
+ addViewToCellLayout(view, -1, (int) item.id, lp, true);
+ }
+
+ /**
+ * Updates the item cellX and cellY position
+ */
+ private void updateItemXY(ShortcutInfo item, int rank) {
+ item.rank = rank;
+ int countX = getCountX();
+ item.cellX = rank % countX;
+ item.cellY = rank / countX;
+ }
+
+ private View addNewView(ShortcutInfo item) {
+ final BubbleTextView textView = (BubbleTextView) mInflater.inflate(
+ R.layout.folder_application, 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);
+ addViewToCellLayout(textView, -1, (int)item.id, lp, true);
+ return textView;
+ }
+
+ /**
+ * Refer {@link #findNearestArea(int, int, int, int, View, boolean, int[])}
+ *
+ * @return The rank of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ public int findNearestArea(int pixelX, int pixelY, int spanX, int spanY) {
+ findNearestArea(pixelX, pixelY, spanX, spanY, null, false, sTempPosArray);
+ if (mFolder.isLayoutRtl()) {
+ sTempPosArray[0] = getCountX() - sTempPosArray[0] - 1;
+ }
+
+ // Convert this position to rank.
+ return Math.min(mAllocatedContentSize - 1,
+ sTempPosArray[1] * getCountX() + sTempPosArray[0]);
+ }
+
+ public boolean isFull() {
+ return getItemCount() >= mMaxNumItems;
+ }
+
+ public int getItemCount() {
+ return getShortcutsAndWidgets().getChildCount();
+ }
+
+ /**
+ * Updates position and rank of all the children in the view based.
+ * @param list the ordered list of children.
+ * @param itemCount if greater than the total children count, empty spaces are left at the end.
+ */
+ public void arrangeChildren(ArrayList<View> list, int itemCount) {
+ setupContentDimensions(itemCount);
+ removeAllViews();
+
+ int newX, newY;
+ int rank = 0;
+ int countX = getCountX();
+ for (View v : list) {
+ CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
+ newX = rank % countX;
+ newY = rank / countX;
+ 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 ++;
+ addViewToCellLayout(v, -1, (int)info.id, lp, true);
+ }
+ }
+
+ /**
+ * Reorders the items such that the {@param empty} spot moves to {@param target}
+ */
+ public void realTimeReorder(int empty, int target) {
+ boolean wrap;
+ int startX;
+ int endX;
+ int startY;
+ int delay = 0;
+ float delayAmount = START_VIEW_REORDER_DELAY;
+
+ int countX = getCountX();
+ int emptyX = empty % getCountX();
+ int emptyY = empty / countX;
+
+ int targetX = target % countX;
+ int targetY = target / countX;
+
+ if (target > empty) {
+ wrap = emptyX == countX - 1;
+ startY = wrap ? emptyY + 1 : emptyY;
+ for (int y = startY; y <= targetY; y++) {
+ startX = y == emptyY ? emptyX + 1 : 0;
+ endX = y < targetY ? countX - 1 : targetX;
+ for (int x = startX; x <= endX; x++) {
+ View v = getChildAt(x,y);
+ if (animateChildToPosition(v, emptyX, emptyY,
+ REORDER_ANIMATION_DURATION, delay, true, true)) {
+ emptyX = x;
+ emptyY = y;
+ delay += delayAmount;
+ delayAmount *= VIEW_REORDER_DELAY_FACTOR;
+ }
+ }
+ }
+ } else {
+ wrap = emptyX == 0;
+ startY = wrap ? emptyY - 1 : emptyY;
+ for (int y = startY; y >= targetY; y--) {
+ startX = y == emptyY ? emptyX - 1 : countX - 1;
+ endX = y > targetY ? 0 : targetX;
+ for (int x = startX; x >= endX; x--) {
+ View v = getChildAt(x,y);
+ if (animateChildToPosition(v, emptyX, emptyY,
+ REORDER_ANIMATION_DURATION, delay, true, true)) {
+ emptyX = x;
+ emptyY = y;
+ delay += delayAmount;
+ delayAmount *= VIEW_REORDER_DELAY_FACTOR;
+ }
+ }
+ }
+ }
+ }
+}