c0b52fefbc
- Routing the various places where we call through to delete from LauncherModel through Launcher, which will delegate the removal of the icon from the workspace, and properly handle the removal of all items and their contents from the db. Bug: 23944119 Change-Id: I022fe2b3e79da16b5af87505c4362490b8422686
1407 lines
51 KiB
Java
1407 lines
51 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.SuppressLint;
|
|
import android.annotation.TargetApi;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Point;
|
|
import android.graphics.PointF;
|
|
import android.graphics.Rect;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
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.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.accessibility.AccessibilityEvent;
|
|
import android.view.accessibility.AccessibilityManager;
|
|
import android.view.animation.AccelerateInterpolator;
|
|
import android.view.animation.AnimationUtils;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.launcher3.CellLayout.CellInfo;
|
|
import com.android.launcher3.DragController.DragListener;
|
|
import com.android.launcher3.FolderInfo.FolderListener;
|
|
import com.android.launcher3.UninstallDropTarget.UninstallSource;
|
|
import com.android.launcher3.Workspace.ItemOperator;
|
|
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource;
|
|
import com.android.launcher3.util.Thunk;
|
|
import com.android.launcher3.util.UiThreadCircularReveal;
|
|
|
|
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, DragListener, UninstallSource, AccessibilityDragSource,
|
|
Stats.LaunchSourceProvider {
|
|
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;
|
|
|
|
/**
|
|
* Time for which the scroll hint is shown before automatically changing page.
|
|
*/
|
|
public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY;
|
|
|
|
/**
|
|
* Fraction of icon width which behave as scroll region.
|
|
*/
|
|
private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
|
|
|
|
private static final int FOLDER_NAME_ANIMATION_DURATION = 633;
|
|
|
|
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 Alarm mOnScrollHintAlarm = new Alarm();
|
|
@Thunk final Alarm mScrollPauseAlarm = new Alarm();
|
|
|
|
@Thunk 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;
|
|
|
|
@Thunk FolderIcon mFolderIcon;
|
|
|
|
@Thunk FolderPagedView mContent;
|
|
@Thunk View mContentWrapper;
|
|
ExtendedEditText mFolderName;
|
|
|
|
private View mFooter;
|
|
private int mFooterHeight;
|
|
|
|
// Cell ranks used for drag and drop
|
|
@Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank;
|
|
|
|
@Thunk 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;
|
|
@Thunk float mFolderIconPivotX;
|
|
@Thunk float mFolderIconPivotY;
|
|
private boolean mIsEditingName = false;
|
|
|
|
private boolean mDestroyed;
|
|
|
|
@Thunk Runnable mDeferredAction;
|
|
private boolean mDeferDropAfterUninstall;
|
|
private boolean mUninstallSuccessful;
|
|
|
|
// Folder scrolling
|
|
private int mScrollAreaOffset;
|
|
|
|
@Thunk int mScrollHintDir = DragController.SCROLL_NONE;
|
|
@Thunk int mCurrentScrollDir = DragController.SCROLL_NONE;
|
|
|
|
/**
|
|
* 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 = (FolderPagedView) findViewById(R.id.folder_content);
|
|
mContent.setFolder(this);
|
|
|
|
mFolderName = (ExtendedEditText) findViewById(R.id.folder_name);
|
|
mFolderName.setOnBackKeyListener(new ExtendedEditText.OnBackKeyListener() {
|
|
@Override
|
|
public boolean onBackKey() {
|
|
// Close the activity on back key press
|
|
doneEditingFolderName(true);
|
|
return false;
|
|
}
|
|
});
|
|
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);
|
|
|
|
mFooter = findViewById(R.id.folder_footer);
|
|
|
|
// We find out how tall footer 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;
|
|
mFooter.measure(measureSpec, measureSpec);
|
|
mFooterHeight = mFooter.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;
|
|
return beginDrag(v, false);
|
|
}
|
|
|
|
private boolean beginDrag(View v, boolean accessible) {
|
|
Object tag = v.getTag();
|
|
if (tag instanceof ShortcutInfo) {
|
|
ShortcutInfo item = (ShortcutInfo) tag;
|
|
if (!v.isInTouchMode()) {
|
|
return false;
|
|
}
|
|
|
|
mLauncher.getWorkspace().beginDragShared(v, new Point(), this, accessible);
|
|
|
|
mCurrentDragInfo = item;
|
|
mEmptyCellRank = item.rank;
|
|
mCurrentDragView = v;
|
|
|
|
mContent.removeItem(mCurrentDragView);
|
|
mInfo.remove(mCurrentDragInfo);
|
|
mDragInProgress = true;
|
|
mItemAddedBackToSelfViaIcon = false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void startDrag(CellInfo cellInfo, boolean accessible) {
|
|
beginDrag(cellInfo.cell, accessible);
|
|
}
|
|
|
|
@Override
|
|
public void enableAccessibleDrag(boolean enable) {
|
|
mLauncher.getSearchDropTargetBar().enableAccessibleDrag(enable);
|
|
for (int i = 0; i < mContent.getChildCount(); i++) {
|
|
mContent.getPageAt(i).enableAccessibleDrag(enable, CellLayout.FOLDER_ACCESSIBILITY_DRAG);
|
|
}
|
|
|
|
mFooter.setImportantForAccessibility(enable ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS :
|
|
IMPORTANT_FOR_ACCESSIBILITY_AUTO);
|
|
mLauncher.getWorkspace().setAddNewPageOnDrag(!enable);
|
|
}
|
|
|
|
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.
|
|
*/
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
@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
|
|
*/
|
|
public FolderInfo getInfo() {
|
|
return mInfo;
|
|
}
|
|
|
|
void bind(FolderInfo info) {
|
|
mInfo = info;
|
|
ArrayList<ShortcutInfo> children = info.contents;
|
|
Collections.sort(children, ITEM_POS_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.
|
|
*/
|
|
@SuppressLint("InflateParams")
|
|
static Folder fromXml(Launcher launcher) {
|
|
return (Folder) launcher.getLayoutInflater().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;
|
|
|
|
mContent.completePendingPageChanges();
|
|
if (!mDragInProgress) {
|
|
// Open on the first page.
|
|
mContent.snapToPageImmediately(0);
|
|
}
|
|
|
|
Animator openFolderAnim = null;
|
|
final Runnable onCompleteRunnable;
|
|
if (!Utilities.ATLEAST_LOLLIPOP) {
|
|
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();
|
|
|
|
AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
|
|
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);
|
|
|
|
Animator drift = ObjectAnimator.ofPropertyValuesHolder(this, tx, ty);
|
|
drift.setDuration(mMaterialExpandDuration);
|
|
drift.setStartDelay(mMaterialExpandStagger);
|
|
drift.setInterpolator(new LogDecelerateInterpolator(100, 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.hypot(rx, ry);
|
|
|
|
Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(),
|
|
(int) getPivotY(), 0, radius);
|
|
reveal.setDuration(mMaterialExpandDuration);
|
|
reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
|
|
|
|
mContentWrapper.setAlpha(0f);
|
|
Animator iconsAlpha = ObjectAnimator.ofFloat(mContentWrapper, "alpha", 0f, 1f);
|
|
iconsAlpha.setDuration(mMaterialExpandDuration);
|
|
iconsAlpha.setStartDelay(mMaterialExpandStagger);
|
|
iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
|
|
|
|
mFooter.setAlpha(0f);
|
|
Animator textAlpha = ObjectAnimator.ofFloat(mFooter, "alpha", 0f, 1f);
|
|
textAlpha.setDuration(mMaterialExpandDuration);
|
|
textAlpha.setStartDelay(mMaterialExpandStagger);
|
|
textAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
|
|
|
|
anim.play(drift);
|
|
anim.play(iconsAlpha);
|
|
anim.play(textAlpha);
|
|
anim.play(reveal);
|
|
|
|
openFolderAnim = anim;
|
|
|
|
mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null);
|
|
mFooter.setLayerType(LAYER_TYPE_HARDWARE, null);
|
|
onCompleteRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
|
|
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();
|
|
}
|
|
});
|
|
|
|
// Footer animation
|
|
if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) {
|
|
int footerWidth = mContent.getDesiredWidth()
|
|
- mFooter.getPaddingLeft() - mFooter.getPaddingRight();
|
|
|
|
float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString());
|
|
float translation = (footerWidth - textWidth) / 2;
|
|
mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation);
|
|
mContent.setMarkerScale(0);
|
|
|
|
// Do not update the flag if we are in drag mode. The flag will be updated, when we
|
|
// actually drop the icon.
|
|
final boolean updateAnimationFlag = !mDragInProgress;
|
|
openFolderAnim.addListener(new AnimatorListenerAdapter() {
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION)
|
|
.translationX(0)
|
|
.setInterpolator(Utilities.ATLEAST_LOLLIPOP ?
|
|
AnimationUtils.loadInterpolator(mLauncher,
|
|
android.R.interpolator.fast_out_slow_in)
|
|
: new LogDecelerateInterpolator(100, 0));
|
|
mContent.animateMarkers();
|
|
|
|
if (updateAnimationFlag) {
|
|
mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
mFolderName.setTranslationX(0);
|
|
mContent.setMarkerScale(1);
|
|
}
|
|
|
|
openFolderAnim.start();
|
|
|
|
// Make sure the folder picks up the last drag move even if the finger doesn't move.
|
|
if (mDragController.isDragging()) {
|
|
mDragController.forceTouchMove();
|
|
}
|
|
|
|
FolderPagedView pages = (FolderPagedView) mContent;
|
|
pages.verifyVisibleHighResIcons(pages.getNextPage());
|
|
}
|
|
|
|
public void beginExternalDrag(ShortcutInfo item) {
|
|
mCurrentDragInfo = item;
|
|
mEmptyCellRank = mContent.allocateRankForNewItem(item);
|
|
mIsExternalDrag = true;
|
|
mDragInProgress = true;
|
|
|
|
// Since this folder opened by another controller, it might not get onDrop or
|
|
// onDropComplete. Perform cleanup once drag-n-drop ends.
|
|
mDragController.addDragListener(this);
|
|
}
|
|
|
|
@Override
|
|
public void onDragStart(DragSource source, Object info, int dragAction) { }
|
|
|
|
@Override
|
|
public void onDragEnd() {
|
|
if (mIsExternalDrag && mDragInProgress) {
|
|
completeDragExit();
|
|
}
|
|
mDragController.removeDragListener(this);
|
|
}
|
|
|
|
@Thunk 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();
|
|
// Get the area offset such that the folder only closes if half the drag icon width
|
|
// is outside the folder area
|
|
mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
@Override
|
|
public void onDragOver(DragObject d) {
|
|
onDragOver(d, REORDER_DELAY);
|
|
}
|
|
|
|
private int getTargetRank(DragObject d, float[] recycle) {
|
|
recycle = d.getVisualCenter(recycle);
|
|
return mContent.findNearestArea(
|
|
(int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop());
|
|
}
|
|
|
|
@Thunk void onDragOver(DragObject d, int reorderDelay) {
|
|
if (mScrollPauseAlarm.alarmPending()) {
|
|
return;
|
|
}
|
|
final float[] r = new float[2];
|
|
mTargetRank = getTargetRank(d, r);
|
|
|
|
if (mTargetRank != mPrevTargetRank) {
|
|
mReorderAlarm.cancelAlarm();
|
|
mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
|
|
mReorderAlarm.setAlarm(REORDER_DELAY);
|
|
mPrevTargetRank = mTargetRank;
|
|
}
|
|
|
|
float x = r[0];
|
|
int currentPage = mContent.getNextPage();
|
|
|
|
float cellOverlap = mContent.getCurrentCellLayout().getCellWidth()
|
|
* ICON_OVERSCROLL_WIDTH_FACTOR;
|
|
boolean isOutsideLeftEdge = x < cellOverlap;
|
|
boolean isOutsideRightEdge = x > (getWidth() - cellOverlap);
|
|
|
|
if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) {
|
|
showScrollHint(DragController.SCROLL_LEFT, d);
|
|
} else if (currentPage < (mContent.getPageCount() - 1)
|
|
&& (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) {
|
|
showScrollHint(DragController.SCROLL_RIGHT, d);
|
|
} else {
|
|
mOnScrollHintAlarm.cancelAlarm();
|
|
if (mScrollHintDir != DragController.SCROLL_NONE) {
|
|
mContent.clearScrollHint();
|
|
mScrollHintDir = DragController.SCROLL_NONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void showScrollHint(int direction, DragObject d) {
|
|
// Show scroll hint on the right
|
|
if (mScrollHintDir != direction) {
|
|
mContent.showScrollHint(direction);
|
|
mScrollHintDir = direction;
|
|
}
|
|
|
|
// Set alarm for when the hint is complete
|
|
if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) {
|
|
mCurrentScrollDir = direction;
|
|
mOnScrollHintAlarm.cancelAlarm();
|
|
mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d));
|
|
mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION);
|
|
|
|
mReorderAlarm.cancelAlarm();
|
|
mTargetRank = mEmptyCellRank;
|
|
}
|
|
}
|
|
|
|
OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
|
|
public void onAlarm(Alarm alarm) {
|
|
completeDragExit();
|
|
}
|
|
};
|
|
|
|
public void completeDragExit() {
|
|
if (mInfo.opened) {
|
|
mLauncher.closeFolder();
|
|
mRearrangeOnClose = true;
|
|
} else if (mState == STATE_ANIMATING) {
|
|
mRearrangeOnClose = true;
|
|
} else {
|
|
rearrangeChildren();
|
|
clearDragInfo();
|
|
}
|
|
}
|
|
|
|
private void clearDragInfo() {
|
|
mCurrentDragInfo = null;
|
|
mCurrentDragView = null;
|
|
mSuppressOnAdd = false;
|
|
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();
|
|
|
|
mOnScrollHintAlarm.cancelAlarm();
|
|
mScrollPauseAlarm.cancelAlarm();
|
|
if (mScrollHintDir != DragController.SCROLL_NONE) {
|
|
mContent.clearScrollHint();
|
|
mScrollHintDir = DragController.SCROLL_NONE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When performing an accessibility drop, onDrop is sent immediately after onDragEnter. So we
|
|
* need to complete all transient states based on timers.
|
|
*/
|
|
@Override
|
|
public void prepareAccessibilityDrop() {
|
|
if (mReorderAlarm.alarmPending()) {
|
|
mReorderAlarm.cancelAlarm();
|
|
mReorderAlarmListener.onAlarm(mReorderAlarm);
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// The drag failed, we need to return the item to the folder
|
|
ShortcutInfo info = (ShortcutInfo) d.dragInfo;
|
|
View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info)
|
|
? mCurrentDragView : mContent.createNewView(info);
|
|
ArrayList<View> views = getItemsInReadingOrder();
|
|
views.add(info.rank, icon);
|
|
mContent.arrangeChildren(views, views.size());
|
|
mItemsInvalidated = true;
|
|
|
|
mSuppressOnAdd = true;
|
|
mFolderIcon.onDrop(d);
|
|
mSuppressOnAdd = false;
|
|
}
|
|
|
|
if (target != this) {
|
|
if (mOnExitAlarm.alarmPending()) {
|
|
mOnExitAlarm.cancelAlarm();
|
|
if (!successfulDrop) {
|
|
mSuppressFolderDeletion = true;
|
|
}
|
|
mScrollPauseAlarm.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.
|
|
updateItemLocationsInDatabaseBatch();
|
|
|
|
// Use the item count to check for multi-page as the folder UI may not have
|
|
// been refreshed yet.
|
|
if (getItemCount() <= mContent.itemsPerPage()) {
|
|
// Show the animation, next time something is added to the folder.
|
|
mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher);
|
|
}
|
|
|
|
}
|
|
|
|
@Override
|
|
public void deferCompleteDropAfterUninstallActivity() {
|
|
mDeferDropAfterUninstall = true;
|
|
}
|
|
|
|
@Override
|
|
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;
|
|
}
|
|
|
|
@Override
|
|
public void onFlingToDelete(DragObject d, 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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
DeviceProfile grid = mLauncher.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;
|
|
|
|
// We need to bound the folder to the currently visible workspace area
|
|
mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect);
|
|
int left = Math.min(Math.max(sTempRect.left, centeredLeft),
|
|
sTempRect.left + sTempRect.width() - width);
|
|
int top = Math.min(Math.max(sTempRect.top, centeredTop),
|
|
sTempRect.top + sTempRect.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 >= sTempRect.width()) {
|
|
// If the folder doesn't fit within the bounds, center it about the desired bounds
|
|
left = sTempRect.left + (sTempRect.width() - width) / 2;
|
|
}
|
|
if (height >= sTempRect.height()) {
|
|
top = sTempRect.top + (sTempRect.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() {
|
|
DeviceProfile grid = mLauncher.getDeviceProfile();
|
|
Rect workspacePadding = grid.getWorkspacePadding(mContent.mIsRtl);
|
|
int maxContentAreaHeight = grid.availableHeightPx -
|
|
workspacePadding.top - workspacePadding.bottom -
|
|
mFooterHeight;
|
|
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 + mFooterHeight;
|
|
}
|
|
|
|
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);
|
|
|
|
if (mContent.getChildCount() > 0) {
|
|
int cellIconGap = (mContent.getPageAt(0).getCellWidth()
|
|
- mLauncher.getDeviceProfile().iconSizePx) / 2;
|
|
mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap,
|
|
mFooter.getPaddingTop(),
|
|
mContent.getPaddingRight() + cellIconGap,
|
|
mFooter.getPaddingBottom());
|
|
}
|
|
mFooter.measure(contentAreaWidthSpec,
|
|
MeasureSpec.makeMeasureSpec(mFooterHeight, 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;
|
|
}
|
|
|
|
// TODO remove this once GSA code fix is submitted
|
|
public ViewGroup getContent() {
|
|
return (ViewGroup) mContent;
|
|
}
|
|
|
|
public int getItemCount() {
|
|
return mContent.getItemCount();
|
|
}
|
|
|
|
@Thunk 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;
|
|
clearDragInfo();
|
|
}
|
|
|
|
@Thunk 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(cellLayout, finalItem);
|
|
LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
|
|
mInfo.screenId, mInfo.cellX, mInfo.cellY);
|
|
}
|
|
if (getItemCount() <= 1) {
|
|
mLauncher.removeItem(mFolderIcon, mInfo, true /* deleteFromDb */);
|
|
if (mFolderIcon instanceof DropTarget) {
|
|
mDragController.removeDropTarget((DropTarget) mFolderIcon);
|
|
}
|
|
}
|
|
// 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
|
|
public 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);
|
|
}
|
|
};
|
|
}
|
|
|
|
// If the icon was dropped while the page was being scrolled, we need to compute
|
|
// the target location again such that the icon is placed of the final page.
|
|
if (!mContent.rankOnCurrentPage(mEmptyCellRank)) {
|
|
// Reorder again.
|
|
mTargetRank = getTargetRank(d, null);
|
|
|
|
// Rearrange items immediately.
|
|
mReorderAlarmListener.onAlarm(mReorderAlarm);
|
|
|
|
mOnScrollHintAlarm.cancelAlarm();
|
|
mScrollPauseAlarm.cancelAlarm();
|
|
}
|
|
mContent.completePendingPageChanges();
|
|
|
|
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;
|
|
mDragInProgress = false;
|
|
|
|
if (mContent.getPageCount() > 1) {
|
|
// The animation has already been shown while opening the folder.
|
|
mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
@Override
|
|
public void onAdd(ShortcutInfo item) {
|
|
// 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.allocateRankForNewItem(item));
|
|
mItemsInvalidated = true;
|
|
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.removeItem(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);
|
|
outRect.left -= mScrollAreaOffset;
|
|
outRect.right += mScrollAreaOffset;
|
|
}
|
|
|
|
@Override
|
|
public void fillInLaunchSourceData(Bundle sourceData) {
|
|
// Fill in from the folder icon's launch source provider first
|
|
Stats.LaunchSourceUtils.populateSourceDataFromAncestorProvider(mFolderIcon, sourceData);
|
|
sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_FOLDER);
|
|
sourceData.putInt(Stats.SOURCE_EXTRA_SUB_CONTAINER_PAGE, mContent.getCurrentPage());
|
|
}
|
|
|
|
private class OnScrollHintListener implements OnAlarmListener {
|
|
|
|
private final DragObject mDragObject;
|
|
|
|
OnScrollHintListener(DragObject object) {
|
|
mDragObject = object;
|
|
}
|
|
|
|
/**
|
|
* Scroll hint has been shown long enough. Now scroll to appropriate page.
|
|
*/
|
|
@Override
|
|
public void onAlarm(Alarm alarm) {
|
|
if (mCurrentScrollDir == DragController.SCROLL_LEFT) {
|
|
mContent.scrollLeft();
|
|
mScrollHintDir = DragController.SCROLL_NONE;
|
|
} else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) {
|
|
mContent.scrollRight();
|
|
mScrollHintDir = DragController.SCROLL_NONE;
|
|
} else {
|
|
// This should not happen
|
|
return;
|
|
}
|
|
mCurrentScrollDir = DragController.SCROLL_NONE;
|
|
|
|
// Pause drag event until the scrolling is finished
|
|
mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject));
|
|
mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY);
|
|
}
|
|
}
|
|
|
|
private class OnScrollFinishedListener implements OnAlarmListener {
|
|
|
|
private final DragObject mDragObject;
|
|
|
|
OnScrollFinishedListener(DragObject object) {
|
|
mDragObject = object;
|
|
}
|
|
|
|
/**
|
|
* Page scroll is complete.
|
|
*/
|
|
@Override
|
|
public void onAlarm(Alarm alarm) {
|
|
// Reorder immediately on page change.
|
|
onDragOver(mDragObject, 1);
|
|
}
|
|
}
|
|
|
|
// Compares item position based on rank and position giving priority to the rank.
|
|
public static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() {
|
|
|
|
@Override
|
|
public int compare(ItemInfo lhs, ItemInfo rhs) {
|
|
if (lhs.rank != rhs.rank) {
|
|
return lhs.rank - rhs.rank;
|
|
} else if (lhs.cellY != rhs.cellY) {
|
|
return lhs.cellY - rhs.cellY;
|
|
} else {
|
|
return lhs.cellX - rhs.cellX;
|
|
}
|
|
}
|
|
};
|
|
}
|