/** * 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.view.LayoutInflater; import android.view.View; import com.android.launcher3.Workspace.ItemOperator; import java.util.ArrayList; public class FolderCellLayout extends CellLayout implements Folder.FolderContent { 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(); setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); setInvertIfRtl(true); } @Override 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); } @Override public ArrayList bindItems(ArrayList items) { ArrayList extra = new ArrayList(); 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; } @Override public int allocateRankForNewItem(ShortcutInfo info) { int rank = getItemCount(); mFolder.rearrangeChildren(rank + 1); return rank; } @Override public View createAndAddViewForRank(ShortcutInfo item, int rank) { updateItemXY(item, rank); return addNewView(item); } @Override 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, mFolder.mLauncher.getViewIdForItem(item), lp, true); } @Override public void removeItem(View v) { removeView(v); } /** * 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, mFolder.mLauncher.getViewIdForItem(item), lp, true); return textView; } /** * Refer {@link #findNearestArea(int, int, int, int, View, boolean, int[])} */ @Override public int findNearestArea(int pixelX, int pixelY) { findNearestArea(pixelX, pixelY, 1, 1, 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]); } @Override public boolean isFull() { return getItemCount() >= mMaxNumItems; } @Override public int getItemCount() { return getShortcutsAndWidgets().getChildCount(); } @Override public void arrangeChildren(ArrayList 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, mFolder.mLauncher.getViewIdForItem(info), lp, true); } } @Override public View iterateOverItems(ItemOperator op) { for (int j = 0; j < getCountY(); j++) { for (int i = 0; i < getCountX(); i++) { View v = 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), getCountX(), getCountY()); } @Override public void setFocusOnFirstChild() { View firstChild = getChildAt(0, 0); if (firstChild != null) { firstChild.requestFocus(); } } @Override public View getLastItem() { int lastRank = getShortcutsAndWidgets().getChildCount() - 1; // count can be zero if the folder is not yet laid out. int count = getCountX(); if (count > 0) { return getShortcutsAndWidgets().getChildAt(lastRank % count, lastRank / count); } else { return getShortcutsAndWidgets().getChildAt(lastRank); } } @Override 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; } } } } } }