mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-20 19:38:21 +00:00
1155 lines
41 KiB
Java
1155 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.launcher3;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ObjectAnimator;
|
|
import android.animation.PropertyValuesHolder;
|
|
import android.annotation.TargetApi;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.PointF;
|
|
import android.graphics.Rect;
|
|
import android.os.Build;
|
|
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.animation.AccelerateInterpolator;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.launcher3.FolderInfo.FolderListener;
|
|
import com.android.launcher3.Workspace.ItemOperator;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
|
|
/**
|
|
* 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";
|
|
|
|
/**
|
|
* We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this
|
|
* results in CellLayout being measured as UNSPECIFIED, which it does not support.
|
|
*/
|
|
private static final int MIN_CONTENT_DIMEN = 5;
|
|
|
|
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 static final int REORDER_DELAY = 250;
|
|
private static final int ON_EXIT_CLOSE_DELAY = 400;
|
|
private static final Rect sTempRect = new Rect();
|
|
|
|
private static String sDefaultFolderName;
|
|
private static String sHintText;
|
|
|
|
private final Alarm mReorderAlarm = new Alarm();
|
|
private final Alarm mOnExitAlarm = new Alarm();
|
|
|
|
private final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
|
|
|
|
private final int mExpandDuration;
|
|
private final int mMaterialExpandDuration;
|
|
private final int mMaterialExpandStagger;
|
|
|
|
private final InputMethodManager mInputMethodManager;
|
|
|
|
protected final Launcher mLauncher;
|
|
protected DragController mDragController;
|
|
protected FolderInfo mInfo;
|
|
|
|
private FolderIcon mFolderIcon;
|
|
|
|
private FolderContent mContent;
|
|
private View mContentWrapper;
|
|
FolderEditText mFolderName;
|
|
|
|
private View mBottomContent;
|
|
private int mBottomContentHeight;
|
|
|
|
// Cell ranks used for drag and drop
|
|
private int mTargetRank, mPrevTargetRank, mEmptyCellRank;
|
|
|
|
private int mState = STATE_NONE;
|
|
private boolean mRearrangeOnClose = false;
|
|
boolean mItemsInvalidated = false;
|
|
private ShortcutInfo mCurrentDragInfo;
|
|
private View mCurrentDragView;
|
|
private boolean mIsExternalDrag;
|
|
boolean mSuppressOnAdd = false;
|
|
private boolean mDragInProgress = false;
|
|
private boolean mDeleteFolderOnDropCompleted = false;
|
|
private boolean mSuppressFolderDeletion = false;
|
|
private boolean mItemAddedBackToSelfViaIcon = false;
|
|
private float mFolderIconPivotX;
|
|
private float mFolderIconPivotY;
|
|
private boolean mIsEditingName = false;
|
|
|
|
private boolean mDestroyed;
|
|
|
|
private Runnable mDeferredAction;
|
|
private boolean mDeferDropAfterUninstall;
|
|
private boolean mUninstallSuccessful;
|
|
|
|
/**
|
|
* Used to inflate the Workspace from XML.
|
|
*
|
|
* @param context The application's context.
|
|
* @param attrs The attributes set containing the Workspace's customization values.
|
|
*/
|
|
public Folder(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
setAlwaysDrawnWithCacheEnabled(false);
|
|
mInputMethodManager = (InputMethodManager)
|
|
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
|
|
Resources res = getResources();
|
|
mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration);
|
|
mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration);
|
|
mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger);
|
|
|
|
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 behavior when clicking the text field (since it will always gain focus on click).
|
|
setFocusableInTouchMode(true);
|
|
}
|
|
|
|
@Override
|
|
protected void onFinishInflate() {
|
|
super.onFinishInflate();
|
|
mContentWrapper = findViewById(R.id.folder_content_wrapper);
|
|
mContent = (FolderContent) findViewById(R.id.folder_content);
|
|
mContent.setFolder(this);
|
|
|
|
mFolderName = (FolderEditText) findViewById(R.id.folder_name);
|
|
mFolderName.setFolder(this);
|
|
mFolderName.setOnFocusChangeListener(this);
|
|
|
|
// 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);
|
|
|
|
// We only have the folder name at the bottom for now
|
|
mBottomContent = mFolderName;
|
|
// We find out how tall the bottom content 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;
|
|
mBottomContent.measure(measureSpec, measureSpec);
|
|
mBottomContentHeight = mBottomContent.getMeasuredHeight();
|
|
}
|
|
|
|
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) {
|
|
mLauncher.onClick(v);
|
|
}
|
|
}
|
|
|
|
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.getWorkspace().beginDragShared(v, this);
|
|
|
|
mCurrentDragInfo = item;
|
|
mEmptyCellRank = item.rank;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
public 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;
|
|
}
|
|
|
|
void bind(FolderInfo info) {
|
|
mInfo = info;
|
|
ArrayList<ShortcutInfo> children = info.contents;
|
|
Collections.sort(children, Utilities.RANK_COMPARATOR);
|
|
|
|
ArrayList<ShortcutInfo> overflow = mContent.bindItems(children);
|
|
|
|
// 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);
|
|
}
|
|
|
|
DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
|
|
if (lp == null) {
|
|
lp = new DragLayer.LayoutParams(0, 0);
|
|
lp.customPosition = true;
|
|
setLayoutParams(lp);
|
|
}
|
|
centerAboutIcon();
|
|
|
|
mItemsInvalidated = true;
|
|
updateTextViewFocus();
|
|
mInfo.addListener(this);
|
|
|
|
if (!sDefaultFolderName.contentEquals(mInfo.title)) {
|
|
mFolderName.setText(mInfo.title);
|
|
} else {
|
|
mFolderName.setText("");
|
|
}
|
|
|
|
// In case any children didn't come across during loading, clean up the folder accordingly
|
|
mFolderIcon.post(new Runnable() {
|
|
public void run() {
|
|
if (getItemCount() <= 1) {
|
|
replaceFolderWithFinalItem();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
private void prepareReveal() {
|
|
setScaleX(1f);
|
|
setScaleY(1f);
|
|
setAlpha(1f);
|
|
mState = STATE_SMALL;
|
|
}
|
|
|
|
public void animateOpen() {
|
|
if (!(getParent() instanceof DragLayer)) return;
|
|
|
|
Animator openFolderAnim = null;
|
|
final Runnable onCompleteRunnable;
|
|
if (!Utilities.isLmpOrAbove()) {
|
|
positionAndSizeAsIcon();
|
|
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.setDuration(mExpandDuration);
|
|
openFolderAnim = oa;
|
|
|
|
setLayerType(LAYER_TYPE_HARDWARE, null);
|
|
onCompleteRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
setLayerType(LAYER_TYPE_NONE, null);
|
|
}
|
|
};
|
|
} else {
|
|
prepareReveal();
|
|
centerAboutIcon();
|
|
|
|
int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
|
|
int height = getFolderHeight();
|
|
|
|
float transX = - 0.075f * (width / 2 - getPivotX());
|
|
float transY = - 0.075f * (height / 2 - getPivotY());
|
|
setTranslationX(transX);
|
|
setTranslationY(transY);
|
|
PropertyValuesHolder tx = PropertyValuesHolder.ofFloat("translationX", transX, 0);
|
|
PropertyValuesHolder ty = PropertyValuesHolder.ofFloat("translationY", transY, 0);
|
|
|
|
int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX());
|
|
int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY());
|
|
float radius = (float) Math.sqrt(rx * rx + ry * ry);
|
|
AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
|
|
Animator reveal = LauncherAnimUtils.createCircularReveal(this, (int) getPivotX(),
|
|
(int) getPivotY(), 0, radius);
|
|
reveal.setDuration(mMaterialExpandDuration);
|
|
reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
|
|
|
|
mContentWrapper.setAlpha(0f);
|
|
Animator iconsAlpha = LauncherAnimUtils.ofFloat(mContentWrapper, "alpha", 0f, 1f);
|
|
iconsAlpha.setDuration(mMaterialExpandDuration);
|
|
iconsAlpha.setStartDelay(mMaterialExpandStagger);
|
|
iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
|
|
|
|
mBottomContent.setAlpha(0f);
|
|
Animator textAlpha = LauncherAnimUtils.ofFloat(mBottomContent, "alpha", 0f, 1f);
|
|
textAlpha.setDuration(mMaterialExpandDuration);
|
|
textAlpha.setStartDelay(mMaterialExpandStagger);
|
|
textAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
|
|
|
|
Animator drift = LauncherAnimUtils.ofPropertyValuesHolder(this, tx, ty);
|
|
drift.setDuration(mMaterialExpandDuration);
|
|
drift.setStartDelay(mMaterialExpandStagger);
|
|
drift.setInterpolator(new LogDecelerateInterpolator(60, 0));
|
|
|
|
anim.play(drift);
|
|
anim.play(iconsAlpha);
|
|
anim.play(textAlpha);
|
|
anim.play(reveal);
|
|
|
|
openFolderAnim = anim;
|
|
|
|
mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null);
|
|
onCompleteRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
|
|
}
|
|
};
|
|
}
|
|
openFolderAnim.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
|
|
mContent.getAccessibilityDescription());
|
|
mState = STATE_ANIMATING;
|
|
}
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mState = STATE_OPEN;
|
|
|
|
if (onCompleteRunnable != null) {
|
|
onCompleteRunnable.run();
|
|
}
|
|
|
|
mContent.setFocusOnFirstChild();
|
|
}
|
|
});
|
|
openFolderAnim.start();
|
|
|
|
// Make sure the folder picks up the last drag move even if the finger doesn't move.
|
|
if (mDragController.isDragging()) {
|
|
mDragController.forceTouchMove();
|
|
}
|
|
}
|
|
|
|
public void beginExternalDrag(ShortcutInfo item) {
|
|
mCurrentDragInfo = item;
|
|
mEmptyCellRank = mContent.allocateNewLastItemRank();
|
|
mIsExternalDrag = true;
|
|
mDragInProgress = true;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
public void onDragEnter(DragObject d) {
|
|
mPrevTargetRank = -1;
|
|
mOnExitAlarm.cancelAlarm();
|
|
}
|
|
|
|
OnAlarmListener mReorderAlarmListener = new OnAlarmListener() {
|
|
public void onAlarm(Alarm alarm) {
|
|
mContent.realTimeReorder(mEmptyCellRank, mTargetRank);
|
|
mEmptyCellRank = mTargetRank;
|
|
}
|
|
};
|
|
|
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
|
public boolean isLayoutRtl() {
|
|
return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
|
|
}
|
|
|
|
public void onDragOver(DragObject d) {
|
|
final float[] r = d.getVisualCenter(null);
|
|
r[0] -= getPaddingLeft();
|
|
r[1] -= getPaddingTop();
|
|
|
|
mTargetRank = mContent.findNearestArea((int) r[0], (int) r[1]);
|
|
if (mTargetRank != mPrevTargetRank) {
|
|
mReorderAlarm.cancelAlarm();
|
|
mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
|
|
mReorderAlarm.setAlarm(REORDER_DELAY);
|
|
mPrevTargetRank = mTargetRank;
|
|
}
|
|
}
|
|
|
|
OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
|
|
public void onAlarm(Alarm alarm) {
|
|
completeDragExit();
|
|
}
|
|
};
|
|
|
|
public void completeDragExit() {
|
|
mLauncher.closeFolder();
|
|
mCurrentDragInfo = null;
|
|
mCurrentDragView = null;
|
|
mSuppressOnAdd = false;
|
|
mRearrangeOnClose = true;
|
|
mIsExternalDrag = false;
|
|
}
|
|
|
|
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(final View target, final DragObject d,
|
|
final boolean isFlingToDelete, final boolean success) {
|
|
if (mDeferDropAfterUninstall) {
|
|
Log.d(TAG, "Deferred handling drop because waiting for uninstall.");
|
|
mDeferredAction = new Runnable() {
|
|
public void run() {
|
|
onDropCompleted(target, d, isFlingToDelete, success);
|
|
mDeferredAction = null;
|
|
}
|
|
};
|
|
return;
|
|
}
|
|
|
|
boolean beingCalledAfterUninstall = mDeferredAction != null;
|
|
boolean successfulDrop =
|
|
success && (!beingCalledAfterUninstall || mUninstallSuccessful);
|
|
|
|
if (successfulDrop) {
|
|
if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
|
|
replaceFolderWithFinalItem();
|
|
}
|
|
} else {
|
|
rearrangeChildren();
|
|
// The drag failed, we need to return the item to the folder
|
|
mFolderIcon.onDrop(d);
|
|
}
|
|
|
|
if (target != this) {
|
|
if (mOnExitAlarm.alarmPending()) {
|
|
mOnExitAlarm.cancelAlarm();
|
|
if (!successfulDrop) {
|
|
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.
|
|
updateItemLocationsInDatabaseBatch();
|
|
}
|
|
|
|
public void deferCompleteDropAfterUninstallActivity() {
|
|
mDeferDropAfterUninstall = true;
|
|
}
|
|
|
|
public void onUninstallActivityReturned(boolean success) {
|
|
mDeferDropAfterUninstall = false;
|
|
mUninstallSuccessful = success;
|
|
if (mDeferredAction != null) {
|
|
mDeferredAction.run();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public float getIntrinsicIconScaleFactor() {
|
|
return 1f;
|
|
}
|
|
|
|
@Override
|
|
public boolean supportsFlingToDelete() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean supportsAppInfoDropTarget() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean supportsDeleteDropTarget() {
|
|
return true;
|
|
}
|
|
|
|
public void onFlingToDelete(DragObject d, int x, int y, PointF vec) {
|
|
// Do nothing
|
|
}
|
|
|
|
@Override
|
|
public void onFlingToDeleteCompleted() {
|
|
// Do nothing
|
|
}
|
|
|
|
private void updateItemLocationsInDatabaseBatch() {
|
|
ArrayList<View> list = getItemsInReadingOrder();
|
|
ArrayList<ItemInfo> items = new ArrayList<ItemInfo>();
|
|
for (int i = 0; i < list.size(); i++) {
|
|
View v = list.get(i);
|
|
ItemInfo info = (ItemInfo) v.getTag();
|
|
info.rank = i;
|
|
items.add(info);
|
|
}
|
|
|
|
LauncherModel.moveItemsInDatabase(mLauncher, items, mInfo.id, 0);
|
|
}
|
|
|
|
public void addItemLocationsInDatabase() {
|
|
ArrayList<View> list = getItemsInReadingOrder();
|
|
for (int i = 0; i < list.size(); i++) {
|
|
View v = list.get(i);
|
|
ItemInfo info = (ItemInfo) v.getTag();
|
|
LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0,
|
|
info.cellX, info.cellY, false);
|
|
}
|
|
}
|
|
|
|
public void notifyDrop() {
|
|
if (mDragInProgress) {
|
|
mItemAddedBackToSelfViaIcon = true;
|
|
}
|
|
}
|
|
|
|
public boolean isDropEnabled() {
|
|
return true;
|
|
}
|
|
|
|
public boolean isFull() {
|
|
return mContent.isFull();
|
|
}
|
|
|
|
private void centerAboutIcon() {
|
|
DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
|
|
|
|
DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer);
|
|
int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
|
|
int height = getFolderHeight();
|
|
|
|
float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect);
|
|
|
|
LauncherAppState app = LauncherAppState.getInstance();
|
|
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
|
|
|
|
int centerX = (int) (sTempRect.left + sTempRect.width() * scale / 2);
|
|
int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2);
|
|
int centeredLeft = centerX - width / 2;
|
|
int centeredTop = centerY - height / 2;
|
|
int currentPage = mLauncher.getWorkspace().getNextPage();
|
|
// 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 (grid.isPhone() && (grid.availableWidthPx - width) < grid.iconSizePx) {
|
|
// Center the folder if it is full (on phones only)
|
|
left = (grid.availableWidthPx - width) / 2;
|
|
} else if (width >= bounds.width()) {
|
|
// If the folder doesn't fit within the bounds, center it about the desired bounds
|
|
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 int getContentAreaHeight() {
|
|
LauncherAppState app = LauncherAppState.getInstance();
|
|
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
|
|
Rect workspacePadding = grid.getWorkspacePadding(grid.isLandscape ?
|
|
CellLayout.LANDSCAPE : CellLayout.PORTRAIT);
|
|
int maxContentAreaHeight = grid.availableHeightPx -
|
|
workspacePadding.top - workspacePadding.bottom -
|
|
mBottomContentHeight;
|
|
int height = Math.min(maxContentAreaHeight,
|
|
mContent.getDesiredHeight());
|
|
return Math.max(height, MIN_CONTENT_DIMEN);
|
|
}
|
|
|
|
private int getContentAreaWidth() {
|
|
return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
|
|
}
|
|
|
|
private int getFolderHeight() {
|
|
return getFolderHeight(getContentAreaHeight());
|
|
}
|
|
|
|
private int getFolderHeight(int contentAreaHeight) {
|
|
return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mBottomContentHeight;
|
|
}
|
|
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
int contentWidth = getContentAreaWidth();
|
|
int contentHeight = getContentAreaHeight();
|
|
|
|
int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY);
|
|
int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY);
|
|
|
|
mContent.setFixedSize(contentWidth, contentHeight);
|
|
mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec);
|
|
|
|
// Move the bottom content below mContent
|
|
mBottomContent.measure(contentAreaWidthSpec,
|
|
MeasureSpec.makeMeasureSpec(mBottomContentHeight, MeasureSpec.EXACTLY));
|
|
|
|
int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
|
|
int folderHeight = getFolderHeight(contentHeight);
|
|
setMeasuredDimension(folderWidth, folderHeight);
|
|
}
|
|
|
|
/**
|
|
* Rearranges the children based on their rank.
|
|
*/
|
|
public void rearrangeChildren() {
|
|
rearrangeChildren(-1);
|
|
}
|
|
|
|
/**
|
|
* Rearranges the children based on their rank.
|
|
* @param itemCount if greater than the total children count, empty spaces are left at the end,
|
|
* otherwise it is ignored.
|
|
*/
|
|
public void rearrangeChildren(int itemCount) {
|
|
ArrayList<View> views = getItemsInReadingOrder();
|
|
mContent.arrangeChildren(views, Math.max(itemCount, views.size()));
|
|
mItemsInvalidated = true;
|
|
}
|
|
|
|
public int getItemCount() {
|
|
return mContent.getItemCount();
|
|
}
|
|
|
|
private void onCloseComplete() {
|
|
DragLayer parent = (DragLayer) getParent();
|
|
if (parent != null) {
|
|
parent.removeView(this);
|
|
}
|
|
mDragController.removeDropTarget((DropTarget) this);
|
|
clearFocus();
|
|
mFolderIcon.requestFocus();
|
|
|
|
if (mRearrangeOnClose) {
|
|
rearrangeChildren();
|
|
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.screenId);
|
|
|
|
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.screenId, mInfo.cellX, mInfo.cellY);
|
|
}
|
|
if (getItemCount() <= 1) {
|
|
// Remove the folder
|
|
LauncherModel.deleteItemFromDatabase(mLauncher, mInfo);
|
|
if (cellLayout != null) {
|
|
// b/12446428 -- sometimes the cell layout has already gone away?
|
|
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. We need to add the new item with addInScreenFromBind()
|
|
// to ensure that hotseat items are placed correctly.
|
|
if (child != null) {
|
|
mLauncher.getWorkspace().addInScreenFromBind(child, mInfo.container, mInfo.screenId,
|
|
mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY);
|
|
}
|
|
}
|
|
};
|
|
View finalChild = mContent.getLastItem();
|
|
if (finalChild != null) {
|
|
mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable);
|
|
} else {
|
|
onCompleteRunnable.run();
|
|
}
|
|
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 = mContent.getLastItem();
|
|
if (lastChild != null) {
|
|
mFolderName.setNextFocusDownId(lastChild.getId());
|
|
mFolderName.setNextFocusRightId(lastChild.getId());
|
|
mFolderName.setNextFocusLeftId(lastChild.getId());
|
|
mFolderName.setNextFocusUpId(lastChild.getId());
|
|
}
|
|
}
|
|
|
|
public void onDrop(DragObject d) {
|
|
Runnable cleanUpRunnable = null;
|
|
|
|
// If we are coming from All Apps space, we defer removing the extra empty screen
|
|
// until the folder closes
|
|
if (d.dragSource != mLauncher.getWorkspace() && !(d.dragSource instanceof Folder)) {
|
|
cleanUpRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mLauncher.exitSpringLoadedDragModeDelayed(true,
|
|
Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT,
|
|
null);
|
|
}
|
|
};
|
|
}
|
|
|
|
View currentDragView;
|
|
ShortcutInfo si = mCurrentDragInfo;
|
|
if (mIsExternalDrag) {
|
|
currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank);
|
|
// Actually move the item in the database if it was an external drag. Call this
|
|
// before creating the view, so that ShortcutInfo is updated appropriately.
|
|
LauncherModel.addOrMoveItemInDatabase(
|
|
mLauncher, si, mInfo.id, 0, si.cellX, si.cellY);
|
|
|
|
// We only need to update the locations if it doesn't get handled in #onDropCompleted.
|
|
if (d.dragSource != this) {
|
|
updateItemLocationsInDatabaseBatch();
|
|
}
|
|
mIsExternalDrag = false;
|
|
} else {
|
|
currentDragView = mCurrentDragView;
|
|
mContent.addViewForRank(currentDragView, si, mEmptyCellRank);
|
|
}
|
|
|
|
if (d.dragView.hasDrawn()) {
|
|
|
|
// Temporarily reset the scale such that the animation target gets calculated correctly.
|
|
float scaleX = getScaleX();
|
|
float scaleY = getScaleY();
|
|
setScaleX(1.0f);
|
|
setScaleY(1.0f);
|
|
mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, currentDragView,
|
|
cleanUpRunnable, null);
|
|
setScaleX(scaleX);
|
|
setScaleY(scaleY);
|
|
} else {
|
|
d.deferDragViewCleanupPostAnimation = false;
|
|
currentDragView.setVisibility(VISIBLE);
|
|
}
|
|
mItemsInvalidated = true;
|
|
rearrangeChildren();
|
|
|
|
// Temporarily suppress the listener, as we did all the work already here.
|
|
mSuppressOnAdd = true;
|
|
mInfo.add(si);
|
|
mSuppressOnAdd = false;
|
|
// Clear the drag info, as it is no longer being dragged.
|
|
mCurrentDragInfo = null;
|
|
}
|
|
|
|
// 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;
|
|
mContent.createAndAddViewForRank(item, mContent.allocateNewLastItemRank());
|
|
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 {
|
|
rearrangeChildren();
|
|
}
|
|
if (getItemCount() <= 1) {
|
|
replaceFolderWithFinalItem();
|
|
}
|
|
}
|
|
|
|
private View getViewForInfo(final ShortcutInfo item) {
|
|
return mContent.iterateOverItems(new ItemOperator() {
|
|
|
|
@Override
|
|
public boolean evaluate(ItemInfo info, View view, View parent) {
|
|
return info == item;
|
|
}
|
|
});
|
|
}
|
|
|
|
public void onItemsChanged() {
|
|
updateTextViewFocus();
|
|
}
|
|
|
|
public void onTitleChanged(CharSequence title) {
|
|
}
|
|
|
|
public ArrayList<View> getItemsInReadingOrder() {
|
|
if (mItemsInvalidated) {
|
|
mItemsInReadingOrder.clear();
|
|
mContent.iterateOverItems(new ItemOperator() {
|
|
|
|
@Override
|
|
public boolean evaluate(ItemInfo info, View view, View parent) {
|
|
mItemsInReadingOrder.add(view);
|
|
return false;
|
|
}
|
|
});
|
|
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();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void getHitRectRelativeToDragLayer(Rect outRect) {
|
|
getHitRect(outRect);
|
|
}
|
|
|
|
public static interface FolderContent {
|
|
void setFolder(Folder f);
|
|
|
|
void removeView(View v);
|
|
|
|
boolean isFull();
|
|
int getItemCount();
|
|
|
|
int getDesiredWidth();
|
|
int getDesiredHeight();
|
|
void setFixedSize(int width, int height);
|
|
|
|
/**
|
|
* Iterates over all its items in a reading order.
|
|
* @return the view for which the operator returned true.
|
|
*/
|
|
View iterateOverItems(ItemOperator op);
|
|
View getLastItem();
|
|
|
|
String getAccessibilityDescription();
|
|
|
|
/**
|
|
* Binds items to the layout.
|
|
* @return list of items that could not be bound, probably because we hit the max size limit.
|
|
*/
|
|
ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> children);
|
|
|
|
/**
|
|
* Create space for a new item at the end, and returns the rank for that item.
|
|
* Resizes the content if necessary.
|
|
*/
|
|
int allocateNewLastItemRank();
|
|
|
|
View createAndAddViewForRank(ShortcutInfo item, int rank);
|
|
|
|
/**
|
|
* Adds the {@param view} to the layout based on {@param rank} and updated the position
|
|
* related attributes. It assumes that {@param item} is already attached to the view.
|
|
*/
|
|
void addViewForRank(View view, ShortcutInfo item, int rank);
|
|
|
|
/**
|
|
* Reorders the items such that the {@param empty} spot moves to {@param target}
|
|
*/
|
|
void realTimeReorder(int empty, int target);
|
|
|
|
/**
|
|
* @return the rank of the cell nearest to the provided pixel position.
|
|
*/
|
|
int findNearestArea(int pixelX, int pixelY);
|
|
|
|
/**
|
|
* Updates position and rank of all the children in the view based.
|
|
* @param list the ordered list of children.
|
|
* @param itemCount if greater than the total children count, empty spaces are left
|
|
* at the end.
|
|
*/
|
|
void arrangeChildren(ArrayList<View> list, int itemCount);
|
|
|
|
/**
|
|
* Sets the focus on the first visible child.
|
|
*/
|
|
void setFocusOnFirstChild();
|
|
}
|
|
}
|