diff options
Diffstat (limited to 'src/com/android/launcher3/Folder.java')
-rw-r--r-- | src/com/android/launcher3/Folder.java | 1114 |
1 files changed, 1114 insertions, 0 deletions
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java new file mode 100644 index 000000000..a7b5c5c54 --- /dev/null +++ b/src/com/android/launcher3/Folder.java @@ -0,0 +1,1114 @@ +/* + * 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.launcher3; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.InputType; +import android.text.Selection; +import android.text.Spannable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ActionMode; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.R; +import com.android.launcher3.FolderInfo.FolderListener; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * Represents a set of icons chosen by the user or generated by the system. + */ +public class Folder extends LinearLayout implements DragSource, View.OnClickListener, + View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, + View.OnFocusChangeListener { + private static final String TAG = "Launcher.Folder"; + + protected DragController mDragController; + protected Launcher mLauncher; + protected FolderInfo mInfo; + + 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 static final int REORDER_ANIMATION_DURATION = 230; + private static final int ON_EXIT_CLOSE_DELAY = 800; + private boolean mRearrangeOnClose = false; + private FolderIcon mFolderIcon; + private int mMaxCountX; + private int mMaxCountY; + private int mMaxNumItems; + private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); + private Drawable mIconDrawable; + boolean mItemsInvalidated = false; + 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(); + private int mFolderNameHeight; + private Rect mTempRect = new Rect(); + private boolean mDragInProgress = false; + private boolean mDeleteFolderOnDropCompleted = false; + private boolean mSuppressFolderDeletion = false; + private boolean mItemAddedBackToSelfViaIcon = false; + FolderEditText mFolderName; + private float mFolderIconPivotX; + private float mFolderIconPivotY; + + private boolean mIsEditingName = false; + private InputMethodManager mInputMethodManager; + + private static String sDefaultFolderName; + private static String sHintText; + + private boolean mDestroyed; + + /** + * 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(); + + Resources res = getResources(); + mMaxCountX = res.getInteger(R.integer.folder_max_count_x); + mMaxCountY = res.getInteger(R.integer.folder_max_count_y); + mMaxNumItems = res.getInteger(R.integer.folder_max_num_items); + if (mMaxCountX < 0 || mMaxCountY < 0 || mMaxNumItems < 0) { + mMaxCountX = LauncherModel.getCellCountX(); + mMaxCountY = LauncherModel.getCellCountY(); + mMaxNumItems = mMaxCountX * mMaxCountY; + } + + mInputMethodManager = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + mExpandDuration = res.getInteger(R.integer.config_folderAnimDuration); + + if (sDefaultFolderName == null) { + sDefaultFolderName = res.getString(R.string.folder_name); + } + if (sHintText == null) { + sHintText = res.getString(R.string.folder_hint_text); + } + mLauncher = (Launcher) context; + // We need this view to be focusable in touch mode so that when text editing of the folder + // name is complete, we have something to focus on, thus hiding the cursor and giving + // reliable behvior when clicking the text field (since it will always gain focus on click). + setFocusableInTouchMode(true); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mContent = (CellLayout) findViewById(R.id.folder_content); + mContent.setGridSize(0, 0); + mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); + mContent.setInvertIfRtl(true); + mFolderName = (FolderEditText) findViewById(R.id.folder_name); + mFolderName.setFolder(this); + mFolderName.setOnFocusChangeListener(this); + + // We find out how tall the text view 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; + mFolderName.measure(measureSpec, measureSpec); + mFolderNameHeight = mFolderName.getMeasuredHeight(); + + // We disable action mode for now since it messes up the view on phones + mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback); + mFolderName.setOnEditorActionListener(this); + mFolderName.setSelectAllOnFocus(true); + mFolderName.setInputType(mFolderName.getInputType() | + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); + } + + private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + public void onDestroyActionMode(ActionMode mode) { + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + }; + + 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(v, item.intent, item); + } + } + + public boolean onLongClick(View v) { + // Return if global dragging is not enabled + if (!mLauncher.isDraggingEnabled()) return true; + + Object tag = v.getTag(); + if (tag instanceof ShortcutInfo) { + ShortcutInfo item = (ShortcutInfo) tag; + if (!v.isInTouchMode()) { + return false; + } + + mLauncher.dismissFolderCling(null); + + mLauncher.getWorkspace().onDragStartedWithItem(v); + mLauncher.getWorkspace().beginDragShared(v, this); + mIconDrawable = ((TextView) v).getCompoundDrawables()[1]; + + mCurrentDragInfo = item; + mEmptyCell[0] = item.cellX; + mEmptyCell[1] = item.cellY; + mCurrentDragView = v; + + mContent.removeView(mCurrentDragView); + mInfo.remove(mCurrentDragInfo); + mDragInProgress = true; + mItemAddedBackToSelfViaIcon = false; + } + return true; + } + + public boolean isEditingName() { + return mIsEditingName; + } + + public void startEditingFolderName() { + mFolderName.setHint(""); + mIsEditingName = true; + } + + public void dismissEditingName() { + mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + doneEditingFolderName(true); + } + + public void doneEditingFolderName(boolean commit) { + mFolderName.setHint(sHintText); + // Convert to a string here to ensure that no other state associated with the text field + // gets saved. + String newTitle = mFolderName.getText().toString(); + mInfo.setTitle(newTitle); + LauncherModel.updateItemInDatabase(mLauncher, mInfo); + + if (commit) { + sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + String.format(getContext().getString(R.string.folder_renamed), newTitle)); + } + // In order to clear the focus from the text field, we set the focus on ourself. This + // ensures that every time the field is clicked, focus is gained, giving reliable behavior. + requestFocus(); + + Selection.setSelection((Spannable) mFolderName.getText(), 0, 0); + mIsEditingName = false; + } + + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + dismissEditingName(); + return true; + } + return false; + } + + public View getEditTextRegion() { + return mFolderName; + } + + public Drawable getDragDrawable() { + return mIconDrawable; + } + + /** + * 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 void setDragController(DragController dragController) { + mDragController = dragController; + } + + void setFolderIcon(FolderIcon icon) { + mFolderIcon = icon; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // When the folder gets focus, we don't want to announce the list of items. + return true; + } + + /** + * @return the FolderInfo object associated with this folder + */ + FolderInfo getInfo() { + return mInfo; + } + + private class GridComparator implements Comparator<ShortcutInfo> { + int mNumCols; + public GridComparator(int numCols) { + mNumCols = numCols; + } + + @Override + public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { + int lhIndex = lhs.cellY * mNumCols + lhs.cellX; + int rhIndex = rhs.cellY * mNumCols + rhs.cellX; + return (lhIndex - rhIndex); + } + } + + private void placeInReadingOrder(ArrayList<ShortcutInfo> items) { + int maxX = 0; + int count = items.size(); + for (int i = 0; i < count; i++) { + ShortcutInfo item = items.get(i); + if (item.cellX > maxX) { + maxX = item.cellX; + } + } + + GridComparator gridComparator = new GridComparator(maxX + 1); + Collections.sort(items, gridComparator); + final int countX = mContent.getCountX(); + for (int i = 0; i < count; i++) { + int x = i % countX; + int y = i / countX; + ShortcutInfo item = items.get(i); + item.cellX = x; + item.cellY = y; + } + } + + void bind(FolderInfo info) { + mInfo = info; + ArrayList<ShortcutInfo> children = info.contents; + ArrayList<ShortcutInfo> overflow = new ArrayList<ShortcutInfo>(); + setupContentForNumItems(children.size()); + placeInReadingOrder(children); + int count = 0; + for (int i = 0; i < children.size(); i++) { + ShortcutInfo child = (ShortcutInfo) children.get(i); + if (!createAndAddShortcut(child)) { + overflow.add(child); + } else { + count++; + } + } + + // We rearrange the items in case there are any empty gaps + setupContentForNumItems(count); + + // If our folder has too many items we prune them from the list. This is an issue + // when upgrading from the old Folders implementation which could contain an unlimited + // number of items. + for (ShortcutInfo item: overflow) { + mInfo.remove(item); + LauncherModel.deleteItemFromDatabase(mLauncher, item); + } + + mItemsInvalidated = true; + updateTextViewFocus(); + mInfo.addListener(this); + + if (!sDefaultFolderName.contentEquals(mInfo.title)) { + mFolderName.setText(mInfo.title); + } else { + mFolderName.setText(""); + } + updateItemLocationsInDatabase(); + } + + /** + * 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 DragLayer)) return; + setScaleX(0.8f); + setScaleY(0.8f); + setAlpha(0f); + mState = STATE_SMALL; + } + + public void animateOpen() { + positionAndSizeAsIcon(); + + if (!(getParent() instanceof DragLayer)) return; + centerAboutIcon(); + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); + final ObjectAnimator oa = + LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); + + oa.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + String.format(getContext().getString(R.string.folder_opened), + mContent.getCountX(), mContent.getCountY())); + mState = STATE_ANIMATING; + } + @Override + public void onAnimationEnd(Animator animation) { + mState = STATE_OPEN; + setLayerType(LAYER_TYPE_NONE, null); + Cling cling = mLauncher.showFirstRunFoldersCling(); + if (cling != null) { + cling.bringToFront(); + } + setFocusOnFirstChild(); + } + }); + oa.setDuration(mExpandDuration); + setLayerType(LAYER_TYPE_HARDWARE, null); + oa.start(); + } + + private void sendCustomAccessibilityEvent(int type, String text) { + AccessibilityManager accessibilityManager = (AccessibilityManager) + getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.isEnabled()) { + AccessibilityEvent event = AccessibilityEvent.obtain(type); + onInitializeAccessibilityEvent(event); + event.getText().add(text); + accessibilityManager.sendAccessibilityEvent(event); + } + } + + private void setFocusOnFirstChild() { + View firstChild = mContent.getChildAt(0, 0); + if (firstChild != null) { + firstChild.requestFocus(); + } + } + + public void animateClosed() { + if (!(getParent() instanceof DragLayer)) return; + PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); + PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); + PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); + final ObjectAnimator oa = + LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); + + oa.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + onCloseComplete(); + setLayerType(LAYER_TYPE_NONE, null); + mState = STATE_SMALL; + } + @Override + public void onAnimationStart(Animator animation) { + sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + getContext().getString(R.string.folder_closed)); + mState = STATE_ANIMATING; + } + }); + oa.setDuration(mExpandDuration); + setLayerType(LAYER_TYPE_HARDWARE, null); + 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(DragObject d) { + final ItemInfo item = (ItemInfo) d.dragInfo; + final int itemType = item.itemType; + return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || + itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && + !isFull()); + } + + 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]; + return true; + } else { + return false; + } + } + + protected boolean createAndAddShortcut(ShortcutInfo item) { + final TextView textView = + (TextView) mInflater.inflate(R.layout.application, 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); + + // We need to check here to verify that the given item's location isn't already occupied + // by another item. + if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0 + || item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) { + // This shouldn't happen, log it. + Log.e(TAG, "Folder order not properly persisted during bind"); + if (!findAndSetEmptyCells(item)) { + return false; + } + } + + CellLayout.LayoutParams lp = + new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); + boolean insert = false; + textView.setOnKeyListener(new FolderKeyEventListener()); + mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); + return true; + } + + public void onDragEnter(DragObject d) { + mPreviousTargetCell[0] = -1; + mPreviousTargetCell[1] = -1; + 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; + int delay = 0; + float delayAmount = 30; + 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, delay, true, true)) { + empty[0] = x; + empty[1] = y; + delay += delayAmount; + delayAmount *= 0.9; + } + } + } + } 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, delay, true, true)) { + empty[0] = x; + empty[1] = y; + delay += delayAmount; + delayAmount *= 0.9; + } + } + } + } + } + + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + public void onDragOver(DragObject d) { + 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 (isLayoutRtl()) { + mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1; + } + + 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) { + completeDragExit(); + } + }; + + public void completeDragExit() { + mLauncher.closeFolder(); + 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(); + } + + public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, + boolean success) { + if (success) { + if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon) { + replaceFolderWithFinalItem(); + } + } else { + setupContentForNumItems(getItemCount()); + // The drag failed, we need to return the item to the folder + mFolderIcon.onDrop(d); + } + + if (target != this) { + if (mOnExitAlarm.alarmPending()) { + mOnExitAlarm.cancelAlarm(); + if (!success) { + mSuppressFolderDeletion = true; + } + completeDragExit(); + } + } + + mDeleteFolderOnDropCompleted = false; + mDragInProgress = false; + mItemAddedBackToSelfViaIcon = false; + mCurrentDragInfo = null; + mCurrentDragView = null; + mSuppressOnAdd = false; + + // Reordering may have occured, and we need to save the new item locations. We do this once + // at the end to prevent unnecessary database operations. + updateItemLocationsInDatabase(); + } + + @Override + public boolean supportsFlingToDelete() { + return true; + } + + public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { + // Do nothing + } + + @Override + public void onFlingToDeleteCompleted() { + // Do nothing + } + + private void updateItemLocationsInDatabase() { + ArrayList<View> list = getItemsInReadingOrder(); + for (int i = 0; i < list.size(); i++) { + View v = list.get(i); + ItemInfo info = (ItemInfo) v.getTag(); + LauncherModel.moveItemInDatabase(mLauncher, info, mInfo.id, 0, + info.cellX, info.cellY); + } + } + + public void notifyDrop() { + if (mDragInProgress) { + mItemAddedBackToSelfViaIcon = true; + } + } + + public boolean isDropEnabled() { + return true; + } + + public DropTarget getDropTargetDelegate(DragObject d) { + return null; + } + + private void setupContentDimensions(int count) { + ArrayList<View> 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 || 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; + } + mContent.setGridSize(countX, countY); + arrangeChildren(list); + } + + public boolean isFull() { + return getItemCount() >= mMaxNumItems; + } + + private void centerAboutIcon() { + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + + int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight() + + mFolderNameHeight; + DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer); + + float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect); + + int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2); + int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2); + int centeredLeft = centerX - width / 2; + int centeredTop = centerY - height / 2; + + int currentPage = mLauncher.getWorkspace().getCurrentPage(); + // In case the workspace is scrolling, we need to use the final scroll to compute + // the folders bounds. + mLauncher.getWorkspace().setFinalScrollForPageChange(currentPage); + // We first fetch the currently visible CellLayoutChildren + CellLayout currentLayout = (CellLayout) mLauncher.getWorkspace().getChildAt(currentPage); + ShortcutAndWidgetContainer boundingLayout = currentLayout.getShortcutsAndWidgets(); + Rect bounds = new Rect(); + parent.getDescendantRectRelativeToSelf(boundingLayout, bounds); + // We reset the workspaces scroll + mLauncher.getWorkspace().resetFinalScrollForPageChange(currentPage); + + // We need to bound the folder to the currently visible CellLayoutChildren + int left = Math.min(Math.max(bounds.left, centeredLeft), + bounds.left + bounds.width() - width); + int top = Math.min(Math.max(bounds.top, centeredTop), + bounds.top + bounds.height() - height); + // If the folder doesn't fit within the bounds, center it about the desired bounds + if (width >= bounds.width()) { + left = bounds.left + (bounds.width() - width) / 2; + } + if (height >= bounds.height()) { + top = bounds.top + (bounds.height() - height) / 2; + } + + int folderPivotX = width / 2 + (centeredLeft - left); + int folderPivotY = height / 2 + (centeredTop - top); + setPivotX(folderPivotX); + setPivotY(folderPivotY); + mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() * + (1.0f * folderPivotX / width)); + mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * + (1.0f * folderPivotY / height)); + + lp.width = width; + lp.height = height; + lp.x = left; + lp.y = top; + } + + float getPivotXForIconAnimation() { + return mFolderIconPivotX; + } + float getPivotYForIconAnimation() { + return mFolderIconPivotY; + } + + private void setupContentForNumItems(int count) { + setupContentDimensions(count); + + DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); + if (lp == null) { + lp = new DragLayer.LayoutParams(0, 0); + lp.customPosition = true; + setLayoutParams(lp); + } + centerAboutIcon(); + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); + int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight() + + mFolderNameHeight; + + int contentWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(), + MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredHeight(), + MeasureSpec.EXACTLY); + mContent.measure(contentWidthSpec, contentHeightSpec); + + mFolderName.measure(contentWidthSpec, + MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); + setMeasuredDimension(width, height); + } + + private void arrangeChildren(ArrayList<View> 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(); + if (info.cellX != vacant[0] || info.cellY != vacant[1]) { + info.cellX = vacant[0]; + info.cellY = vacant[1]; + LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, + info.cellX, info.cellY); + } + boolean insert = false; + mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); + } + mItemsInvalidated = true; + } + + public int getItemCount() { + return mContent.getShortcutsAndWidgets().getChildCount(); + } + + public View getItemAt(int index) { + return mContent.getShortcutsAndWidgets().getChildAt(index); + } + + private void onCloseComplete() { + DragLayer parent = (DragLayer) getParent(); + if (parent != null) { + parent.removeView(this); + } + mDragController.removeDropTarget((DropTarget) this); + clearFocus(); + mFolderIcon.requestFocus(); + + if (mRearrangeOnClose) { + setupContentForNumItems(getItemCount()); + mRearrangeOnClose = false; + } + if (getItemCount() <= 1) { + if (!mDragInProgress && !mSuppressFolderDeletion) { + replaceFolderWithFinalItem(); + } else if (mDragInProgress) { + mDeleteFolderOnDropCompleted = true; + } + } + mSuppressFolderDeletion = false; + } + + private void replaceFolderWithFinalItem() { + // Add the last remaining child to the workspace in place of the folder + Runnable onCompleteRunnable = new Runnable() { + @Override + public void run() { + CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screen); + + View child = null; + // Move the item from the folder to the workspace, in the position of the folder + if (getItemCount() == 1) { + ShortcutInfo finalItem = mInfo.contents.get(0); + child = mLauncher.createShortcut(R.layout.application, cellLayout, + finalItem); + LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, + mInfo.screen, mInfo.cellX, mInfo.cellY); + } + if (getItemCount() <= 1) { + // Remove the folder + LauncherModel.deleteItemFromDatabase(mLauncher, mInfo); + cellLayout.removeView(mFolderIcon); + if (mFolderIcon instanceof DropTarget) { + mDragController.removeDropTarget((DropTarget) mFolderIcon); + } + mLauncher.removeFolder(mInfo); + } + // We add the child after removing the folder to prevent both from existing at + // the same time in the CellLayout. + if (child != null) { + mLauncher.getWorkspace().addInScreen(child, mInfo.container, mInfo.screen, + mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY); + } + } + }; + View finalChild = getItemAt(0); + if (finalChild != null) { + mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); + } + mDestroyed = true; + } + + boolean isDestroyed() { + return mDestroyed; + } + + // This method keeps track of the last item in the folder for the purposes + // of keyboard focus + private void updateTextViewFocus() { + View lastChild = getItemAt(getItemCount() - 1); + getItemAt(getItemCount() - 1); + if (lastChild != null) { + mFolderName.setNextFocusDownId(lastChild.getId()); + mFolderName.setNextFocusRightId(lastChild.getId()); + mFolderName.setNextFocusLeftId(lastChild.getId()); + mFolderName.setNextFocusUpId(lastChild.getId()); + } + } + + 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, currently this is the only path possible, however + // we keep this as a distinct code path. + 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); + if (d.dragView.hasDrawn()) { + mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, mCurrentDragView); + } else { + d.deferDragViewCleanupPostAnimation = false; + mCurrentDragView.setVisibility(VISIBLE); + } + mItemsInvalidated = true; + setupContentDimensions(getItemCount()); + mSuppressOnAdd = true; + } + mInfo.add(item); + } + + // This is used so the item doesn't immediately appear in the folder when added. In one case + // we need to create the illusion that the item isn't added back to the folder yet, to + // to correspond to the animation of the icon back into the folder. This is + public void hideItem(ShortcutInfo info) { + View v = getViewForInfo(info); + v.setVisibility(INVISIBLE); + } + public void showItem(ShortcutInfo info) { + View v = getViewForInfo(info); + v.setVisibility(VISIBLE); + } + + public void onAdd(ShortcutInfo item) { + mItemsInvalidated = true; + // If the item was dropped onto this open folder, we have done the work associated + // with adding the item to the folder, as indicated by mSuppressOnAdd being set + 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 this item is being dragged from this open folder, we have already handled + // 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); + if (mState == STATE_ANIMATING) { + mRearrangeOnClose = true; + } else { + setupContentForNumItems(getItemCount()); + } + if (getItemCount() <= 1) { + replaceFolderWithFinalItem(); + } + } + + private View getViewForInfo(ShortcutInfo item) { + for (int j = 0; j < mContent.getCountY(); j++) { + for (int i = 0; i < mContent.getCountX(); i++) { + View v = mContent.getChildAt(i, j); + if (v.getTag() == item) { + return v; + } + } + } + return null; + } + + public void onItemsChanged() { + updateTextViewFocus(); + } + + public void onTitleChanged(CharSequence title) { + } + + public ArrayList<View> 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; + } + + public void getLocationInDragLayer(int[] loc) { + mLauncher.getDragLayer().getLocationInDragLayer(this, loc); + } + + public void onFocusChange(View v, boolean hasFocus) { + if (v == mFolderName && hasFocus) { + startEditingFolderName(); + } + } +} |