307fe23f12
--> A bunch of stuff breaks when scaling the hotseat. More specifically,
drag and drop animations between hotseat and workspace,
scaling on pick up, folder animations, determination of
item placement. This CL fixes these issues so that the
hotseat or hotseat items are ready to be scaled.
--> For now, using 90% scale factor for 7-inch+ UIs
Change-Id: Iac098409347e76139e4d726a071397b56ac684d2
1119 lines
41 KiB
Java
1119 lines
41 KiB
Java
/*
|
|
* 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 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.launcher.R;
|
|
import com.android.launcher2.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 ObjectAnimator mOpenCloseAnimator;
|
|
|
|
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);
|
|
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 = mOpenCloseAnimator =
|
|
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);
|
|
buildLayer();
|
|
post(new Runnable() {
|
|
public void run() {
|
|
// Check if the animator changed in the meantime
|
|
if (oa != mOpenCloseAnimator)
|
|
return;
|
|
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 = mOpenCloseAnimator =
|
|
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);
|
|
buildLayer();
|
|
post(new Runnable() {
|
|
public void run() {
|
|
// Check if the animator changed in the meantime
|
|
if (oa != mOpenCloseAnimator)
|
|
return;
|
|
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 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 (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 {
|
|
// The drag failed, we need to return the item to the folder
|
|
mFolderIcon.onDrop(d);
|
|
|
|
// We're going to trigger a "closeFolder" which may occur before this item has
|
|
// been added back to the folder -- this could cause the folder to be deleted
|
|
if (mOnExitAlarm.alarmPending()) {
|
|
mSuppressFolderDeletion = true;
|
|
}
|
|
}
|
|
|
|
if (target != this) {
|
|
if (mOnExitAlarm.alarmPending()) {
|
|
mOnExitAlarm.cancelAlarm();
|
|
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);
|
|
}
|
|
|
|
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() {
|
|
return getItemsInReadingOrder(true);
|
|
}
|
|
|
|
public ArrayList<View> getItemsInReadingOrder(boolean includeCurrentDragItem) {
|
|
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) {
|
|
ShortcutInfo info = (ShortcutInfo) v.getTag();
|
|
if (info != mCurrentDragInfo || includeCurrentDragItem) {
|
|
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();
|
|
}
|
|
}
|
|
}
|