summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/folder
diff options
context:
space:
mode:
authorSunny Goyal <sunnygoyal@google.com>2016-02-18 22:09:23 +0000
committerSunny Goyal <sunnygoyal@google.com>2016-02-23 02:02:54 +0000
commit261194387beebaa7927ec4e310274218b651494d (patch)
tree31ebc51a25f275e9aa28129aca1ab3ed4c0cb85d /src/com/android/launcher3/folder
parent7de283a5ecb39bf1f264b5d0d8019b6fabdd6ae3 (diff)
downloadandroid_packages_apps_Trebuchet-261194387beebaa7927ec4e310274218b651494d.tar.gz
android_packages_apps_Trebuchet-261194387beebaa7927ec4e310274218b651494d.tar.bz2
android_packages_apps_Trebuchet-261194387beebaa7927ec4e310274218b651494d.zip
Revert "Reverting refactoring of Folder and FolderIcon to avoid dependencies breakage"
This reverts commit fc956e5a2a818c06ed3424e15b0aa20a3f604658. Change-Id: Ib3b5156b8fc3cad35c4634d61d5390c848ce1f93
Diffstat (limited to 'src/com/android/launcher3/folder')
-rw-r--r--src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java7
-rw-r--r--src/com/android/launcher3/folder/Folder.java1506
-rw-r--r--src/com/android/launcher3/folder/FolderIcon.java776
-rw-r--r--src/com/android/launcher3/folder/FolderPagedView.java2
-rw-r--r--src/com/android/launcher3/folder/StackFolderIconLayoutRule.java5
5 files changed, 2289 insertions, 7 deletions
diff --git a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
index b73c04f81..44d7ac6e9 100644
--- a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
+++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
@@ -1,12 +1,15 @@
package com.android.launcher3.folder;
import android.graphics.Path;
+import android.graphics.Point;
-import com.android.launcher3.FolderIcon;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
public class ClippedFolderIconLayoutRule implements FolderIcon.PreviewLayoutRule {
- public static final int MAX_NUM_ITEMS_IN_PREVIEW = 4;
+ static final int MAX_NUM_ITEMS_IN_PREVIEW = 4;
private static final int MIN_NUM_ITEMS_IN_PREVIEW = 2;
final float MIN_SCALE = 0.48f;
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
new file mode 100644
index 000000000..a411c481c
--- /dev/null
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -0,0 +1,1506 @@
+/*
+ * 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.folder;
+
+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.FocusFinder;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewDebug;
+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.Alarm;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.CellLayout.CellInfo;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.FolderInfo;
+import com.android.launcher3.FolderInfo.FolderListener;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LogDecelerateInterpolator;
+import com.android.launcher3.OnAlarmListener;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Stats;
+import com.android.launcher3.UninstallDropTarget.UninstallSource;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragController.DragListener;
+import com.android.launcher3.dragndrop.DragLayer;
+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;
+ public FolderInfo mInfo;
+
+ @Thunk FolderIcon mFolderIcon;
+
+ @Thunk FolderPagedView mContent;
+ @Thunk View mContentWrapper;
+ public ExtendedEditText mFolderName;
+
+ private View mFooter;
+ private int mFooterHeight;
+
+ // Cell ranks used for drag and drop
+ @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank;
+
+ @ViewDebug.ExportedProperty(category = "launcher",
+ mapping = {
+ @ViewDebug.IntToString(from = STATE_NONE, to = "STATE_NONE"),
+ @ViewDebug.IntToString(from = STATE_SMALL, to = "STATE_SMALL"),
+ @ViewDebug.IntToString(from = STATE_ANIMATING, to = "STATE_ANIMATING"),
+ @ViewDebug.IntToString(from = STATE_OPEN, to = "STATE_OPEN"),
+ })
+ @Thunk int mState = STATE_NONE;
+ @ViewDebug.ExportedProperty(category = "launcher")
+ 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;
+
+ @ViewDebug.ExportedProperty(category = "launcher")
+ 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);
+
+ if (!Utilities.ATLEAST_MARSHMALLOW) {
+ // We disable action mode in older OSes where floating selection menu is not yet
+ // available.
+ mFolderName.setCustomSelectionActionModeCallback(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;
+ }
+ });
+ }
+ 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();
+ }
+
+ 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,
+ getContext().getString(R.string.folder_renamed, newTitle));
+ }
+
+ // This ensures that focus is gained every time the field is clicked, which selects all
+ // the text and brings up the soft keyboard if necessary.
+ mFolderName.clearFocus();
+
+ 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
+ protected void onAttachedToWindow() {
+ // requestFocus() causes the focus onto the folder itself, which doesn't cause visual
+ // effect but the next arrow key can start the keyboard focus inside of the folder, not
+ // the folder itself.
+ requestFocus();
+ super.onAttachedToWindow();
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ // When the folder gets focus, we don't want to announce the list of items.
+ return true;
+ }
+
+ @Override
+ public View focusSearch(int direction) {
+ // When the folder is focused, further focus search should be within the folder contents.
+ return FocusFinder.getInstance().findNextFocus(this, null, direction);
+ }
+
+ /**
+ * @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 launcher The main activity.
+ *
+ * @return A new UserFolder.
+ */
+ @SuppressLint("InflateParams")
+ static Folder fromXml(Launcher launcher) {
+ return (Folder) launcher.getLayoutInflater().inflate(
+ FeatureFlags.LAUNCHER3_ICON_NORMALIZATION
+ ? R.layout.user_folder_icon_normalized : 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);
+ }
+
+ // This is set to true in close(), but isn't reset to false until onDropCompleted(). This
+ // leads to an consistent state if you drag out of the folder and drag back in without
+ // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
+ mDeleteFolderOnDropCompleted = false;
+
+ Animator openFolderAnim = null;
+ final Runnable onCompleteRunnable;
+ if (!Utilities.ATLEAST_LOLLIPOP) {
+ positionAndSizeAsIcon();
+ centerAboutIcon();
+
+ final ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(this, 1, 1, 1);
+ 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(TRANSLATION_X, transX, 0);
+ PropertyValuesHolder ty = PropertyValuesHolder.ofFloat(TRANSLATION_Y, 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;
+
+ 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() {
+
+ @SuppressLint("InlinedApi")
+ @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();
+ }
+
+ mContent.verifyVisibleHighResIcons(mContent.getNextPage());
+ }
+
+ /**
+ * Opens the folder without any animation
+ */
+ public void open() {
+ if (!(getParent() instanceof DragLayer)) return;
+
+ mContent.completePendingPageChanges();
+ if (!mDragInProgress) {
+ // Open on the first page.
+ mContent.snapToPageImmediately(0);
+ }
+ centerAboutIcon();
+ mFolderName.setTranslationX(0);
+ mContent.setMarkerScale(1);
+
+ // Make sure the folder picks up the last drag move even if the finger doesn't move.
+ if (mDragController.isDragging()) {
+ mDragController.forceTouchMove();
+ }
+
+ mContent.verifyVisibleHighResIcons(mContent.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, ItemInfo 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;
+ final ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(this, 0, 0.9f, 0.9f);
+ oa.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setLayerType(LAYER_TYPE_NONE, null);
+ close(true);
+ }
+ @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 void close(boolean wasAnimated) {
+ // TODO: Clear all active animations.
+ DragLayer parent = (DragLayer) getParent();
+ if (parent != null) {
+ parent.removeView(this);
+ }
+ mDragController.removeDropTarget(this);
+ clearFocus();
+ if (wasAnimated) {
+ mFolderIcon.requestFocus();
+ }
+
+ if (mRearrangeOnClose) {
+ rearrangeChildren();
+ mRearrangeOnClose = false;
+ }
+ if (getItemCount() <= 1) {
+ if (!mDragInProgress && !mSuppressFolderDeletion) {
+ replaceFolderWithFinalItem();
+ } else if (mDragInProgress) {
+ mDeleteFolderOnDropCompleted = true;
+ }
+ }
+ mSuppressFolderDeletion = false;
+ clearDragInfo();
+ mState = STATE_SMALL;
+ }
+
+ public boolean acceptDrop(DragObject d) {
+ final ItemInfo item = 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;
+
+ if (d.stateAnnouncer != null) {
+ d.stateAnnouncer.announce(getContext().getString(R.string.move_to_position,
+ mTargetRank + 1));
+ }
+ }
+
+ 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);
+ }
+
+ if (!isFlingToDelete) {
+ // Fling to delete already exits spring loaded mode after the animation finishes.
+ mLauncher.exitSpringLoadedDragModeDelayed(successfulDrop,
+ Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
+ }
+ }
+
+ @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 !FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND;
+ }
+
+ @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 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;
+ }
+
+ public float getPivotXForIconAnimation() {
+ return mFolderIconPivotX;
+ }
+ public 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;
+ }
+
+ public int getItemCount() {
+ return mContent.getItemCount();
+ }
+
+ @Thunk void replaceFolderWithFinalItem() {
+ // Add the last remaining child to the workspace in place of the folder
+ Runnable onCompleteRunnable = new Runnable() {
+ @Override
+ public void run() {
+ int itemCount = mInfo.contents.size();
+ if (itemCount <= 1) {
+ View newIcon = null;
+
+ if (itemCount == 1) {
+ // Move the item from the folder to the workspace, in the position of the
+ // folder
+ CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container,
+ mInfo.screenId);
+ ShortcutInfo finalItem = mInfo.contents.remove(0);
+ newIcon = mLauncher.createShortcut(cellLayout, finalItem);
+ LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
+ mInfo.screenId, mInfo.cellX, mInfo.cellY);
+ }
+
+ // Remove the folder
+ mLauncher.removeItem(mFolderIcon, mInfo, true /* deleteFromDb */);
+ if (mFolderIcon instanceof DropTarget) {
+ mDragController.removeDropTarget((DropTarget) mFolderIcon);
+ }
+
+ if (newIcon != null) {
+ // 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.
+ mLauncher.getWorkspace().addInScreenFromBind(newIcon, mInfo.container,
+ mInfo.screenId, mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY);
+
+ // Focus the newly created child
+ newIcon.requestFocus();
+ }
+ }
+ }
+ };
+ View finalChild = mContent.getLastItem();
+ if (finalChild != null) {
+ mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable);
+ } else {
+ onCompleteRunnable.run();
+ }
+ mDestroyed = true;
+ }
+
+ public boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ // This method keeps track of the first and last item in the folder for the purposes
+ // of keyboard focus
+ public void updateTextViewFocus() {
+ final View firstChild = mContent.getFirstItem();
+ final View lastChild = mContent.getLastItem();
+ if (firstChild != null && lastChild != null) {
+ mFolderName.setNextFocusDownId(lastChild.getId());
+ mFolderName.setNextFocusRightId(lastChild.getId());
+ mFolderName.setNextFocusLeftId(lastChild.getId());
+ mFolderName.setNextFocusUpId(lastChild.getId());
+ // Hitting TAB from the folder name wraps around to the first item on the current
+ // folder page, and hitting SHIFT+TAB from that item wraps back to the folder name.
+ mFolderName.setNextFocusForwardId(firstChild.getId());
+ // When clicking off the folder when editing the name, this Folder gains focus. When
+ // pressing an arrow key from that state, give the focus to the first item.
+ this.setNextFocusDownId(firstChild.getId());
+ this.setNextFocusRightId(firstChild.getId());
+ this.setNextFocusLeftId(firstChild.getId());
+ this.setNextFocusUpId(firstChild.getId());
+ // When pressing shift+tab in the above state, give the focus to the last item.
+ setOnKeyListener(new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ boolean isShiftPlusTab = keyCode == KeyEvent.KEYCODE_TAB &&
+ event.hasModifiers(KeyEvent.META_SHIFT_ON);
+ if (isShiftPlusTab && Folder.this.isFocused()) {
+ return lastChild.requestFocus();
+ }
+ return false;
+ }
+ });
+ }
+ }
+
+ 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) {
+ if (mInfo.opened) {
+ mLauncher.closeFolder(this, true);
+ } else {
+ 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 onFocusChange(View v, boolean hasFocus) {
+ if (v == mFolderName) {
+ if (hasFocus) {
+ startEditingFolderName();
+ } else {
+ dismissEditingName();
+ }
+ }
+ }
+
+ @Override
+ public void getHitRectRelativeToDragLayer(Rect outRect) {
+ getHitRect(outRect);
+ outRect.left -= mScrollAreaOffset;
+ outRect.right += mScrollAreaOffset;
+ }
+
+ @Override
+ public void fillInLaunchSourceData(View v, 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;
+ }
+ }
+ };
+}
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
new file mode 100644
index 000000000..5c084d949
--- /dev/null
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -0,0 +1,776 @@
+/*
+ * 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.folder;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Looper;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.launcher3.Alarm;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.CheckLongPressHelper;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.FolderInfo;
+import com.android.launcher3.FolderInfo.FolderListener;
+import com.android.launcher3.IconCache;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.OnAlarmListener;
+import com.android.launcher3.PreloadIconDrawable;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.SimpleOnStylusPressListener;
+import com.android.launcher3.StylusEventHelper;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.util.Thunk;
+
+import java.util.ArrayList;
+
+/**
+ * An icon that can appear on in the workspace representing an {@link Folder}.
+ */
+public class FolderIcon extends FrameLayout implements FolderListener {
+ @Thunk
+ Launcher mLauncher;
+ @Thunk Folder mFolder;
+ private FolderInfo mInfo;
+ @Thunk static boolean sStaticValuesDirty = true;
+
+ public static final int NUM_ITEMS_IN_PREVIEW = FeatureFlags.LAUNCHER3_CLIPPED_FOLDER_ICON ?
+ ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW :
+ StackFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
+
+ private CheckLongPressHelper mLongPressHelper;
+ private StylusEventHelper mStylusEventHelper;
+
+ // The number of icons to display in the
+ private static final int CONSUMPTION_ANIMATION_DURATION = 100;
+ private static final int DROP_IN_ANIMATION_DURATION = 400;
+ private static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
+ private static final int FINAL_ITEM_ANIMATION_DURATION = 200;
+
+ // The degree to which the inner ring grows when accepting drop
+ private static final float INNER_RING_GROWTH_FACTOR = 0.15f;
+
+ // The degree to which the outer ring is scaled in its natural state
+ private static final float OUTER_RING_GROWTH_FACTOR = 0.3f;
+
+ // Flag as to whether or not to draw an outer ring. Currently none is designed.
+ public static final boolean HAS_OUTER_RING = true;
+
+ // Flag whether the folder should open itself when an item is dragged over is enabled.
+ public static final boolean SPRING_LOADING_ENABLED = true;
+
+ // Delay when drag enters until the folder opens, in miliseconds.
+ private static final int ON_OPEN_DELAY = 800;
+
+ public static Drawable sSharedFolderLeaveBehind = null;
+
+ @Thunk ImageView mPreviewBackground;
+ @Thunk
+ BubbleTextView mFolderName;
+
+ FolderRingAnimator mFolderRingAnimator = null;
+
+ // These variables are all associated with the drawing of the preview; they are stored
+ // as member variables for shared usage and to avoid computation on each frame
+ private int mIntrinsicIconSize;
+ private int mAvailableSpaceInPreview;
+ private int mPreviewOffsetX;
+ private int mPreviewOffsetY;
+ private int mTotalWidth;
+
+ private PreviewLayoutRule mPreviewLayoutRule;
+
+ boolean mAnimating = false;
+ private Rect mOldBounds = new Rect();
+
+ private float mSlop;
+
+ private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0);
+ @Thunk PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0);
+ @Thunk ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>();
+
+ private Alarm mOpenAlarm = new Alarm();
+ @Thunk
+ ItemInfo mDragInfo;
+
+ public FolderIcon(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public FolderIcon(Context context) {
+ super(context);
+ init();
+ }
+
+ private void init() {
+ mLongPressHelper = new CheckLongPressHelper(this);
+ mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
+ mPreviewLayoutRule = FeatureFlags.LAUNCHER3_CLIPPED_FOLDER_ICON ?
+ new ClippedFolderIconLayoutRule() :
+ new StackFolderIconLayoutRule();
+
+ setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
+ }
+
+ public boolean isDropEnabled() {
+ final ViewGroup cellLayoutChildren = (ViewGroup) getParent();
+ final ViewGroup cellLayout = (ViewGroup) cellLayoutChildren.getParent();
+ final Workspace workspace = (Workspace) cellLayout.getParent();
+ return !workspace.workspaceInModalState();
+ }
+
+ public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
+ FolderInfo folderInfo, IconCache iconCache) {
+ @SuppressWarnings("all") // suppress dead code warning
+ final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
+ if (error) {
+ throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " +
+ "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " +
+ "is dependent on this");
+ }
+
+ DeviceProfile grid = launcher.getDeviceProfile();
+
+ FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false);
+ icon.setClipToPadding(false);
+ icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name);
+ icon.mFolderName.setText(folderInfo.title);
+ icon.mFolderName.setCompoundDrawablePadding(0);
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
+ lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
+
+ // Offset the preview background to center this view accordingly
+ icon.mPreviewBackground = (ImageView) icon.findViewById(R.id.preview_background);
+ lp = (FrameLayout.LayoutParams) icon.mPreviewBackground.getLayoutParams();
+ lp.topMargin = grid.folderBackgroundOffset;
+ lp.width = grid.folderIconSizePx;
+ lp.height = grid.folderIconSizePx;
+
+ icon.setTag(folderInfo);
+ icon.setOnClickListener(launcher);
+ icon.mInfo = folderInfo;
+ icon.mLauncher = launcher;
+ icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
+ Folder folder = Folder.fromXml(launcher);
+ folder.setDragController(launcher.getDragController());
+ folder.setFolderIcon(icon);
+ folder.bind(folderInfo);
+ icon.mFolder = folder;
+
+ icon.mFolderRingAnimator = new FolderRingAnimator(launcher, icon);
+ folderInfo.addListener(icon);
+
+ icon.setOnFocusChangeListener(launcher.mFocusHandler);
+ return icon;
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ sStaticValuesDirty = true;
+ return super.onSaveInstanceState();
+ }
+
+ public static class FolderRingAnimator {
+ public int mCellX;
+ public int mCellY;
+ @Thunk
+ CellLayout mCellLayout;
+ public float mOuterRingSize;
+ public float mInnerRingSize;
+ public FolderIcon mFolderIcon = null;
+ public static Drawable sSharedOuterRingDrawable = null;
+ public static Drawable sSharedInnerRingDrawable = null;
+ public static int sPreviewSize = -1;
+ public static int sPreviewPadding = -1;
+
+ private ValueAnimator mAcceptAnimator;
+ private ValueAnimator mNeutralAnimator;
+
+ public FolderRingAnimator(Launcher launcher, FolderIcon folderIcon) {
+ mFolderIcon = folderIcon;
+ Resources res = launcher.getResources();
+
+ // We need to reload the static values when configuration changes in case they are
+ // different in another configuration
+ if (sStaticValuesDirty) {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new RuntimeException("FolderRingAnimator loading drawables on non-UI thread "
+ + Thread.currentThread());
+ }
+
+ DeviceProfile grid = launcher.getDeviceProfile();
+ sPreviewSize = grid.folderIconSizePx;
+ sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding);
+ sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer);
+ sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_nolip);
+ sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest);
+ sStaticValuesDirty = false;
+ }
+ }
+
+ public void animateToAcceptState() {
+ if (mNeutralAnimator != null) {
+ mNeutralAnimator.cancel();
+ }
+ mAcceptAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f);
+ mAcceptAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
+
+ final int previewSize = sPreviewSize;
+ mAcceptAnimator.addUpdateListener(new AnimatorUpdateListener() {
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float percent = (Float) animation.getAnimatedValue();
+ mOuterRingSize = (1 + percent * OUTER_RING_GROWTH_FACTOR) * previewSize;
+ mInnerRingSize = (1 + percent * INNER_RING_GROWTH_FACTOR) * previewSize;
+ if (mCellLayout != null) {
+ mCellLayout.invalidate();
+ }
+ }
+ });
+ mAcceptAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mFolderIcon != null) {
+ mFolderIcon.mPreviewBackground.setVisibility(INVISIBLE);
+ }
+ }
+ });
+ mAcceptAnimator.start();
+ }
+
+ public void animateToNaturalState() {
+ if (mAcceptAnimator != null) {
+ mAcceptAnimator.cancel();
+ }
+ mNeutralAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f);
+ mNeutralAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
+
+ final int previewSize = sPreviewSize;
+ mNeutralAnimator.addUpdateListener(new AnimatorUpdateListener() {
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float percent = (Float) animation.getAnimatedValue();
+ mOuterRingSize = (1 + (1 - percent) * OUTER_RING_GROWTH_FACTOR) * previewSize;
+ mInnerRingSize = (1 + (1 - percent) * INNER_RING_GROWTH_FACTOR) * previewSize;
+ if (mCellLayout != null) {
+ mCellLayout.invalidate();
+ }
+ }
+ });
+ mNeutralAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCellLayout != null) {
+ mCellLayout.hideFolderAccept(FolderRingAnimator.this);
+ }
+ if (mFolderIcon != null) {
+ mFolderIcon.mPreviewBackground.setVisibility(VISIBLE);
+ }
+ }
+ });
+ mNeutralAnimator.start();
+ }
+
+ // Location is expressed in window coordinates
+ public void getCell(int[] loc) {
+ loc[0] = mCellX;
+ loc[1] = mCellY;
+ }
+
+ // Location is expressed in window coordinates
+ public void setCell(int x, int y) {
+ mCellX = x;
+ mCellY = y;
+ }
+
+ public void setCellLayout(CellLayout layout) {
+ mCellLayout = layout;
+ }
+
+ public float getOuterRingSize() {
+ return mOuterRingSize;
+ }
+
+ public float getInnerRingSize() {
+ return mInnerRingSize;
+ }
+ }
+
+ public Folder getFolder() {
+ return mFolder;
+ }
+
+ public FolderInfo getFolderInfo() {
+ return mInfo;
+ }
+
+ private boolean willAcceptItem(ItemInfo item) {
+ final int itemType = item.itemType;
+ return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
+ itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
+ !mFolder.isFull() && item != mInfo && !mInfo.opened);
+ }
+
+ public boolean acceptDrop(ItemInfo dragInfo) {
+ final ItemInfo item = dragInfo;
+ return !mFolder.isDestroyed() && willAcceptItem(item);
+ }
+
+ public void addItem(ShortcutInfo item) {
+ mInfo.add(item);
+ }
+
+ public void onDragEnter(ItemInfo dragInfo) {
+ if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return;
+ CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
+ CellLayout layout = (CellLayout) getParent().getParent();
+ mFolderRingAnimator.setCell(lp.cellX, lp.cellY);
+ mFolderRingAnimator.setCellLayout(layout);
+ mFolderRingAnimator.animateToAcceptState();
+ layout.showFolderAccept(mFolderRingAnimator);
+ mOpenAlarm.setOnAlarmListener(mOnOpenListener);
+ if (SPRING_LOADING_ENABLED &&
+ ((dragInfo instanceof AppInfo) || (dragInfo instanceof ShortcutInfo))) {
+ // TODO: we currently don't support spring-loading for PendingAddShortcutInfos even
+ // though widget-style shortcuts can be added to folders. The issue is that we need
+ // to deal with configuration activities which are currently handled in
+ // Workspace#onDropExternal.
+ mOpenAlarm.setAlarm(ON_OPEN_DELAY);
+ }
+ mDragInfo = dragInfo;
+ }
+
+ OnAlarmListener mOnOpenListener = new OnAlarmListener() {
+ public void onAlarm(Alarm alarm) {
+ ShortcutInfo item;
+ if (mDragInfo instanceof AppInfo) {
+ // Came from all apps -- make a copy.
+ item = ((AppInfo) mDragInfo).makeShortcut();
+ item.spanX = 1;
+ item.spanY = 1;
+ } else {
+ // ShortcutInfo
+ item = (ShortcutInfo) mDragInfo;
+ }
+ mFolder.beginExternalDrag(item);
+ mLauncher.openFolder(FolderIcon.this, true);
+ }
+ };
+
+ public void performCreateAnimation(final ShortcutInfo destInfo, final View destView,
+ final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect,
+ float scaleRelativeToDragLayer, Runnable postAnimationRunnable) {
+
+ // These correspond two the drawable and view that the icon was dropped _onto_
+ Drawable animateDrawable = getTopDrawable((TextView) destView);
+ computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
+ destView.getMeasuredWidth());
+
+ // This will animate the first item from it's position as an icon into its
+ // position as the first item in the preview
+ animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null);
+ addItem(destInfo);
+
+ // This will animate the dragView (srcView) into the new folder
+ onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable, null);
+ }
+
+ public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) {
+ Drawable animateDrawable = getTopDrawable((TextView) finalView);
+ computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(),
+ finalView.getMeasuredWidth());
+
+ // This will animate the first item from it's position as an icon into its
+ // position as the first item in the preview
+ animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true,
+ onCompleteRunnable);
+ }
+
+ public void onDragExit(Object dragInfo) {
+ onDragExit();
+ }
+
+ public void onDragExit() {
+ mFolderRingAnimator.animateToNaturalState();
+ mOpenAlarm.cancelAlarm();
+ }
+
+ private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect,
+ float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable,
+ DragObject d) {
+ item.cellX = -1;
+ item.cellY = -1;
+
+ // Typically, the animateView corresponds to the DragView; however, if this is being done
+ // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
+ // will not have a view to animate
+ if (animateView != null) {
+ DragLayer dragLayer = mLauncher.getDragLayer();
+ Rect from = new Rect();
+ dragLayer.getViewRectRelativeToSelf(animateView, from);
+ Rect to = finalRect;
+ if (to == null) {
+ to = new Rect();
+ Workspace workspace = mLauncher.getWorkspace();
+ // Set cellLayout and this to it's final state to compute final animation locations
+ workspace.setFinalTransitionTransform((CellLayout) getParent().getParent());
+ float scaleX = getScaleX();
+ float scaleY = getScaleY();
+ setScaleX(1.0f);
+ setScaleY(1.0f);
+ scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to);
+ // Finished computing final animation locations, restore current state
+ setScaleX(scaleX);
+ setScaleY(scaleY);
+ workspace.resetTransitionTransform((CellLayout) getParent().getParent());
+ }
+
+ int[] center = new int[2];
+ float scale = getLocalCenterForIndex(index, index + 1, center);
+ center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]);
+ center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]);
+
+ to.offset(center[0] - animateView.getMeasuredWidth() / 2,
+ center[1] - animateView.getMeasuredHeight() / 2);
+
+ float finalAlpha = index < mPreviewLayoutRule.numItems() ? 0.5f : 0f;
+
+ float finalScale = scale * scaleRelativeToDragLayer;
+ dragLayer.animateView(animateView, from, to, finalAlpha,
+ 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
+ new DecelerateInterpolator(2), new AccelerateInterpolator(2),
+ postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
+ addItem(item);
+ mHiddenItems.add(item);
+ mFolder.hideItem(item);
+ postDelayed(new Runnable() {
+ public void run() {
+ mHiddenItems.remove(item);
+ mFolder.showItem(item);
+ invalidate();
+ }
+ }, DROP_IN_ANIMATION_DURATION);
+ } else {
+ addItem(item);
+ }
+ }
+
+ public void onDrop(DragObject d) {
+ ShortcutInfo item;
+ if (d.dragInfo instanceof AppInfo) {
+ // Came from all apps -- make a copy
+ item = ((AppInfo) d.dragInfo).makeShortcut();
+ } else {
+ item = (ShortcutInfo) d.dragInfo;
+ }
+ mFolder.notifyDrop();
+ onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable, d);
+ }
+
+ private void computePreviewDrawingParams(int drawableSize, int totalSize) {
+ if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) {
+ DeviceProfile grid = mLauncher.getDeviceProfile();
+
+ mIntrinsicIconSize = drawableSize;
+ mTotalWidth = totalSize;
+
+ final int previewSize = FolderRingAnimator.sPreviewSize;
+ final int previewPadding = FolderRingAnimator.sPreviewPadding;
+
+ mAvailableSpaceInPreview = (previewSize - 2 * previewPadding);
+
+ mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2;
+ mPreviewOffsetY = previewPadding + grid.folderBackgroundOffset + getPaddingTop();
+
+ mPreviewLayoutRule.init(mAvailableSpaceInPreview, mIntrinsicIconSize,
+ Utilities.isRtl(getResources()));
+ }
+ }
+
+ private void computePreviewDrawingParams(Drawable d) {
+ computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth());
+ }
+
+ static class PreviewItemDrawingParams {
+ PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
+ this.transX = transX;
+ this.transY = transY;
+ this.scale = scale;
+ this.overlayAlpha = overlayAlpha;
+ }
+ float transX;
+ float transY;
+ float scale;
+ float overlayAlpha;
+ Drawable drawable;
+ }
+
+ private float getLocalCenterForIndex(int index, int curNumItems, int[] center) {
+ mParams = computePreviewItemDrawingParams(Math.min(mPreviewLayoutRule.numItems(), index),
+ curNumItems, mParams);
+
+ mParams.transX += mPreviewOffsetX;
+ mParams.transY += mPreviewOffsetY;
+ float offsetX = mParams.transX + (mParams.scale * mIntrinsicIconSize) / 2;
+ float offsetY = mParams.transY + (mParams.scale * mIntrinsicIconSize) / 2;
+
+ center[0] = (int) Math.round(offsetX);
+ center[1] = (int) Math.round(offsetY);
+ return mParams.scale;
+ }
+
+ private PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
+ PreviewItemDrawingParams params) {
+ return mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params);
+ }
+
+ private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) {
+ canvas.save();
+ canvas.translate(params.transX, params.transY);
+ canvas.scale(params.scale, params.scale);
+ Drawable d = params.drawable;
+
+ if (d != null) {
+ mOldBounds.set(d.getBounds());
+ d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
+ if (d instanceof FastBitmapDrawable) {
+ FastBitmapDrawable fd = (FastBitmapDrawable) d;
+ float oldBrightness = fd.getBrightness();
+ fd.setBrightness(params.overlayAlpha);
+ d.draw(canvas);
+ fd.setBrightness(oldBrightness);
+ } else {
+ d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255),
+ PorterDuff.Mode.SRC_ATOP);
+ d.draw(canvas);
+ d.clearColorFilter();
+ }
+ d.setBounds(mOldBounds);
+ }
+ canvas.restore();
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+
+ if (mFolder == null) return;
+ if (mFolder.getItemCount() == 0 && !mAnimating) return;
+
+ ArrayList<View> items = mFolder.getItemsInReadingOrder();
+ Drawable d;
+ TextView v;
+
+ // Update our drawing parameters if necessary
+ if (mAnimating) {
+ computePreviewDrawingParams(mAnimParams.drawable);
+ } else {
+ v = (TextView) items.get(0);
+ d = getTopDrawable(v);
+ computePreviewDrawingParams(d);
+ }
+
+ canvas.save();
+ canvas.translate(mPreviewOffsetX, mPreviewOffsetY);
+ Path clipPath = mPreviewLayoutRule.getClipPath();
+ if (clipPath != null) {
+ canvas.clipPath(clipPath);
+ }
+
+ int nItemsInPreview = Math.min(items.size(), mPreviewLayoutRule.numItems());
+ if (!mAnimating) {
+ for (int i = nItemsInPreview - 1; i >= 0; i--) {
+ v = (TextView) items.get(i);
+ if (!mHiddenItems.contains(v.getTag())) {
+ d = getTopDrawable(v);
+ mParams = computePreviewItemDrawingParams(i, nItemsInPreview, mParams);
+ mParams.drawable = d;
+ drawPreviewItem(canvas, mParams);
+ }
+ }
+ } else {
+ drawPreviewItem(canvas, mAnimParams);
+ }
+ canvas.restore();
+ }
+
+ private Drawable getTopDrawable(TextView v) {
+ Drawable d = v.getCompoundDrawables()[1];
+ return (d instanceof PreloadIconDrawable) ? ((PreloadIconDrawable) d).mIcon : d;
+ }
+
+ private void animateFirstItem(final Drawable d, int duration, final boolean reverse,
+ final Runnable onCompleteRunnable) {
+
+ final PreviewItemDrawingParams finalParams =
+ computePreviewItemDrawingParams(0, reverse ? 1 : 2, null);
+
+ float iconSize = mLauncher.getDeviceProfile().iconSizePx;
+ final float scale0 = iconSize / d.getIntrinsicWidth() ;
+ final float transX0 = (mAvailableSpaceInPreview - iconSize) / 2;
+ final float transY0 = (mAvailableSpaceInPreview - iconSize) / 2;
+ mAnimParams.drawable = d;
+
+ ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f);
+ va.addUpdateListener(new AnimatorUpdateListener(){
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float progress = (Float) animation.getAnimatedValue();
+ if (reverse) {
+ progress = 1 - progress;
+ mPreviewBackground.setAlpha(progress);
+ }
+
+ mAnimParams.transX = transX0 + progress * (finalParams.transX - transX0);
+ mAnimParams.transY = transY0 + progress * (finalParams.transY - transY0);
+ mAnimParams.scale = scale0 + progress * (finalParams.scale - scale0);
+ invalidate();
+ }
+ });
+ va.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mAnimating = true;
+ }
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimating = false;
+ if (onCompleteRunnable != null) {
+ onCompleteRunnable.run();
+ }
+ }
+ });
+ va.setDuration(duration);
+ va.start();
+ }
+
+ public void setTextVisible(boolean visible) {
+ if (visible) {
+ mFolderName.setVisibility(VISIBLE);
+ } else {
+ mFolderName.setVisibility(INVISIBLE);
+ }
+ }
+
+ public boolean getTextVisible() {
+ return mFolderName.getVisibility() == VISIBLE;
+ }
+
+ public void onItemsChanged() {
+ invalidate();
+ requestLayout();
+ }
+
+ public void onAdd(ShortcutInfo item) {
+ invalidate();
+ requestLayout();
+ }
+
+ public void onRemove(ShortcutInfo item) {
+ invalidate();
+ requestLayout();
+ }
+
+ public void onTitleChanged(CharSequence title) {
+ mFolderName.setText(title);
+ setContentDescription(getContext().getString(R.string.folder_name_format, title));
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Call the superclass onTouchEvent first, because sometimes it changes the state to
+ // isPressed() on an ACTION_UP
+ boolean result = super.onTouchEvent(event);
+
+ // Check for a stylus button press, if it occurs cancel any long press checks.
+ if (mStylusEventHelper.onMotionEvent(event)) {
+ mLongPressHelper.cancelLongPress();
+ return true;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mLongPressHelper.postCheckForLongPress();
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ mLongPressHelper.cancelLongPress();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) {
+ mLongPressHelper.cancelLongPress();
+ }
+ break;
+ }
+ return result;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ }
+
+ @Override
+ public void cancelLongPress() {
+ super.cancelLongPress();
+ mLongPressHelper.cancelLongPress();
+ }
+
+ public interface PreviewLayoutRule {
+ public PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
+ PreviewItemDrawingParams params);
+
+ public void init(int availableSpace, int intrinsicIconSize, boolean rtl);
+
+ public int numItems();
+ public Path getClipPath();
+ }
+}
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 7fc5d2b04..c25444e06 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -33,8 +33,6 @@ import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
import com.android.launcher3.FocusIndicatorView;
-import com.android.launcher3.Folder;
-import com.android.launcher3.FolderIcon;
import com.android.launcher3.IconCache;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
diff --git a/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java b/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java
index 01eeecd43..87f5f897b 100644
--- a/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java
+++ b/src/com/android/launcher3/folder/StackFolderIconLayoutRule.java
@@ -18,12 +18,11 @@ package com.android.launcher3.folder;
import android.graphics.Path;
-import com.android.launcher3.FolderIcon;
-import com.android.launcher3.FolderIcon.PreviewItemDrawingParams;
+import com.android.launcher3.folder.FolderIcon.PreviewItemDrawingParams;
public class StackFolderIconLayoutRule implements FolderIcon.PreviewLayoutRule {
- public static final int MAX_NUM_ITEMS_IN_PREVIEW = 3;
+ static final int MAX_NUM_ITEMS_IN_PREVIEW = 3;
// The degree to which the item in the back of the stack is scaled [0...1]
// (0 means it's not scaled at all, 1 means it's scaled to nothing)