/* * 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. */ package com.android.launcher2; import java.util.ArrayList; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.widget.AdapterView; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import com.android.launcher.R; import com.android.launcher2.FolderInfo.FolderListener; /** * Represents a set of icons chosen by the user or generated by the system. */ public class Folder extends LinearLayout implements DragSource, OnItemLongClickListener, OnItemClickListener, OnClickListener, View.OnLongClickListener, DropTarget, FolderListener { protected DragController mDragController; protected Launcher mLauncher; protected FolderInfo mInfo; /** * Which item is being dragged */ protected ShortcutInfo mDragItem; private static final String TAG = "Launcher.Folder"; static final int STATE_NONE = -1; static final int STATE_SMALL = 0; static final int STATE_ANIMATING = 1; static final int STATE_OPEN = 2; private int mExpandDuration; protected CellLayout mContent; private final LayoutInflater mInflater; private final IconCache mIconCache; private int mState = STATE_NONE; private int[] mDragItemPosition = new int[2]; private static final int FULL_GROW = 0; private static final int PARTIAL_GROW = 1; private int mMode = PARTIAL_GROW; private boolean mRearrangeOnClose = false; private FolderIcon mFolderIcon; private int mMaxCountX; private int mMaxCountY; private Rect mNewSize = new Rect(); private ArrayList mItemsInReadingOrder = new ArrayList(); boolean mItemsInvalidated = false; /** * Used to inflate the Workspace from XML. * * @param context The application's context. * @param attrs The attribtues set containing the Workspace's customization values. */ public Folder(Context context, AttributeSet attrs) { super(context, attrs); setAlwaysDrawnWithCacheEnabled(false); mInflater = LayoutInflater.from(context); mIconCache = ((LauncherApplication)context.getApplicationContext()).getIconCache(); mExpandDuration = getResources().getInteger(R.integer.config_folderAnimDuration); mMaxCountX = LauncherModel.getCellCountX() - 1; mMaxCountY = LauncherModel.getCellCountY() - 1; } @Override protected void onFinishInflate() { super.onFinishInflate(); mContent = (CellLayout) findViewById(R.id.folder_content); mContent.setGridSize(0, 0); mContent.enableHardwareLayers(); } public void onItemClick(AdapterView parent, View v, int position, long id) { ShortcutInfo app = (ShortcutInfo) parent.getItemAtPosition(position); int[] pos = new int[2]; v.getLocationOnScreen(pos); app.intent.setSourceBounds(new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight())); mLauncher.startActivitySafely(app.intent, app); } public void onClick(View v) { Object tag = v.getTag(); if (tag instanceof ShortcutInfo) { // refactor this code from Folder ShortcutInfo item = (ShortcutInfo) tag; int[] pos = new int[2]; v.getLocationOnScreen(pos); item.intent.setSourceBounds(new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight())); mLauncher.startActivitySafely(item.intent, item); } } 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; } mLauncher.getWorkspace().onDragStartedWithItem(v); mDragController.startDrag(v, this, item, DragController.DRAG_ACTION_COPY); mDragItemPosition[0] = item.cellX; mDragItemPosition[1] = item.cellY; mInfo.remove(item); mDragItem = item; } else { mLauncher.closeFolder(this); mLauncher.showRenameDialog(mInfo); } return true; } /** * We need to handle touch events to prevent them from falling through to the workspace below. */ @Override public boolean onTouchEvent(MotionEvent ev) { return true; } public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { if (!view.isInTouchMode()) { return false; } ShortcutInfo app = (ShortcutInfo) parent.getItemAtPosition(position); mDragController.startDrag(view, this, app, DragController.DRAG_ACTION_COPY); mLauncher.closeFolder(this); mDragItem = app; return true; } public void setDragController(DragController dragController) { mDragController = dragController; } public void onDragViewVisible() { } void setLauncher(Launcher launcher) { mLauncher = launcher; } void setFolderIcon(FolderIcon icon) { mFolderIcon = icon; } /** * @return the FolderInfo object associated with this folder */ FolderInfo getInfo() { return mInfo; } void onOpen() { // When the folder opens, we need to refresh the GridView's selection by // forcing a layout // TODO: find out if this is still necessary mContent.requestLayout(); requestFocus(); } void onClose() { final Workspace workspace = mLauncher.getWorkspace(); workspace.getChildAt(workspace.getCurrentPage()).requestFocus(); } void bind(FolderInfo info) { mInfo = info; ArrayList children = info.contents; setupContentForNumItems(children.size()); for (int i = 0; i < children.size(); i++) { ShortcutInfo child = (ShortcutInfo) children.get(i); createAndAddShortcut(child); } mItemsInvalidated = true; mInfo.addListener(this); } /** * Creates a new UserFolder, inflated from R.layout.user_folder. * * @param context The application's context. * * @return A new UserFolder. */ static Folder fromXml(Context context) { return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null); } /** * This method is intended to make the UserFolder to be visually identical in size and position * to its associated FolderIcon. This allows for a seamless transition into the expanded state. */ private void positionAndSizeAsIcon() { if (!(getParent() instanceof CellLayoutChildren)) return; CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams(); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); if (mMode == PARTIAL_GROW) { setScaleX(0.8f); setScaleY(0.8f); setAlpha(0f); } else { lp.width = iconLp.width; lp.height = iconLp.height; lp.x = iconLp.x; lp.y = iconLp.y; mContent.setAlpha(0); } mState = STATE_SMALL; } public void animateOpen() { if (mState != STATE_SMALL) { positionAndSizeAsIcon(); } if (!(getParent() instanceof CellLayoutChildren)) return; ObjectAnimator oa; CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); centerAboutIcon(); if (mMode == PARTIAL_GROW) { PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); } else { PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", mNewSize.width()); PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", mNewSize.height()); PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", mNewSize.left); PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", mNewSize.top); oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y); oa.addUpdateListener(new AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { requestLayout(); } }); PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f); ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha); alphaOa.setDuration(mExpandDuration); alphaOa.setInterpolator(new AccelerateInterpolator(2.0f)); alphaOa.start(); } oa.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mState = STATE_ANIMATING; } @Override public void onAnimationEnd(Animator animation) { mState = STATE_OPEN; } }); oa.setDuration(mExpandDuration); oa.start(); } public void animateClosed() { if (!(getParent() instanceof CellLayoutChildren)) return; CellLayoutChildren clc = (CellLayoutChildren) getParent(); final CellLayout cellLayout = (CellLayout) clc.getParent(); ObjectAnimator oa; if (mMode == PARTIAL_GROW) { PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); } else { CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams(); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", iconLp.width); PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", iconLp.height); PropertyValuesHolder x = PropertyValuesHolder.ofInt("x",iconLp.x); PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", iconLp.y); oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y); oa.addUpdateListener(new AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator animation) { requestLayout(); } }); PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f); ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha); alphaOa.setDuration(mExpandDuration); alphaOa.setInterpolator(new DecelerateInterpolator(2.0f)); alphaOa.start(); } oa.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { onCloseComplete(); cellLayout.removeViewWithoutMarkingCells(Folder.this); mState = STATE_SMALL; } @Override public void onAnimationStart(Animator animation) { mState = STATE_ANIMATING; } }); oa.setDuration(mExpandDuration); oa.start(); } void notifyDataSetChanged() { // recreate all the children if the data set changes under us. We may want to do this more // intelligently (ie just removing the views that should no longer exist) mContent.removeAllViewsInLayout(); bind(mInfo); } public boolean acceptDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { final ItemInfo item = (ItemInfo) dragInfo; final int itemType = item.itemType; return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && !isFull()); } public void onDrop(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { ShortcutInfo item; if (dragInfo instanceof ApplicationInfo) { // Came from all apps -- make a copy item = ((ApplicationInfo)dragInfo).makeShortcut(); item.spanX = 1; item.spanY = 1; } else { item = (ShortcutInfo)dragInfo; } mInfo.add(item); LauncherModel.addOrMoveItemInDatabase(mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); } protected boolean findAndSetEmptyCells(ShortcutInfo item) { int[] emptyCell = new int[2]; if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) { item.cellX = emptyCell[0]; item.cellY = emptyCell[1]; LauncherModel.addOrMoveItemInDatabase( mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); return true; } else { return false; } } protected void createAndAddShortcut(ShortcutInfo item) { final TextView textView = (TextView) mInflater.inflate(R.layout.application_boxed, this, false); textView.setCompoundDrawablesWithIntrinsicBounds(null, new FastBitmapDrawable(item.getIcon(mIconCache)), null, null); textView.setText(item.title); textView.setTag(item); textView.setOnClickListener(this); textView.setOnLongClickListener(this); CellLayout.LayoutParams lp = new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); boolean insert = false; mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); } public void onDragEnter(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { mContent.onDragEnter(); } public void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { float[] r = mapPointFromScreenToContent(x, y, null); mContent.visualizeDropLocation(null, null, (int) r[0], (int) r[1], 1, 1); } public void onDragExit(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { 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, Object dragInfo, boolean success) { } public boolean isDropEnabled() { return true; } public DropTarget getDropTargetDelegate(DragSource source, int x, int y, int xOffset, int yOffset, DragView dragView, Object dragInfo) { return null; } private void setupContentDimension(int count) { ArrayList list = getItemsInReadingOrder(); int countX = mContent.getCountX(); int countY = mContent.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 && 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; } mContent.setGridSize(countX, countY); arrangeChildren(list); } public boolean isFull() { return getItemCount() >= mMaxCountX * mMaxCountY; } private void centerAboutIcon() { CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams(); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight(); int centerX = iconLp.x + iconLp.width / 2; int centerY = iconLp.y + iconLp.height / 2; int centeredLeft = centerX - width / 2; int centeredTop = centerY - height / 2; CellLayoutChildren clc = (CellLayoutChildren) getParent(); int parentWidth = 0; int parentHeight = 0; if (clc != null) { parentWidth = clc.getMeasuredWidth(); parentHeight = clc.getMeasuredHeight(); } int left = Math.min(Math.max(0, centeredLeft), parentWidth - width); int top = Math.min(Math.max(0, centeredTop), parentHeight - height); int folderPivotX = width / 2 + (centeredLeft - left); int folderPivotY = height / 2 + (centeredTop - top); setPivotX(folderPivotX); setPivotY(folderPivotY); int folderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() * (1.0f * folderPivotX / width)); int folderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * (1.0f * folderPivotY / height)); mFolderIcon.setPivotX(folderIconPivotX); mFolderIcon.setPivotY(folderIconPivotY); if (mMode == PARTIAL_GROW) { lp.width = width; lp.height = height; lp.x = left; lp.y = top; } else { mNewSize.set(left, top, left + width, top + height); } } private void setupContentForNumItems(int count) { setupContentDimension(count); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); if (lp == null) { lp = new CellLayout.LayoutParams(0, 0, -1, -1); lp.isLockedToGrid = false; setLayoutParams(lp); } centerAboutIcon(); } private void arrangeChildren(ArrayList list) { int[] vacant = new int[2]; if (list == null) { list = getItemsInReadingOrder(); } mContent.removeAllViews(); for (int i = 0; i < list.size(); i++) { View v = list.get(i); mContent.getVacantCell(vacant, 1, 1); CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); lp.cellX = vacant[0]; lp.cellY = vacant[1]; ItemInfo info = (ItemInfo) v.getTag(); info.cellX = vacant[0]; info.cellY = vacant[1]; boolean insert = false; mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, info.cellX, info.cellY); } 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); } public int getItemCount() { return mContent.getChildrenLayout().getChildCount(); } public View getItemAt(int index) { return mContent.getChildrenLayout().getChildAt(index); } private void onCloseComplete() { if (mRearrangeOnClose) { setupContentForNumItems(getItemCount()); mRearrangeOnClose = false; } } public void onRemove(ShortcutInfo item) { mItemsInvalidated = true; View v = mContent.getChildAt(mDragItemPosition[0], mDragItemPosition[1]); mContent.removeView(v); if (mState == STATE_ANIMATING) { mRearrangeOnClose = true; } else { setupContentForNumItems(getItemCount()); } } public ArrayList getItemsInReadingOrder() { if (mItemsInvalidated) { mItemsInReadingOrder.clear(); for (int j = 0; j < mContent.getCountY(); j++) { for (int i = 0; i < mContent.getCountX(); i++) { View v = mContent.getChildAt(i, j); if (v != null) { mItemsInReadingOrder.add(v); } } } mItemsInvalidated = false; } return mItemsInReadingOrder; } }