2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.launcher3;
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.animation.PropertyValuesHolder;
24 import android.annotation.SuppressLint;
25 import android.annotation.TargetApi;
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.graphics.Point;
29 import android.graphics.PointF;
30 import android.graphics.Rect;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.text.InputType;
34 import android.text.Selection;
35 import android.text.Spannable;
36 import android.util.AttributeSet;
37 import android.util.Log;
38 import android.view.ActionMode;
39 import android.view.KeyEvent;
40 import android.view.Menu;
41 import android.view.MenuItem;
42 import android.view.MotionEvent;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.view.accessibility.AccessibilityEvent;
46 import android.view.accessibility.AccessibilityManager;
47 import android.view.animation.AccelerateInterpolator;
48 import android.view.animation.AnimationUtils;
49 import android.view.inputmethod.EditorInfo;
50 import android.view.inputmethod.InputMethodManager;
51 import android.widget.LinearLayout;
52 import android.widget.TextView;
54 import com.android.launcher3.CellLayout.CellInfo;
55 import com.android.launcher3.DragController.DragListener;
56 import com.android.launcher3.FolderInfo.FolderListener;
57 import com.android.launcher3.UninstallDropTarget.UninstallSource;
58 import com.android.launcher3.Workspace.ItemOperator;
59 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource;
60 import com.android.launcher3.util.Thunk;
61 import com.android.launcher3.util.UiThreadCircularReveal;
63 import java.util.ArrayList;
64 import java.util.Collections;
65 import java.util.Comparator;
68 * Represents a set of icons chosen by the user or generated by the system.
70 public class Folder extends LinearLayout implements DragSource, View.OnClickListener,
71 View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
72 View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource,
73 Stats.LaunchSourceProvider {
74 private static final String TAG = "Launcher.Folder";
77 * We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this
78 * results in CellLayout being measured as UNSPECIFIED, which it does not support.
80 private static final int MIN_CONTENT_DIMEN = 5;
82 static final int STATE_NONE = -1;
83 static final int STATE_SMALL = 0;
84 static final int STATE_ANIMATING = 1;
85 static final int STATE_OPEN = 2;
88 * Time for which the scroll hint is shown before automatically changing page.
90 public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY;
93 * Fraction of icon width which behave as scroll region.
95 private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
97 private static final int FOLDER_NAME_ANIMATION_DURATION = 633;
99 private static final int REORDER_DELAY = 250;
100 private static final int ON_EXIT_CLOSE_DELAY = 400;
101 private static final Rect sTempRect = new Rect();
103 private static String sDefaultFolderName;
104 private static String sHintText;
106 private final Alarm mReorderAlarm = new Alarm();
107 private final Alarm mOnExitAlarm = new Alarm();
108 private final Alarm mOnScrollHintAlarm = new Alarm();
109 @Thunk final Alarm mScrollPauseAlarm = new Alarm();
111 @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
113 private final int mExpandDuration;
114 private final int mMaterialExpandDuration;
115 private final int mMaterialExpandStagger;
117 private final InputMethodManager mInputMethodManager;
119 protected final Launcher mLauncher;
120 protected DragController mDragController;
121 protected FolderInfo mInfo;
123 @Thunk FolderIcon mFolderIcon;
125 @Thunk FolderPagedView mContent;
126 @Thunk View mContentWrapper;
127 ExtendedEditText mFolderName;
129 private View mFooter;
130 private int mFooterHeight;
132 // Cell ranks used for drag and drop
133 @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank;
135 @Thunk int mState = STATE_NONE;
136 private boolean mRearrangeOnClose = false;
137 boolean mItemsInvalidated = false;
138 private ShortcutInfo mCurrentDragInfo;
139 private View mCurrentDragView;
140 private boolean mIsExternalDrag;
141 boolean mSuppressOnAdd = false;
142 private boolean mDragInProgress = false;
143 private boolean mDeleteFolderOnDropCompleted = false;
144 private boolean mSuppressFolderDeletion = false;
145 private boolean mItemAddedBackToSelfViaIcon = false;
146 @Thunk float mFolderIconPivotX;
147 @Thunk float mFolderIconPivotY;
148 private boolean mIsEditingName = false;
150 private boolean mDestroyed;
152 @Thunk Runnable mDeferredAction;
153 private boolean mDeferDropAfterUninstall;
154 private boolean mUninstallSuccessful;
157 private int mScrollAreaOffset;
159 @Thunk int mScrollHintDir = DragController.SCROLL_NONE;
160 @Thunk int mCurrentScrollDir = DragController.SCROLL_NONE;
163 * Used to inflate the Workspace from XML.
165 * @param context The application's context.
166 * @param attrs The attributes set containing the Workspace's customization values.
168 public Folder(Context context, AttributeSet attrs) {
169 super(context, attrs);
170 setAlwaysDrawnWithCacheEnabled(false);
171 mInputMethodManager = (InputMethodManager)
172 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
174 Resources res = getResources();
175 mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration);
176 mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration);
177 mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger);
179 if (sDefaultFolderName == null) {
180 sDefaultFolderName = res.getString(R.string.folder_name);
182 if (sHintText == null) {
183 sHintText = res.getString(R.string.folder_hint_text);
185 mLauncher = (Launcher) context;
186 // We need this view to be focusable in touch mode so that when text editing of the folder
187 // name is complete, we have something to focus on, thus hiding the cursor and giving
188 // reliable behavior when clicking the text field (since it will always gain focus on click).
189 setFocusableInTouchMode(true);
193 protected void onFinishInflate() {
194 super.onFinishInflate();
195 mContentWrapper = findViewById(R.id.folder_content_wrapper);
196 mContent = (FolderPagedView) findViewById(R.id.folder_content);
197 mContent.setFolder(this);
199 mFolderName = (ExtendedEditText) findViewById(R.id.folder_name);
200 mFolderName.setOnBackKeyListener(new ExtendedEditText.OnBackKeyListener() {
202 public boolean onBackKey() {
203 // Close the activity on back key press
204 doneEditingFolderName(true);
208 mFolderName.setOnFocusChangeListener(this);
210 // We disable action mode for now since it messes up the view on phones
211 mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback);
212 mFolderName.setOnEditorActionListener(this);
213 mFolderName.setSelectAllOnFocus(true);
214 mFolderName.setInputType(mFolderName.getInputType() |
215 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
217 mFooter = findViewById(R.id.folder_footer);
219 // We find out how tall footer wants to be (it is set to wrap_content), so that
220 // we can allocate the appropriate amount of space for it.
221 int measureSpec = MeasureSpec.UNSPECIFIED;
222 mFooter.measure(measureSpec, measureSpec);
223 mFooterHeight = mFooter.getMeasuredHeight();
226 private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
227 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
231 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
235 public void onDestroyActionMode(ActionMode mode) {
238 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
243 public void onClick(View v) {
244 Object tag = v.getTag();
245 if (tag instanceof ShortcutInfo) {
246 mLauncher.onClick(v);
250 public boolean onLongClick(View v) {
251 // Return if global dragging is not enabled
252 if (!mLauncher.isDraggingEnabled()) return true;
253 return beginDrag(v, false);
256 private boolean beginDrag(View v, boolean accessible) {
257 Object tag = v.getTag();
258 if (tag instanceof ShortcutInfo) {
259 ShortcutInfo item = (ShortcutInfo) tag;
260 if (!v.isInTouchMode()) {
264 mLauncher.getWorkspace().beginDragShared(v, new Point(), this, accessible);
266 mCurrentDragInfo = item;
267 mEmptyCellRank = item.rank;
268 mCurrentDragView = v;
270 mContent.removeItem(mCurrentDragView);
271 mInfo.remove(mCurrentDragInfo);
272 mDragInProgress = true;
273 mItemAddedBackToSelfViaIcon = false;
279 public void startDrag(CellInfo cellInfo, boolean accessible) {
280 beginDrag(cellInfo.cell, accessible);
284 public void enableAccessibleDrag(boolean enable) {
285 mLauncher.getSearchDropTargetBar().enableAccessibleDrag(enable);
286 for (int i = 0; i < mContent.getChildCount(); i++) {
287 mContent.getPageAt(i).enableAccessibleDrag(enable, CellLayout.FOLDER_ACCESSIBILITY_DRAG);
290 mFooter.setImportantForAccessibility(enable ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS :
291 IMPORTANT_FOR_ACCESSIBILITY_AUTO);
292 mLauncher.getWorkspace().setAddNewPageOnDrag(!enable);
295 public boolean isEditingName() {
296 return mIsEditingName;
299 public void startEditingFolderName() {
300 mFolderName.setHint("");
301 mIsEditingName = true;
304 public void dismissEditingName() {
305 mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
306 doneEditingFolderName(true);
309 public void doneEditingFolderName(boolean commit) {
310 mFolderName.setHint(sHintText);
311 // Convert to a string here to ensure that no other state associated with the text field
313 String newTitle = mFolderName.getText().toString();
314 mInfo.setTitle(newTitle);
315 LauncherModel.updateItemInDatabase(mLauncher, mInfo);
318 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
319 String.format(getContext().getString(R.string.folder_renamed), newTitle));
321 // In order to clear the focus from the text field, we set the focus on ourself. This
322 // ensures that every time the field is clicked, focus is gained, giving reliable behavior.
325 Selection.setSelection((Spannable) mFolderName.getText(), 0, 0);
326 mIsEditingName = false;
329 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
330 if (actionId == EditorInfo.IME_ACTION_DONE) {
331 dismissEditingName();
337 public View getEditTextRegion() {
342 * We need to handle touch events to prevent them from falling through to the workspace below.
344 @SuppressLint("ClickableViewAccessibility")
346 public boolean onTouchEvent(MotionEvent ev) {
350 public void setDragController(DragController dragController) {
351 mDragController = dragController;
354 public void setFolderIcon(FolderIcon icon) {
359 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
360 // When the folder gets focus, we don't want to announce the list of items.
365 * @return the FolderInfo object associated with this folder
367 public FolderInfo getInfo() {
371 void bind(FolderInfo info) {
373 ArrayList<ShortcutInfo> children = info.contents;
374 Collections.sort(children, ITEM_POS_COMPARATOR);
376 ArrayList<ShortcutInfo> overflow = mContent.bindItems(children);
378 // If our folder has too many items we prune them from the list. This is an issue
379 // when upgrading from the old Folders implementation which could contain an unlimited
381 for (ShortcutInfo item: overflow) {
383 LauncherModel.deleteItemFromDatabase(mLauncher, item);
386 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
388 lp = new DragLayer.LayoutParams(0, 0);
389 lp.customPosition = true;
394 mItemsInvalidated = true;
395 updateTextViewFocus();
396 mInfo.addListener(this);
398 if (!sDefaultFolderName.contentEquals(mInfo.title)) {
399 mFolderName.setText(mInfo.title);
401 mFolderName.setText("");
404 // In case any children didn't come across during loading, clean up the folder accordingly
405 mFolderIcon.post(new Runnable() {
407 if (getItemCount() <= 1) {
408 replaceFolderWithFinalItem();
415 * Creates a new UserFolder, inflated from R.layout.user_folder.
417 * @param context The application's context.
419 * @return A new UserFolder.
421 @SuppressLint("InflateParams")
422 static Folder fromXml(Launcher launcher) {
423 return (Folder) launcher.getLayoutInflater().inflate(R.layout.user_folder, null);
427 * This method is intended to make the UserFolder to be visually identical in size and position
428 * to its associated FolderIcon. This allows for a seamless transition into the expanded state.
430 private void positionAndSizeAsIcon() {
431 if (!(getParent() instanceof DragLayer)) return;
435 mState = STATE_SMALL;
438 private void prepareReveal() {
442 mState = STATE_SMALL;
445 public void animateOpen() {
446 if (!(getParent() instanceof DragLayer)) return;
448 mContent.completePendingPageChanges();
449 if (!mDragInProgress) {
450 // Open on the first page.
451 mContent.snapToPageImmediately(0);
454 Animator openFolderAnim = null;
455 final Runnable onCompleteRunnable;
456 if (!Utilities.isLmpOrAbove()) {
457 positionAndSizeAsIcon();
460 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1);
461 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f);
462 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f);
463 final ObjectAnimator oa =
464 LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
465 oa.setDuration(mExpandDuration);
468 setLayerType(LAYER_TYPE_HARDWARE, null);
469 onCompleteRunnable = new Runnable() {
472 setLayerType(LAYER_TYPE_NONE, null);
479 AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
480 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
481 int height = getFolderHeight();
483 float transX = - 0.075f * (width / 2 - getPivotX());
484 float transY = - 0.075f * (height / 2 - getPivotY());
485 setTranslationX(transX);
486 setTranslationY(transY);
487 PropertyValuesHolder tx = PropertyValuesHolder.ofFloat("translationX", transX, 0);
488 PropertyValuesHolder ty = PropertyValuesHolder.ofFloat("translationY", transY, 0);
490 Animator drift = ObjectAnimator.ofPropertyValuesHolder(this, tx, ty);
491 drift.setDuration(mMaterialExpandDuration);
492 drift.setStartDelay(mMaterialExpandStagger);
493 drift.setInterpolator(new LogDecelerateInterpolator(100, 0));
495 int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX());
496 int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY());
497 float radius = (float) Math.hypot(rx, ry);
499 Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(),
500 (int) getPivotY(), 0, radius);
501 reveal.setDuration(mMaterialExpandDuration);
502 reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
504 mContentWrapper.setAlpha(0f);
505 Animator iconsAlpha = ObjectAnimator.ofFloat(mContentWrapper, "alpha", 0f, 1f);
506 iconsAlpha.setDuration(mMaterialExpandDuration);
507 iconsAlpha.setStartDelay(mMaterialExpandStagger);
508 iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
510 mFooter.setAlpha(0f);
511 Animator textAlpha = ObjectAnimator.ofFloat(mFooter, "alpha", 0f, 1f);
512 textAlpha.setDuration(mMaterialExpandDuration);
513 textAlpha.setStartDelay(mMaterialExpandStagger);
514 textAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
517 anim.play(iconsAlpha);
518 anim.play(textAlpha);
521 openFolderAnim = anim;
523 mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null);
524 mFooter.setLayerType(LAYER_TYPE_HARDWARE, null);
525 onCompleteRunnable = new Runnable() {
528 mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
529 mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
533 openFolderAnim.addListener(new AnimatorListenerAdapter() {
535 public void onAnimationStart(Animator animation) {
536 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
537 mContent.getAccessibilityDescription());
538 mState = STATE_ANIMATING;
541 public void onAnimationEnd(Animator animation) {
544 if (onCompleteRunnable != null) {
545 onCompleteRunnable.run();
548 mContent.setFocusOnFirstChild();
553 if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) {
554 int footerWidth = mContent.getDesiredWidth()
555 - mFooter.getPaddingLeft() - mFooter.getPaddingRight();
557 float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString());
558 float translation = (footerWidth - textWidth) / 2;
559 mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation);
560 mContent.setMarkerScale(0);
562 // Do not update the flag if we are in drag mode. The flag will be updated, when we
563 // actually drop the icon.
564 final boolean updateAnimationFlag = !mDragInProgress;
565 openFolderAnim.addListener(new AnimatorListenerAdapter() {
568 public void onAnimationEnd(Animator animation) {
569 mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION)
571 .setInterpolator(Utilities.isLmpOrAbove() ?
572 AnimationUtils.loadInterpolator(mLauncher,
573 android.R.interpolator.fast_out_slow_in)
574 : new LogDecelerateInterpolator(100, 0));
575 mContent.animateMarkers();
577 if (updateAnimationFlag) {
578 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
583 mFolderName.setTranslationX(0);
584 mContent.setMarkerScale(1);
587 openFolderAnim.start();
589 // Make sure the folder picks up the last drag move even if the finger doesn't move.
590 if (mDragController.isDragging()) {
591 mDragController.forceTouchMove();
594 FolderPagedView pages = (FolderPagedView) mContent;
595 pages.verifyVisibleHighResIcons(pages.getNextPage());
598 public void beginExternalDrag(ShortcutInfo item) {
599 mCurrentDragInfo = item;
600 mEmptyCellRank = mContent.allocateRankForNewItem(item);
601 mIsExternalDrag = true;
602 mDragInProgress = true;
604 // Since this folder opened by another controller, it might not get onDrop or
605 // onDropComplete. Perform cleanup once drag-n-drop ends.
606 mDragController.addDragListener(this);
610 public void onDragStart(DragSource source, Object info, int dragAction) { }
613 public void onDragEnd() {
614 if (mIsExternalDrag && mDragInProgress) {
617 mDragController.removeDragListener(this);
620 @Thunk void sendCustomAccessibilityEvent(int type, String text) {
621 AccessibilityManager accessibilityManager = (AccessibilityManager)
622 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
623 if (accessibilityManager.isEnabled()) {
624 AccessibilityEvent event = AccessibilityEvent.obtain(type);
625 onInitializeAccessibilityEvent(event);
626 event.getText().add(text);
627 accessibilityManager.sendAccessibilityEvent(event);
631 public void animateClosed() {
632 if (!(getParent() instanceof DragLayer)) return;
633 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0);
634 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f);
635 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f);
636 final ObjectAnimator oa =
637 LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY);
639 oa.addListener(new AnimatorListenerAdapter() {
641 public void onAnimationEnd(Animator animation) {
643 setLayerType(LAYER_TYPE_NONE, null);
644 mState = STATE_SMALL;
647 public void onAnimationStart(Animator animation) {
648 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
649 getContext().getString(R.string.folder_closed));
650 mState = STATE_ANIMATING;
653 oa.setDuration(mExpandDuration);
654 setLayerType(LAYER_TYPE_HARDWARE, null);
658 public boolean acceptDrop(DragObject d) {
659 final ItemInfo item = (ItemInfo) d.dragInfo;
660 final int itemType = item.itemType;
661 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
662 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) &&
666 public void onDragEnter(DragObject d) {
667 mPrevTargetRank = -1;
668 mOnExitAlarm.cancelAlarm();
669 // Get the area offset such that the folder only closes if half the drag icon width
670 // is outside the folder area
671 mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset;
674 OnAlarmListener mReorderAlarmListener = new OnAlarmListener() {
675 public void onAlarm(Alarm alarm) {
676 mContent.realTimeReorder(mEmptyCellRank, mTargetRank);
677 mEmptyCellRank = mTargetRank;
681 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
682 public boolean isLayoutRtl() {
683 return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
687 public void onDragOver(DragObject d) {
688 onDragOver(d, REORDER_DELAY);
691 private int getTargetRank(DragObject d, float[] recycle) {
692 recycle = d.getVisualCenter(recycle);
693 return mContent.findNearestArea(
694 (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop());
697 @Thunk void onDragOver(DragObject d, int reorderDelay) {
698 if (mScrollPauseAlarm.alarmPending()) {
701 final float[] r = new float[2];
702 mTargetRank = getTargetRank(d, r);
704 if (mTargetRank != mPrevTargetRank) {
705 mReorderAlarm.cancelAlarm();
706 mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
707 mReorderAlarm.setAlarm(REORDER_DELAY);
708 mPrevTargetRank = mTargetRank;
712 int currentPage = mContent.getNextPage();
714 float cellOverlap = mContent.getCurrentCellLayout().getCellWidth()
715 * ICON_OVERSCROLL_WIDTH_FACTOR;
716 boolean isOutsideLeftEdge = x < cellOverlap;
717 boolean isOutsideRightEdge = x > (getWidth() - cellOverlap);
719 if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) {
720 showScrollHint(DragController.SCROLL_LEFT, d);
721 } else if (currentPage < (mContent.getPageCount() - 1)
722 && (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) {
723 showScrollHint(DragController.SCROLL_RIGHT, d);
725 mOnScrollHintAlarm.cancelAlarm();
726 if (mScrollHintDir != DragController.SCROLL_NONE) {
727 mContent.clearScrollHint();
728 mScrollHintDir = DragController.SCROLL_NONE;
733 private void showScrollHint(int direction, DragObject d) {
734 // Show scroll hint on the right
735 if (mScrollHintDir != direction) {
736 mContent.showScrollHint(direction);
737 mScrollHintDir = direction;
740 // Set alarm for when the hint is complete
741 if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) {
742 mCurrentScrollDir = direction;
743 mOnScrollHintAlarm.cancelAlarm();
744 mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d));
745 mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION);
747 mReorderAlarm.cancelAlarm();
748 mTargetRank = mEmptyCellRank;
752 OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
753 public void onAlarm(Alarm alarm) {
758 public void completeDragExit() {
760 mLauncher.closeFolder();
761 mRearrangeOnClose = true;
762 } else if (mState == STATE_ANIMATING) {
763 mRearrangeOnClose = true;
770 private void clearDragInfo() {
771 mCurrentDragInfo = null;
772 mCurrentDragView = null;
773 mSuppressOnAdd = false;
774 mIsExternalDrag = false;
777 public void onDragExit(DragObject d) {
778 // We only close the folder if this is a true drag exit, ie. not because
779 // a drop has occurred above the folder.
780 if (!d.dragComplete) {
781 mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener);
782 mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
784 mReorderAlarm.cancelAlarm();
786 mOnScrollHintAlarm.cancelAlarm();
787 mScrollPauseAlarm.cancelAlarm();
788 if (mScrollHintDir != DragController.SCROLL_NONE) {
789 mContent.clearScrollHint();
790 mScrollHintDir = DragController.SCROLL_NONE;
795 * When performing an accessibility drop, onDrop is sent immediately after onDragEnter. So we
796 * need to complete all transient states based on timers.
799 public void prepareAccessibilityDrop() {
800 if (mReorderAlarm.alarmPending()) {
801 mReorderAlarm.cancelAlarm();
802 mReorderAlarmListener.onAlarm(mReorderAlarm);
806 public void onDropCompleted(final View target, final DragObject d,
807 final boolean isFlingToDelete, final boolean success) {
808 if (mDeferDropAfterUninstall) {
809 Log.d(TAG, "Deferred handling drop because waiting for uninstall.");
810 mDeferredAction = new Runnable() {
812 onDropCompleted(target, d, isFlingToDelete, success);
813 mDeferredAction = null;
819 boolean beingCalledAfterUninstall = mDeferredAction != null;
820 boolean successfulDrop =
821 success && (!beingCalledAfterUninstall || mUninstallSuccessful);
823 if (successfulDrop) {
824 if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
825 replaceFolderWithFinalItem();
828 // The drag failed, we need to return the item to the folder
829 ShortcutInfo info = (ShortcutInfo) d.dragInfo;
830 View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info)
831 ? mCurrentDragView : mContent.createNewView(info);
832 ArrayList<View> views = getItemsInReadingOrder();
833 views.add(info.rank, icon);
834 mContent.arrangeChildren(views, views.size());
835 mItemsInvalidated = true;
837 mSuppressOnAdd = true;
838 mFolderIcon.onDrop(d);
839 mSuppressOnAdd = false;
842 if (target != this) {
843 if (mOnExitAlarm.alarmPending()) {
844 mOnExitAlarm.cancelAlarm();
845 if (!successfulDrop) {
846 mSuppressFolderDeletion = true;
848 mScrollPauseAlarm.cancelAlarm();
853 mDeleteFolderOnDropCompleted = false;
854 mDragInProgress = false;
855 mItemAddedBackToSelfViaIcon = false;
856 mCurrentDragInfo = null;
857 mCurrentDragView = null;
858 mSuppressOnAdd = false;
860 // Reordering may have occured, and we need to save the new item locations. We do this once
861 // at the end to prevent unnecessary database operations.
862 updateItemLocationsInDatabaseBatch();
864 // Use the item count to check for multi-page as the folder UI may not have
865 // been refreshed yet.
866 if (getItemCount() <= mContent.itemsPerPage()) {
867 // Show the animation, next time something is added to the folder.
868 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher);
874 public void deferCompleteDropAfterUninstallActivity() {
875 mDeferDropAfterUninstall = true;
879 public void onUninstallActivityReturned(boolean success) {
880 mDeferDropAfterUninstall = false;
881 mUninstallSuccessful = success;
882 if (mDeferredAction != null) {
883 mDeferredAction.run();
888 public float getIntrinsicIconScaleFactor() {
893 public boolean supportsFlingToDelete() {
898 public boolean supportsAppInfoDropTarget() {
903 public boolean supportsDeleteDropTarget() {
908 public void onFlingToDelete(DragObject d, PointF vec) {
913 public void onFlingToDeleteCompleted() {
917 private void updateItemLocationsInDatabaseBatch() {
918 ArrayList<View> list = getItemsInReadingOrder();
919 ArrayList<ItemInfo> items = new ArrayList<ItemInfo>();
920 for (int i = 0; i < list.size(); i++) {
921 View v = list.get(i);
922 ItemInfo info = (ItemInfo) v.getTag();
927 LauncherModel.moveItemsInDatabase(mLauncher, items, mInfo.id, 0);
930 public void addItemLocationsInDatabase() {
931 ArrayList<View> list = getItemsInReadingOrder();
932 for (int i = 0; i < list.size(); i++) {
933 View v = list.get(i);
934 ItemInfo info = (ItemInfo) v.getTag();
935 LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0,
936 info.cellX, info.cellY);
940 public void notifyDrop() {
941 if (mDragInProgress) {
942 mItemAddedBackToSelfViaIcon = true;
946 public boolean isDropEnabled() {
950 public boolean isFull() {
951 return mContent.isFull();
954 private void centerAboutIcon() {
955 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
957 DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer);
958 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
959 int height = getFolderHeight();
961 float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect);
963 DeviceProfile grid = mLauncher.getDeviceProfile();
965 int centerX = (int) (sTempRect.left + sTempRect.width() * scale / 2);
966 int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2);
967 int centeredLeft = centerX - width / 2;
968 int centeredTop = centerY - height / 2;
970 // We need to bound the folder to the currently visible workspace area
971 mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect);
972 int left = Math.min(Math.max(sTempRect.left, centeredLeft),
973 sTempRect.left + sTempRect.width() - width);
974 int top = Math.min(Math.max(sTempRect.top, centeredTop),
975 sTempRect.top + sTempRect.height() - height);
976 if (grid.isPhone && (grid.availableWidthPx - width) < grid.iconSizePx) {
977 // Center the folder if it is full (on phones only)
978 left = (grid.availableWidthPx - width) / 2;
979 } else if (width >= sTempRect.width()) {
980 // If the folder doesn't fit within the bounds, center it about the desired bounds
981 left = sTempRect.left + (sTempRect.width() - width) / 2;
983 if (height >= sTempRect.height()) {
984 top = sTempRect.top + (sTempRect.height() - height) / 2;
987 int folderPivotX = width / 2 + (centeredLeft - left);
988 int folderPivotY = height / 2 + (centeredTop - top);
989 setPivotX(folderPivotX);
990 setPivotY(folderPivotY);
991 mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() *
992 (1.0f * folderPivotX / width));
993 mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() *
994 (1.0f * folderPivotY / height));
1002 float getPivotXForIconAnimation() {
1003 return mFolderIconPivotX;
1005 float getPivotYForIconAnimation() {
1006 return mFolderIconPivotY;
1009 private int getContentAreaHeight() {
1010 DeviceProfile grid = mLauncher.getDeviceProfile();
1011 Rect workspacePadding = grid.getWorkspacePadding(mContent.mIsRtl);
1012 int maxContentAreaHeight = grid.availableHeightPx -
1013 workspacePadding.top - workspacePadding.bottom -
1015 int height = Math.min(maxContentAreaHeight,
1016 mContent.getDesiredHeight());
1017 return Math.max(height, MIN_CONTENT_DIMEN);
1020 private int getContentAreaWidth() {
1021 return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
1024 private int getFolderHeight() {
1025 return getFolderHeight(getContentAreaHeight());
1028 private int getFolderHeight(int contentAreaHeight) {
1029 return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
1032 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1033 int contentWidth = getContentAreaWidth();
1034 int contentHeight = getContentAreaHeight();
1036 int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY);
1037 int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY);
1039 mContent.setFixedSize(contentWidth, contentHeight);
1040 mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec);
1042 if (mContent.getChildCount() > 0) {
1043 int cellIconGap = (mContent.getPageAt(0).getCellWidth()
1044 - mLauncher.getDeviceProfile().iconSizePx) / 2;
1045 mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap,
1046 mFooter.getPaddingTop(),
1047 mContent.getPaddingRight() + cellIconGap,
1048 mFooter.getPaddingBottom());
1050 mFooter.measure(contentAreaWidthSpec,
1051 MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY));
1053 int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
1054 int folderHeight = getFolderHeight(contentHeight);
1055 setMeasuredDimension(folderWidth, folderHeight);
1059 * Rearranges the children based on their rank.
1061 public void rearrangeChildren() {
1062 rearrangeChildren(-1);
1066 * Rearranges the children based on their rank.
1067 * @param itemCount if greater than the total children count, empty spaces are left at the end,
1068 * otherwise it is ignored.
1070 public void rearrangeChildren(int itemCount) {
1071 ArrayList<View> views = getItemsInReadingOrder();
1072 mContent.arrangeChildren(views, Math.max(itemCount, views.size()));
1073 mItemsInvalidated = true;
1076 // TODO remove this once GSA code fix is submitted
1077 public ViewGroup getContent() {
1078 return (ViewGroup) mContent;
1081 public int getItemCount() {
1082 return mContent.getItemCount();
1085 @Thunk void onCloseComplete() {
1086 DragLayer parent = (DragLayer) getParent();
1087 if (parent != null) {
1088 parent.removeView(this);
1090 mDragController.removeDropTarget((DropTarget) this);
1092 mFolderIcon.requestFocus();
1094 if (mRearrangeOnClose) {
1095 rearrangeChildren();
1096 mRearrangeOnClose = false;
1098 if (getItemCount() <= 1) {
1099 if (!mDragInProgress && !mSuppressFolderDeletion) {
1100 replaceFolderWithFinalItem();
1101 } else if (mDragInProgress) {
1102 mDeleteFolderOnDropCompleted = true;
1105 mSuppressFolderDeletion = false;
1109 @Thunk void replaceFolderWithFinalItem() {
1110 // Add the last remaining child to the workspace in place of the folder
1111 Runnable onCompleteRunnable = new Runnable() {
1114 CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screenId);
1117 // Move the item from the folder to the workspace, in the position of the folder
1118 if (getItemCount() == 1) {
1119 ShortcutInfo finalItem = mInfo.contents.get(0);
1120 child = mLauncher.createShortcut(cellLayout, finalItem);
1121 LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
1122 mInfo.screenId, mInfo.cellX, mInfo.cellY);
1124 if (getItemCount() <= 1) {
1125 // Remove the folder
1126 LauncherModel.deleteItemFromDatabase(mLauncher, mInfo);
1127 if (cellLayout != null) {
1128 // b/12446428 -- sometimes the cell layout has already gone away?
1129 cellLayout.removeView(mFolderIcon);
1131 if (mFolderIcon instanceof DropTarget) {
1132 mDragController.removeDropTarget((DropTarget) mFolderIcon);
1134 mLauncher.removeFolder(mInfo);
1136 // We add the child after removing the folder to prevent both from existing at
1137 // the same time in the CellLayout. We need to add the new item with addInScreenFromBind()
1138 // to ensure that hotseat items are placed correctly.
1139 if (child != null) {
1140 mLauncher.getWorkspace().addInScreenFromBind(child, mInfo.container, mInfo.screenId,
1141 mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY);
1145 View finalChild = mContent.getLastItem();
1146 if (finalChild != null) {
1147 mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable);
1149 onCompleteRunnable.run();
1154 boolean isDestroyed() {
1158 // This method keeps track of the last item in the folder for the purposes
1159 // of keyboard focus
1160 public void updateTextViewFocus() {
1161 View lastChild = mContent.getLastItem();
1162 if (lastChild != null) {
1163 mFolderName.setNextFocusDownId(lastChild.getId());
1164 mFolderName.setNextFocusRightId(lastChild.getId());
1165 mFolderName.setNextFocusLeftId(lastChild.getId());
1166 mFolderName.setNextFocusUpId(lastChild.getId());
1170 public void onDrop(DragObject d) {
1171 Runnable cleanUpRunnable = null;
1173 // If we are coming from All Apps space, we defer removing the extra empty screen
1174 // until the folder closes
1175 if (d.dragSource != mLauncher.getWorkspace() && !(d.dragSource instanceof Folder)) {
1176 cleanUpRunnable = new Runnable() {
1179 mLauncher.exitSpringLoadedDragModeDelayed(true,
1180 Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT,
1186 // If the icon was dropped while the page was being scrolled, we need to compute
1187 // the target location again such that the icon is placed of the final page.
1188 if (!mContent.rankOnCurrentPage(mEmptyCellRank)) {
1190 mTargetRank = getTargetRank(d, null);
1192 // Rearrange items immediately.
1193 mReorderAlarmListener.onAlarm(mReorderAlarm);
1195 mOnScrollHintAlarm.cancelAlarm();
1196 mScrollPauseAlarm.cancelAlarm();
1198 mContent.completePendingPageChanges();
1200 View currentDragView;
1201 ShortcutInfo si = mCurrentDragInfo;
1202 if (mIsExternalDrag) {
1203 currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank);
1204 // Actually move the item in the database if it was an external drag. Call this
1205 // before creating the view, so that ShortcutInfo is updated appropriately.
1206 LauncherModel.addOrMoveItemInDatabase(
1207 mLauncher, si, mInfo.id, 0, si.cellX, si.cellY);
1209 // We only need to update the locations if it doesn't get handled in #onDropCompleted.
1210 if (d.dragSource != this) {
1211 updateItemLocationsInDatabaseBatch();
1213 mIsExternalDrag = false;
1215 currentDragView = mCurrentDragView;
1216 mContent.addViewForRank(currentDragView, si, mEmptyCellRank);
1219 if (d.dragView.hasDrawn()) {
1221 // Temporarily reset the scale such that the animation target gets calculated correctly.
1222 float scaleX = getScaleX();
1223 float scaleY = getScaleY();
1226 mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, currentDragView,
1227 cleanUpRunnable, null);
1231 d.deferDragViewCleanupPostAnimation = false;
1232 currentDragView.setVisibility(VISIBLE);
1234 mItemsInvalidated = true;
1235 rearrangeChildren();
1237 // Temporarily suppress the listener, as we did all the work already here.
1238 mSuppressOnAdd = true;
1240 mSuppressOnAdd = false;
1241 // Clear the drag info, as it is no longer being dragged.
1242 mCurrentDragInfo = null;
1243 mDragInProgress = false;
1245 if (mContent.getPageCount() > 1) {
1246 // The animation has already been shown while opening the folder.
1247 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher);
1251 // This is used so the item doesn't immediately appear in the folder when added. In one case
1252 // we need to create the illusion that the item isn't added back to the folder yet, to
1253 // to correspond to the animation of the icon back into the folder. This is
1254 public void hideItem(ShortcutInfo info) {
1255 View v = getViewForInfo(info);
1256 v.setVisibility(INVISIBLE);
1258 public void showItem(ShortcutInfo info) {
1259 View v = getViewForInfo(info);
1260 v.setVisibility(VISIBLE);
1264 public void onAdd(ShortcutInfo item) {
1265 // If the item was dropped onto this open folder, we have done the work associated
1266 // with adding the item to the folder, as indicated by mSuppressOnAdd being set
1267 if (mSuppressOnAdd) return;
1268 mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item));
1269 mItemsInvalidated = true;
1270 LauncherModel.addOrMoveItemInDatabase(
1271 mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
1274 public void onRemove(ShortcutInfo item) {
1275 mItemsInvalidated = true;
1276 // If this item is being dragged from this open folder, we have already handled
1277 // the work associated with removing the item, so we don't have to do anything here.
1278 if (item == mCurrentDragInfo) return;
1279 View v = getViewForInfo(item);
1280 mContent.removeItem(v);
1281 if (mState == STATE_ANIMATING) {
1282 mRearrangeOnClose = true;
1284 rearrangeChildren();
1286 if (getItemCount() <= 1) {
1287 replaceFolderWithFinalItem();
1291 private View getViewForInfo(final ShortcutInfo item) {
1292 return mContent.iterateOverItems(new ItemOperator() {
1295 public boolean evaluate(ItemInfo info, View view, View parent) {
1296 return info == item;
1301 public void onItemsChanged() {
1302 updateTextViewFocus();
1305 public void onTitleChanged(CharSequence title) {
1308 public ArrayList<View> getItemsInReadingOrder() {
1309 if (mItemsInvalidated) {
1310 mItemsInReadingOrder.clear();
1311 mContent.iterateOverItems(new ItemOperator() {
1314 public boolean evaluate(ItemInfo info, View view, View parent) {
1315 mItemsInReadingOrder.add(view);
1319 mItemsInvalidated = false;
1321 return mItemsInReadingOrder;
1324 public void getLocationInDragLayer(int[] loc) {
1325 mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
1328 public void onFocusChange(View v, boolean hasFocus) {
1329 if (v == mFolderName && hasFocus) {
1330 startEditingFolderName();
1335 public void getHitRectRelativeToDragLayer(Rect outRect) {
1336 getHitRect(outRect);
1337 outRect.left -= mScrollAreaOffset;
1338 outRect.right += mScrollAreaOffset;
1342 public void fillInLaunchSourceData(Bundle sourceData) {
1343 // Fill in from the folder icon's launch source provider first
1344 Stats.LaunchSourceUtils.populateSourceDataFromAncestorProvider(mFolderIcon, sourceData);
1345 sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_FOLDER);
1346 sourceData.putInt(Stats.SOURCE_EXTRA_SUB_CONTAINER_PAGE, mContent.getCurrentPage());
1349 private class OnScrollHintListener implements OnAlarmListener {
1351 private final DragObject mDragObject;
1353 OnScrollHintListener(DragObject object) {
1354 mDragObject = object;
1358 * Scroll hint has been shown long enough. Now scroll to appropriate page.
1361 public void onAlarm(Alarm alarm) {
1362 if (mCurrentScrollDir == DragController.SCROLL_LEFT) {
1363 mContent.scrollLeft();
1364 mScrollHintDir = DragController.SCROLL_NONE;
1365 } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) {
1366 mContent.scrollRight();
1367 mScrollHintDir = DragController.SCROLL_NONE;
1369 // This should not happen
1372 mCurrentScrollDir = DragController.SCROLL_NONE;
1374 // Pause drag event until the scrolling is finished
1375 mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject));
1376 mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY);
1380 private class OnScrollFinishedListener implements OnAlarmListener {
1382 private final DragObject mDragObject;
1384 OnScrollFinishedListener(DragObject object) {
1385 mDragObject = object;
1389 * Page scroll is complete.
1392 public void onAlarm(Alarm alarm) {
1393 // Reorder immediately on page change.
1394 onDragOver(mDragObject, 1);
1398 // Compares item position based on rank and position giving priority to the rank.
1399 public static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() {
1402 public int compare(ItemInfo lhs, ItemInfo rhs) {
1403 if (lhs.rank != rhs.rank) {
1404 return lhs.rank - rhs.rank;
1405 } else if (lhs.cellY != rhs.cellY) {
1406 return lhs.cellY - rhs.cellY;
1408 return lhs.cellX - rhs.cellX;