Folder.java revision e393d3af369c452f3b5f5d9c0a23a83aa3aa736b
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17package com.android.launcher3.folder; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.animation.PropertyValuesHolder; 24import android.annotation.SuppressLint; 25import android.annotation.TargetApi; 26import android.content.Context; 27import android.content.res.Resources; 28import android.graphics.PointF; 29import android.graphics.Rect; 30import android.os.Build; 31import android.text.InputType; 32import android.text.Selection; 33import android.text.Spannable; 34import android.util.AttributeSet; 35import android.util.Log; 36import android.view.ActionMode; 37import android.view.FocusFinder; 38import android.view.KeyEvent; 39import android.view.Menu; 40import android.view.MenuItem; 41import android.view.MotionEvent; 42import android.view.View; 43import android.view.ViewDebug; 44import android.view.accessibility.AccessibilityEvent; 45import android.view.animation.AccelerateInterpolator; 46import android.view.animation.AnimationUtils; 47import android.view.inputmethod.EditorInfo; 48import android.view.inputmethod.InputMethodManager; 49import android.widget.LinearLayout; 50import android.widget.TextView; 51 52import com.android.launcher3.Alarm; 53import com.android.launcher3.AppInfo; 54import com.android.launcher3.CellLayout; 55import com.android.launcher3.DeviceProfile; 56import com.android.launcher3.DragSource; 57import com.android.launcher3.DropTarget; 58import com.android.launcher3.ExtendedEditText; 59import com.android.launcher3.FolderInfo; 60import com.android.launcher3.FolderInfo.FolderListener; 61import com.android.launcher3.ItemInfo; 62import com.android.launcher3.Launcher; 63import com.android.launcher3.LauncherAnimUtils; 64import com.android.launcher3.LauncherModel; 65import com.android.launcher3.LauncherSettings; 66import com.android.launcher3.LogDecelerateInterpolator; 67import com.android.launcher3.OnAlarmListener; 68import com.android.launcher3.R; 69import com.android.launcher3.ShortcutInfo; 70import com.android.launcher3.UninstallDropTarget.DropTargetSource; 71import com.android.launcher3.Utilities; 72import com.android.launcher3.Workspace.ItemOperator; 73import com.android.launcher3.accessibility.AccessibileDragListenerAdapter; 74import com.android.launcher3.config.FeatureFlags; 75import com.android.launcher3.dragndrop.DragController; 76import com.android.launcher3.dragndrop.DragController.DragListener; 77import com.android.launcher3.dragndrop.DragLayer; 78import com.android.launcher3.dragndrop.DragOptions; 79import com.android.launcher3.pageindicators.PageIndicatorDots; 80import com.android.launcher3.userevent.nano.LauncherLogProto; 81import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 82import com.android.launcher3.util.CircleRevealOutlineProvider; 83import com.android.launcher3.util.Thunk; 84 85import java.util.ArrayList; 86import java.util.Collections; 87import java.util.Comparator; 88 89/** 90 * Represents a set of icons chosen by the user or generated by the system. 91 */ 92public class Folder extends LinearLayout implements DragSource, View.OnClickListener, 93 View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, 94 View.OnFocusChangeListener, DragListener, DropTargetSource { 95 private static final String TAG = "Launcher.Folder"; 96 97 /** 98 * We avoid measuring {@link #mContent} with a 0 width or height, as this 99 * results in CellLayout being measured as UNSPECIFIED, which it does not support. 100 */ 101 private static final int MIN_CONTENT_DIMEN = 5; 102 103 static final int STATE_NONE = -1; 104 static final int STATE_SMALL = 0; 105 static final int STATE_ANIMATING = 1; 106 static final int STATE_OPEN = 2; 107 108 /** 109 * Time for which the scroll hint is shown before automatically changing page. 110 */ 111 public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY; 112 113 /** 114 * Fraction of icon width which behave as scroll region. 115 */ 116 private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f; 117 118 private static final int FOLDER_NAME_ANIMATION_DURATION = 633; 119 120 private static final int REORDER_DELAY = 250; 121 private static final int ON_EXIT_CLOSE_DELAY = 400; 122 private static final Rect sTempRect = new Rect(); 123 124 private static String sDefaultFolderName; 125 private static String sHintText; 126 127 private final Alarm mReorderAlarm = new Alarm(); 128 private final Alarm mOnExitAlarm = new Alarm(); 129 private final Alarm mOnScrollHintAlarm = new Alarm(); 130 @Thunk final Alarm mScrollPauseAlarm = new Alarm(); 131 132 @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); 133 134 private final int mExpandDuration; 135 private final int mMaterialExpandDuration; 136 private final int mMaterialExpandStagger; 137 138 private final InputMethodManager mInputMethodManager; 139 140 protected final Launcher mLauncher; 141 protected DragController mDragController; 142 public FolderInfo mInfo; 143 144 @Thunk FolderIcon mFolderIcon; 145 146 @Thunk FolderPagedView mContent; 147 public ExtendedEditText mFolderName; 148 private PageIndicatorDots mPageIndicator; 149 150 private View mFooter; 151 private int mFooterHeight; 152 153 // Cell ranks used for drag and drop 154 @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank; 155 156 @ViewDebug.ExportedProperty(category = "launcher", 157 mapping = { 158 @ViewDebug.IntToString(from = STATE_NONE, to = "STATE_NONE"), 159 @ViewDebug.IntToString(from = STATE_SMALL, to = "STATE_SMALL"), 160 @ViewDebug.IntToString(from = STATE_ANIMATING, to = "STATE_ANIMATING"), 161 @ViewDebug.IntToString(from = STATE_OPEN, to = "STATE_OPEN"), 162 }) 163 @Thunk int mState = STATE_NONE; 164 @ViewDebug.ExportedProperty(category = "launcher") 165 private boolean mRearrangeOnClose = false; 166 boolean mItemsInvalidated = false; 167 private View mCurrentDragView; 168 private boolean mIsExternalDrag; 169 private boolean mDragInProgress = false; 170 private boolean mDeleteFolderOnDropCompleted = false; 171 private boolean mSuppressFolderDeletion = false; 172 private boolean mItemAddedBackToSelfViaIcon = false; 173 @Thunk float mFolderIconPivotX; 174 @Thunk float mFolderIconPivotY; 175 private boolean mIsEditingName = false; 176 177 @ViewDebug.ExportedProperty(category = "launcher") 178 private boolean mDestroyed; 179 180 @Thunk Runnable mDeferredAction; 181 private boolean mDeferDropAfterUninstall; 182 private boolean mUninstallSuccessful; 183 184 // Folder scrolling 185 private int mScrollAreaOffset; 186 187 @Thunk int mScrollHintDir = DragController.SCROLL_NONE; 188 @Thunk int mCurrentScrollDir = DragController.SCROLL_NONE; 189 190 /** 191 * Used to inflate the Workspace from XML. 192 * 193 * @param context The application's context. 194 * @param attrs The attributes set containing the Workspace's customization values. 195 */ 196 public Folder(Context context, AttributeSet attrs) { 197 super(context, attrs); 198 setAlwaysDrawnWithCacheEnabled(false); 199 mInputMethodManager = (InputMethodManager) 200 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 201 202 Resources res = getResources(); 203 mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration); 204 mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); 205 mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger); 206 207 if (sDefaultFolderName == null) { 208 sDefaultFolderName = res.getString(R.string.folder_name); 209 } 210 if (sHintText == null) { 211 sHintText = res.getString(R.string.folder_hint_text); 212 } 213 mLauncher = (Launcher) context; 214 // We need this view to be focusable in touch mode so that when text editing of the folder 215 // name is complete, we have something to focus on, thus hiding the cursor and giving 216 // reliable behavior when clicking the text field (since it will always gain focus on click). 217 setFocusableInTouchMode(true); 218 } 219 220 @Override 221 protected void onFinishInflate() { 222 super.onFinishInflate(); 223 mContent = (FolderPagedView) findViewById(R.id.folder_content); 224 mContent.setFolder(this); 225 226 mPageIndicator = (PageIndicatorDots) findViewById(R.id.folder_page_indicator); 227 mFolderName = (ExtendedEditText) findViewById(R.id.folder_name); 228 mFolderName.setOnBackKeyListener(new ExtendedEditText.OnBackKeyListener() { 229 @Override 230 public boolean onBackKey() { 231 // Close the activity on back key press 232 doneEditingFolderName(true); 233 return false; 234 } 235 }); 236 mFolderName.setOnFocusChangeListener(this); 237 238 if (!Utilities.ATLEAST_MARSHMALLOW) { 239 // We disable action mode in older OSes where floating selection menu is not yet 240 // available. 241 mFolderName.setCustomSelectionActionModeCallback(new ActionMode.Callback() { 242 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 243 return false; 244 } 245 246 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 247 return false; 248 } 249 250 public void onDestroyActionMode(ActionMode mode) { 251 } 252 253 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 254 return false; 255 } 256 }); 257 } 258 mFolderName.setOnEditorActionListener(this); 259 mFolderName.setSelectAllOnFocus(true); 260 mFolderName.setInputType(mFolderName.getInputType() | 261 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); 262 263 mFooter = findViewById(R.id.folder_footer); 264 265 // We find out how tall footer wants to be (it is set to wrap_content), so that 266 // we can allocate the appropriate amount of space for it. 267 int measureSpec = MeasureSpec.UNSPECIFIED; 268 mFooter.measure(measureSpec, measureSpec); 269 mFooterHeight = mFooter.getMeasuredHeight(); 270 } 271 272 public void onClick(View v) { 273 Object tag = v.getTag(); 274 if (tag instanceof ShortcutInfo) { 275 mLauncher.onClick(v); 276 } 277 } 278 279 public boolean onLongClick(View v) { 280 // Return if global dragging is not enabled 281 if (!mLauncher.isDraggingEnabled()) return true; 282 return startDrag(v, new DragOptions()); 283 } 284 285 public boolean startDrag(View v, DragOptions options) { 286 Object tag = v.getTag(); 287 if (tag instanceof ShortcutInfo) { 288 ShortcutInfo item = (ShortcutInfo) tag; 289 if (!v.isInTouchMode()) { 290 return false; 291 } 292 293 mEmptyCellRank = item.rank; 294 mCurrentDragView = v; 295 296 mDragController.addDragListener(this); 297 if (options.isAccessibleDrag) { 298 mDragController.addDragListener(new AccessibileDragListenerAdapter( 299 mContent, CellLayout.FOLDER_ACCESSIBILITY_DRAG) { 300 301 @Override 302 protected void enableAccessibleDrag(boolean enable) { 303 super.enableAccessibleDrag(enable); 304 mFooter.setImportantForAccessibility(enable 305 ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS 306 : IMPORTANT_FOR_ACCESSIBILITY_AUTO); 307 } 308 }); 309 } 310 311 mLauncher.getWorkspace().beginDragShared(v, this, options); 312 } 313 return true; 314 } 315 316 @Override 317 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 318 if (dragObject.dragSource != this) { 319 return; 320 } 321 322 mContent.removeItem(mCurrentDragView); 323 if (dragObject.dragInfo instanceof ShortcutInfo) { 324 mItemsInvalidated = true; 325 326 // We do not want to get events for the item being removed, as they will get handled 327 // when the drop completes 328 try (SuppressInfoChanges s = new SuppressInfoChanges()) { 329 mInfo.remove((ShortcutInfo) dragObject.dragInfo, true); 330 } 331 } 332 mDragInProgress = true; 333 mItemAddedBackToSelfViaIcon = false; 334 } 335 336 @Override 337 public void onDragEnd() { 338 if (mIsExternalDrag && mDragInProgress) { 339 completeDragExit(); 340 } 341 mDragController.removeDragListener(this); 342 } 343 344 public boolean isEditingName() { 345 return mIsEditingName; 346 } 347 348 public void startEditingFolderName() { 349 post(new Runnable() { 350 @Override 351 public void run() { 352 mFolderName.setHint(""); 353 mIsEditingName = true; 354 } 355 }); 356 } 357 358 public void dismissEditingName() { 359 mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 360 doneEditingFolderName(true); 361 } 362 363 public void doneEditingFolderName(boolean commit) { 364 mFolderName.setHint(sHintText); 365 // Convert to a string here to ensure that no other state associated with the text field 366 // gets saved. 367 String newTitle = mFolderName.getText().toString(); 368 mInfo.setTitle(newTitle); 369 LauncherModel.updateItemInDatabase(mLauncher, mInfo); 370 371 if (commit) { 372 Utilities.sendCustomAccessibilityEvent( 373 this, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 374 getContext().getString(R.string.folder_renamed, newTitle)); 375 } 376 377 // This ensures that focus is gained every time the field is clicked, which selects all 378 // the text and brings up the soft keyboard if necessary. 379 mFolderName.clearFocus(); 380 381 Selection.setSelection((Spannable) mFolderName.getText(), 0, 0); 382 mIsEditingName = false; 383 } 384 385 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 386 if (actionId == EditorInfo.IME_ACTION_DONE) { 387 dismissEditingName(); 388 return true; 389 } 390 return false; 391 } 392 393 public View getEditTextRegion() { 394 return mFolderName; 395 } 396 397 /** 398 * We need to handle touch events to prevent them from falling through to the workspace below. 399 */ 400 @SuppressLint("ClickableViewAccessibility") 401 @Override 402 public boolean onTouchEvent(MotionEvent ev) { 403 return true; 404 } 405 406 public void setDragController(DragController dragController) { 407 mDragController = dragController; 408 } 409 410 public void setFolderIcon(FolderIcon icon) { 411 mFolderIcon = icon; 412 } 413 414 @Override 415 protected void onAttachedToWindow() { 416 // requestFocus() causes the focus onto the folder itself, which doesn't cause visual 417 // effect but the next arrow key can start the keyboard focus inside of the folder, not 418 // the folder itself. 419 requestFocus(); 420 super.onAttachedToWindow(); 421 } 422 423 @Override 424 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 425 // When the folder gets focus, we don't want to announce the list of items. 426 return true; 427 } 428 429 @Override 430 public View focusSearch(int direction) { 431 // When the folder is focused, further focus search should be within the folder contents. 432 return FocusFinder.getInstance().findNextFocus(this, null, direction); 433 } 434 435 /** 436 * @return the FolderInfo object associated with this folder 437 */ 438 public FolderInfo getInfo() { 439 return mInfo; 440 } 441 442 void bind(FolderInfo info) { 443 mInfo = info; 444 ArrayList<ShortcutInfo> children = info.contents; 445 Collections.sort(children, ITEM_POS_COMPARATOR); 446 447 ArrayList<ShortcutInfo> overflow = mContent.bindItems(children); 448 449 // If our folder has too many items we prune them from the list. This is an issue 450 // when upgrading from the old Folders implementation which could contain an unlimited 451 // number of items. 452 // TODO: Remove this, as with multi-page folders, there will never be any overflow 453 for (ShortcutInfo item: overflow) { 454 mInfo.remove(item, false); 455 LauncherModel.deleteItemFromDatabase(mLauncher, item); 456 } 457 458 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 459 if (lp == null) { 460 lp = new DragLayer.LayoutParams(0, 0); 461 lp.customPosition = true; 462 setLayoutParams(lp); 463 } 464 centerAboutIcon(); 465 466 mItemsInvalidated = true; 467 updateTextViewFocus(); 468 mInfo.addListener(this); 469 470 if (!sDefaultFolderName.contentEquals(mInfo.title)) { 471 mFolderName.setText(mInfo.title); 472 } else { 473 mFolderName.setText(""); 474 } 475 476 // In case any children didn't come across during loading, clean up the folder accordingly 477 mFolderIcon.post(new Runnable() { 478 public void run() { 479 if (getItemCount() <= 1) { 480 replaceFolderWithFinalItem(); 481 } 482 } 483 }); 484 } 485 486 /** 487 * Creates a new UserFolder, inflated from R.layout.user_folder. 488 * 489 * @param launcher The main activity. 490 * 491 * @return A new UserFolder. 492 */ 493 @SuppressLint("InflateParams") 494 static Folder fromXml(Launcher launcher) { 495 return (Folder) launcher.getLayoutInflater().inflate( 496 FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION 497 ? R.layout.user_folder : R.layout.user_folder_icon_normalized, null); 498 } 499 500 /** 501 * This method is intended to make the UserFolder to be visually identical in size and position 502 * to its associated FolderIcon. This allows for a seamless transition into the expanded state. 503 */ 504 private void positionAndSizeAsIcon() { 505 if (!(getParent() instanceof DragLayer)) return; 506 setScaleX(0.8f); 507 setScaleY(0.8f); 508 setAlpha(0f); 509 mState = STATE_SMALL; 510 } 511 512 private void prepareReveal() { 513 setScaleX(1f); 514 setScaleY(1f); 515 setAlpha(1f); 516 mState = STATE_SMALL; 517 } 518 519 public void animateOpen() { 520 if (!(getParent() instanceof DragLayer)) return; 521 522 mContent.completePendingPageChanges(); 523 if (!mDragInProgress) { 524 // Open on the first page. 525 mContent.snapToPageImmediately(0); 526 } 527 528 // This is set to true in close(), but isn't reset to false until onDropCompleted(). This 529 // leads to an inconsistent state if you drag out of the folder and drag back in without 530 // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice. 531 mDeleteFolderOnDropCompleted = false; 532 533 Animator openFolderAnim = null; 534 final Runnable onCompleteRunnable; 535 if (!Utilities.ATLEAST_LOLLIPOP) { 536 positionAndSizeAsIcon(); 537 centerAboutIcon(); 538 539 final ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(this, 1, 1, 1); 540 oa.setDuration(mExpandDuration); 541 openFolderAnim = oa; 542 543 setLayerType(LAYER_TYPE_HARDWARE, null); 544 onCompleteRunnable = new Runnable() { 545 @Override 546 public void run() { 547 setLayerType(LAYER_TYPE_NONE, null); 548 } 549 }; 550 } else { 551 prepareReveal(); 552 centerAboutIcon(); 553 554 AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); 555 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); 556 int height = getFolderHeight(); 557 558 float transX = - 0.075f * (width / 2 - getPivotX()); 559 float transY = - 0.075f * (height / 2 - getPivotY()); 560 setTranslationX(transX); 561 setTranslationY(transY); 562 PropertyValuesHolder tx = PropertyValuesHolder.ofFloat(TRANSLATION_X, transX, 0); 563 PropertyValuesHolder ty = PropertyValuesHolder.ofFloat(TRANSLATION_Y, transY, 0); 564 565 Animator drift = ObjectAnimator.ofPropertyValuesHolder(this, tx, ty); 566 drift.setDuration(mMaterialExpandDuration); 567 drift.setStartDelay(mMaterialExpandStagger); 568 drift.setInterpolator(new LogDecelerateInterpolator(100, 0)); 569 570 int rx = (int) Math.max(Math.max(width - getPivotX(), 0), getPivotX()); 571 int ry = (int) Math.max(Math.max(height - getPivotY(), 0), getPivotY()); 572 float radius = (float) Math.hypot(rx, ry); 573 574 Animator reveal = new CircleRevealOutlineProvider((int) getPivotX(), 575 (int) getPivotY(), 0, radius).createRevealAnimator(this); 576 reveal.setDuration(mMaterialExpandDuration); 577 reveal.setInterpolator(new LogDecelerateInterpolator(100, 0)); 578 579 mContent.setAlpha(0f); 580 Animator iconsAlpha = ObjectAnimator.ofFloat(mContent, "alpha", 0f, 1f); 581 iconsAlpha.setDuration(mMaterialExpandDuration); 582 iconsAlpha.setStartDelay(mMaterialExpandStagger); 583 iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); 584 585 mFooter.setAlpha(0f); 586 Animator textAlpha = ObjectAnimator.ofFloat(mFooter, "alpha", 0f, 1f); 587 textAlpha.setDuration(mMaterialExpandDuration); 588 textAlpha.setStartDelay(mMaterialExpandStagger); 589 textAlpha.setInterpolator(new AccelerateInterpolator(1.5f)); 590 591 anim.play(drift); 592 anim.play(iconsAlpha); 593 anim.play(textAlpha); 594 anim.play(reveal); 595 596 openFolderAnim = anim; 597 598 mContent.setLayerType(LAYER_TYPE_HARDWARE, null); 599 mFooter.setLayerType(LAYER_TYPE_HARDWARE, null); 600 onCompleteRunnable = new Runnable() { 601 @Override 602 public void run() { 603 mContent.setLayerType(LAYER_TYPE_NONE, null); 604 mFooter.setLayerType(LAYER_TYPE_NONE, null); 605 mLauncher.getUserEventDispatcher().resetElapsedContainerMillis(); 606 } 607 }; 608 } 609 openFolderAnim.addListener(new AnimatorListenerAdapter() { 610 @Override 611 public void onAnimationStart(Animator animation) { 612 Utilities.sendCustomAccessibilityEvent( 613 Folder.this, 614 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 615 mContent.getAccessibilityDescription()); 616 mState = STATE_ANIMATING; 617 } 618 @Override 619 public void onAnimationEnd(Animator animation) { 620 mState = STATE_OPEN; 621 622 onCompleteRunnable.run(); 623 mContent.setFocusOnFirstChild(); 624 } 625 }); 626 627 // Footer animation 628 if (mContent.getPageCount() > 1 && !mInfo.hasOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION)) { 629 int footerWidth = mContent.getDesiredWidth() 630 - mFooter.getPaddingLeft() - mFooter.getPaddingRight(); 631 632 float textWidth = mFolderName.getPaint().measureText(mFolderName.getText().toString()); 633 float translation = (footerWidth - textWidth) / 2; 634 mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation); 635 mPageIndicator.prepareEntryAnimation(); 636 637 // Do not update the flag if we are in drag mode. The flag will be updated, when we 638 // actually drop the icon. 639 final boolean updateAnimationFlag = !mDragInProgress; 640 openFolderAnim.addListener(new AnimatorListenerAdapter() { 641 642 @SuppressLint("InlinedApi") 643 @Override 644 public void onAnimationEnd(Animator animation) { 645 mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION) 646 .translationX(0) 647 .setInterpolator(Utilities.ATLEAST_LOLLIPOP ? 648 AnimationUtils.loadInterpolator(mLauncher, 649 android.R.interpolator.fast_out_slow_in) 650 : new LogDecelerateInterpolator(100, 0)); 651 mPageIndicator.playEntryAnimation(); 652 653 if (updateAnimationFlag) { 654 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher); 655 } 656 } 657 }); 658 } else { 659 mFolderName.setTranslationX(0); 660 } 661 662 mPageIndicator.stopAllAnimations(); 663 openFolderAnim.start(); 664 665 // Make sure the folder picks up the last drag move even if the finger doesn't move. 666 if (mDragController.isDragging()) { 667 mDragController.forceTouchMove(); 668 } 669 670 mContent.verifyVisibleHighResIcons(mContent.getNextPage()); 671 } 672 673 public void beginExternalDrag() { 674 mEmptyCellRank = mContent.allocateRankForNewItem(); 675 mIsExternalDrag = true; 676 mDragInProgress = true; 677 678 // Since this folder opened by another controller, it might not get onDrop or 679 // onDropComplete. Perform cleanup once drag-n-drop ends. 680 mDragController.addDragListener(this); 681 } 682 683 public void animateClosed() { 684 if (!(getParent() instanceof DragLayer)) return; 685 final ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(this, 0, 0.9f, 0.9f); 686 oa.addListener(new AnimatorListenerAdapter() { 687 @Override 688 public void onAnimationEnd(Animator animation) { 689 setLayerType(LAYER_TYPE_NONE, null); 690 close(true); 691 } 692 @Override 693 public void onAnimationStart(Animator animation) { 694 Utilities.sendCustomAccessibilityEvent( 695 Folder.this, 696 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 697 getContext().getString(R.string.folder_closed)); 698 mState = STATE_ANIMATING; 699 } 700 }); 701 oa.setDuration(mExpandDuration); 702 setLayerType(LAYER_TYPE_HARDWARE, null); 703 oa.start(); 704 } 705 706 public void close(boolean wasAnimated) { 707 // TODO: Clear all active animations. 708 DragLayer parent = (DragLayer) getParent(); 709 if (parent != null) { 710 parent.removeView(this); 711 } 712 mDragController.removeDropTarget(this); 713 clearFocus(); 714 if (wasAnimated) { 715 mFolderIcon.requestFocus(); 716 } 717 718 if (mRearrangeOnClose) { 719 rearrangeChildren(); 720 mRearrangeOnClose = false; 721 } 722 if (getItemCount() <= 1) { 723 if (!mDragInProgress && !mSuppressFolderDeletion) { 724 replaceFolderWithFinalItem(); 725 } else if (mDragInProgress) { 726 mDeleteFolderOnDropCompleted = true; 727 } 728 } 729 mSuppressFolderDeletion = false; 730 clearDragInfo(); 731 mState = STATE_SMALL; 732 } 733 734 public boolean acceptDrop(DragObject d) { 735 final ItemInfo item = d.dragInfo; 736 final int itemType = item.itemType; 737 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 738 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT || 739 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) && 740 !isFull()); 741 } 742 743 public void onDragEnter(DragObject d) { 744 mPrevTargetRank = -1; 745 mOnExitAlarm.cancelAlarm(); 746 // Get the area offset such that the folder only closes if half the drag icon width 747 // is outside the folder area 748 mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset; 749 } 750 751 OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { 752 public void onAlarm(Alarm alarm) { 753 mContent.realTimeReorder(mEmptyCellRank, mTargetRank); 754 mEmptyCellRank = mTargetRank; 755 } 756 }; 757 758 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 759 public boolean isLayoutRtl() { 760 return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); 761 } 762 763 @Override 764 public void onDragOver(DragObject d) { 765 onDragOver(d, REORDER_DELAY); 766 } 767 768 private int getTargetRank(DragObject d, float[] recycle) { 769 recycle = d.getVisualCenter(recycle); 770 return mContent.findNearestArea( 771 (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop()); 772 } 773 774 @Thunk void onDragOver(DragObject d, int reorderDelay) { 775 if (mScrollPauseAlarm.alarmPending()) { 776 return; 777 } 778 final float[] r = new float[2]; 779 mTargetRank = getTargetRank(d, r); 780 781 if (mTargetRank != mPrevTargetRank) { 782 mReorderAlarm.cancelAlarm(); 783 mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); 784 mReorderAlarm.setAlarm(REORDER_DELAY); 785 mPrevTargetRank = mTargetRank; 786 787 if (d.stateAnnouncer != null) { 788 d.stateAnnouncer.announce(getContext().getString(R.string.move_to_position, 789 mTargetRank + 1)); 790 } 791 } 792 793 float x = r[0]; 794 int currentPage = mContent.getNextPage(); 795 796 float cellOverlap = mContent.getCurrentCellLayout().getCellWidth() 797 * ICON_OVERSCROLL_WIDTH_FACTOR; 798 boolean isOutsideLeftEdge = x < cellOverlap; 799 boolean isOutsideRightEdge = x > (getWidth() - cellOverlap); 800 801 if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) { 802 showScrollHint(DragController.SCROLL_LEFT, d); 803 } else if (currentPage < (mContent.getPageCount() - 1) 804 && (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) { 805 showScrollHint(DragController.SCROLL_RIGHT, d); 806 } else { 807 mOnScrollHintAlarm.cancelAlarm(); 808 if (mScrollHintDir != DragController.SCROLL_NONE) { 809 mContent.clearScrollHint(); 810 mScrollHintDir = DragController.SCROLL_NONE; 811 } 812 } 813 } 814 815 private void showScrollHint(int direction, DragObject d) { 816 // Show scroll hint on the right 817 if (mScrollHintDir != direction) { 818 mContent.showScrollHint(direction); 819 mScrollHintDir = direction; 820 } 821 822 // Set alarm for when the hint is complete 823 if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) { 824 mCurrentScrollDir = direction; 825 mOnScrollHintAlarm.cancelAlarm(); 826 mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d)); 827 mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION); 828 829 mReorderAlarm.cancelAlarm(); 830 mTargetRank = mEmptyCellRank; 831 } 832 } 833 834 OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { 835 public void onAlarm(Alarm alarm) { 836 completeDragExit(); 837 } 838 }; 839 840 public void completeDragExit() { 841 if (mInfo.opened) { 842 mLauncher.closeFolder(); 843 mRearrangeOnClose = true; 844 } else if (mState == STATE_ANIMATING) { 845 mRearrangeOnClose = true; 846 } else { 847 rearrangeChildren(); 848 clearDragInfo(); 849 } 850 } 851 852 private void clearDragInfo() { 853 mCurrentDragView = null; 854 mIsExternalDrag = false; 855 } 856 857 public void onDragExit(DragObject d) { 858 // We only close the folder if this is a true drag exit, ie. not because 859 // a drop has occurred above the folder. 860 if (!d.dragComplete) { 861 mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener); 862 mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); 863 } 864 mReorderAlarm.cancelAlarm(); 865 866 mOnScrollHintAlarm.cancelAlarm(); 867 mScrollPauseAlarm.cancelAlarm(); 868 if (mScrollHintDir != DragController.SCROLL_NONE) { 869 mContent.clearScrollHint(); 870 mScrollHintDir = DragController.SCROLL_NONE; 871 } 872 } 873 874 /** 875 * When performing an accessibility drop, onDrop is sent immediately after onDragEnter. So we 876 * need to complete all transient states based on timers. 877 */ 878 @Override 879 public void prepareAccessibilityDrop() { 880 if (mReorderAlarm.alarmPending()) { 881 mReorderAlarm.cancelAlarm(); 882 mReorderAlarmListener.onAlarm(mReorderAlarm); 883 } 884 } 885 886 public void onDropCompleted(final View target, final DragObject d, 887 final boolean isFlingToDelete, final boolean success) { 888 if (mDeferDropAfterUninstall) { 889 Log.d(TAG, "Deferred handling drop because waiting for uninstall."); 890 mDeferredAction = new Runnable() { 891 public void run() { 892 onDropCompleted(target, d, isFlingToDelete, success); 893 mDeferredAction = null; 894 } 895 }; 896 return; 897 } 898 899 boolean beingCalledAfterUninstall = mDeferredAction != null; 900 boolean successfulDrop = 901 success && (!beingCalledAfterUninstall || mUninstallSuccessful); 902 903 if (successfulDrop) { 904 if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) { 905 replaceFolderWithFinalItem(); 906 } 907 } else { 908 // The drag failed, we need to return the item to the folder 909 ShortcutInfo info = (ShortcutInfo) d.dragInfo; 910 View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info) 911 ? mCurrentDragView : mContent.createNewView(info); 912 ArrayList<View> views = getItemsInReadingOrder(); 913 views.add(info.rank, icon); 914 mContent.arrangeChildren(views, views.size()); 915 mItemsInvalidated = true; 916 917 try (SuppressInfoChanges s = new SuppressInfoChanges()) { 918 mFolderIcon.onDrop(d); 919 } 920 } 921 922 if (target != this) { 923 if (mOnExitAlarm.alarmPending()) { 924 mOnExitAlarm.cancelAlarm(); 925 if (!successfulDrop) { 926 mSuppressFolderDeletion = true; 927 } 928 mScrollPauseAlarm.cancelAlarm(); 929 completeDragExit(); 930 } 931 } 932 933 mDeleteFolderOnDropCompleted = false; 934 mDragInProgress = false; 935 mItemAddedBackToSelfViaIcon = false; 936 mCurrentDragView = null; 937 938 // Reordering may have occured, and we need to save the new item locations. We do this once 939 // at the end to prevent unnecessary database operations. 940 updateItemLocationsInDatabaseBatch(); 941 942 // Use the item count to check for multi-page as the folder UI may not have 943 // been refreshed yet. 944 if (getItemCount() <= mContent.itemsPerPage()) { 945 // Show the animation, next time something is added to the folder. 946 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, false, mLauncher); 947 } 948 949 if (!isFlingToDelete) { 950 // Fling to delete already exits spring loaded mode after the animation finishes. 951 mLauncher.exitSpringLoadedDragModeDelayed(successfulDrop, 952 Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); 953 } 954 } 955 956 @Override 957 public void deferCompleteDropAfterUninstallActivity() { 958 mDeferDropAfterUninstall = true; 959 } 960 961 @Override 962 public void onDragObjectRemoved(boolean success) { 963 mDeferDropAfterUninstall = false; 964 mUninstallSuccessful = success; 965 if (mDeferredAction != null) { 966 mDeferredAction.run(); 967 } 968 } 969 970 @Override 971 public float getIntrinsicIconScaleFactor() { 972 return 1f; 973 } 974 975 @Override 976 public boolean supportsFlingToDelete() { 977 return true; 978 } 979 980 @Override 981 public boolean supportsAppInfoDropTarget() { 982 return !FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND; 983 } 984 985 @Override 986 public boolean supportsDeleteDropTarget() { 987 return true; 988 } 989 990 @Override 991 public void onFlingToDelete(DragObject d, PointF vec) { 992 // Do nothing 993 } 994 995 @Override 996 public void onFlingToDeleteCompleted() { 997 // Do nothing 998 } 999 1000 private void updateItemLocationsInDatabaseBatch() { 1001 ArrayList<View> list = getItemsInReadingOrder(); 1002 ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); 1003 for (int i = 0; i < list.size(); i++) { 1004 View v = list.get(i); 1005 ItemInfo info = (ItemInfo) v.getTag(); 1006 info.rank = i; 1007 items.add(info); 1008 } 1009 1010 LauncherModel.moveItemsInDatabase(mLauncher, items, mInfo.id, 0); 1011 } 1012 1013 public void notifyDrop() { 1014 if (mDragInProgress) { 1015 mItemAddedBackToSelfViaIcon = true; 1016 } 1017 } 1018 1019 public boolean isDropEnabled() { 1020 return true; 1021 } 1022 1023 public boolean isFull() { 1024 return mContent.isFull(); 1025 } 1026 1027 private void centerAboutIcon() { 1028 DeviceProfile grid = mLauncher.getDeviceProfile(); 1029 1030 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 1031 DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer); 1032 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); 1033 int height = getFolderHeight(); 1034 1035 float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect); 1036 int centerX = sTempRect.centerX(); 1037 int centerY = sTempRect.centerY(); 1038 int centeredLeft = centerX - width / 2; 1039 int centeredTop = centerY - height / 2; 1040 1041 // We need to bound the folder to the currently visible workspace area 1042 mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect); 1043 int left = Math.min(Math.max(sTempRect.left, centeredLeft), 1044 sTempRect.right- width); 1045 int top = Math.min(Math.max(sTempRect.top, centeredTop), 1046 sTempRect.bottom - height); 1047 1048 int distFromEdgeOfScreen = mLauncher.getWorkspace().getPaddingLeft() + getPaddingLeft(); 1049 1050 if (grid.isPhone && (grid.availableWidthPx - width) < 4 * distFromEdgeOfScreen) { 1051 // Center the folder if it is very close to being centered anyway, by virtue of 1052 // filling the majority of the viewport. ie. remove it from the uncanny valley 1053 // of centeredness. 1054 left = (grid.availableWidthPx - width) / 2; 1055 } else if (width >= sTempRect.width()) { 1056 // If the folder doesn't fit within the bounds, center it about the desired bounds 1057 left = sTempRect.left + (sTempRect.width() - width) / 2; 1058 } 1059 if (height >= sTempRect.height()) { 1060 // Folder height is greater than page height, center on page 1061 top = sTempRect.top + (sTempRect.height() - height) / 2; 1062 } else { 1063 // Folder height is less than page height, so bound it to the absolute open folder 1064 // bounds if necessary 1065 Rect folderBounds = grid.getAbsoluteOpenFolderBounds(); 1066 left = Math.max(folderBounds.left, Math.min(left, folderBounds.right - width)); 1067 top = Math.max(folderBounds.top, Math.min(top, folderBounds.bottom - height)); 1068 } 1069 1070 int folderPivotX = width / 2 + (centeredLeft - left); 1071 int folderPivotY = height / 2 + (centeredTop - top); 1072 setPivotX(folderPivotX); 1073 setPivotY(folderPivotY); 1074 mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() * 1075 (1.0f * folderPivotX / width)); 1076 mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * 1077 (1.0f * folderPivotY / height)); 1078 1079 lp.width = width; 1080 lp.height = height; 1081 lp.x = left; 1082 lp.y = top; 1083 } 1084 1085 public float getPivotXForIconAnimation() { 1086 return mFolderIconPivotX; 1087 } 1088 public float getPivotYForIconAnimation() { 1089 return mFolderIconPivotY; 1090 } 1091 1092 private int getContentAreaHeight() { 1093 DeviceProfile grid = mLauncher.getDeviceProfile(); 1094 int maxContentAreaHeight = grid.availableHeightPx 1095 - grid.getTotalWorkspacePadding().y - mFooterHeight; 1096 int height = Math.min(maxContentAreaHeight, 1097 mContent.getDesiredHeight()); 1098 return Math.max(height, MIN_CONTENT_DIMEN); 1099 } 1100 1101 private int getContentAreaWidth() { 1102 return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN); 1103 } 1104 1105 private int getFolderHeight() { 1106 return getFolderHeight(getContentAreaHeight()); 1107 } 1108 1109 private int getFolderHeight(int contentAreaHeight) { 1110 return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight; 1111 } 1112 1113 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1114 int contentWidth = getContentAreaWidth(); 1115 int contentHeight = getContentAreaHeight(); 1116 1117 int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY); 1118 int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY); 1119 1120 mContent.setFixedSize(contentWidth, contentHeight); 1121 mContent.measure(contentAreaWidthSpec, contentAreaHeightSpec); 1122 1123 if (mContent.getChildCount() > 0) { 1124 int cellIconGap = (mContent.getPageAt(0).getCellWidth() 1125 - mLauncher.getDeviceProfile().iconSizePx) / 2; 1126 mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap, 1127 mFooter.getPaddingTop(), 1128 mContent.getPaddingRight() + cellIconGap, 1129 mFooter.getPaddingBottom()); 1130 } 1131 mFooter.measure(contentAreaWidthSpec, 1132 MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY)); 1133 1134 int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth; 1135 int folderHeight = getFolderHeight(contentHeight); 1136 setMeasuredDimension(folderWidth, folderHeight); 1137 } 1138 1139 /** 1140 * Rearranges the children based on their rank. 1141 */ 1142 public void rearrangeChildren() { 1143 rearrangeChildren(-1); 1144 } 1145 1146 /** 1147 * Rearranges the children based on their rank. 1148 * @param itemCount if greater than the total children count, empty spaces are left at the end, 1149 * otherwise it is ignored. 1150 */ 1151 public void rearrangeChildren(int itemCount) { 1152 ArrayList<View> views = getItemsInReadingOrder(); 1153 mContent.arrangeChildren(views, Math.max(itemCount, views.size())); 1154 mItemsInvalidated = true; 1155 } 1156 1157 public int getItemCount() { 1158 return mContent.getItemCount(); 1159 } 1160 1161 @Thunk void replaceFolderWithFinalItem() { 1162 // Add the last remaining child to the workspace in place of the folder 1163 Runnable onCompleteRunnable = new Runnable() { 1164 @Override 1165 public void run() { 1166 int itemCount = mInfo.contents.size(); 1167 if (itemCount <= 1) { 1168 View newIcon = null; 1169 1170 if (itemCount == 1) { 1171 // Move the item from the folder to the workspace, in the position of the 1172 // folder 1173 CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, 1174 mInfo.screenId); 1175 ShortcutInfo finalItem = mInfo.contents.remove(0); 1176 newIcon = mLauncher.createShortcut(cellLayout, finalItem); 1177 LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, 1178 mInfo.screenId, mInfo.cellX, mInfo.cellY); 1179 } 1180 1181 // Remove the folder 1182 mLauncher.removeItem(mFolderIcon, mInfo, true /* deleteFromDb */); 1183 if (mFolderIcon instanceof DropTarget) { 1184 mDragController.removeDropTarget((DropTarget) mFolderIcon); 1185 } 1186 1187 if (newIcon != null) { 1188 // We add the child after removing the folder to prevent both from existing 1189 // at the same time in the CellLayout. We need to add the new item with 1190 // addInScreenFromBind() to ensure that hotseat items are placed correctly. 1191 mLauncher.getWorkspace().addInScreenFromBind(newIcon, mInfo.container, 1192 mInfo.screenId, mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY); 1193 1194 // Focus the newly created child 1195 newIcon.requestFocus(); 1196 } 1197 } 1198 } 1199 }; 1200 View finalChild = mContent.getLastItem(); 1201 if (finalChild != null) { 1202 mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); 1203 } else { 1204 onCompleteRunnable.run(); 1205 } 1206 mDestroyed = true; 1207 } 1208 1209 public boolean isDestroyed() { 1210 return mDestroyed; 1211 } 1212 1213 // This method keeps track of the first and last item in the folder for the purposes 1214 // of keyboard focus 1215 public void updateTextViewFocus() { 1216 final View firstChild = mContent.getFirstItem(); 1217 final View lastChild = mContent.getLastItem(); 1218 if (firstChild != null && lastChild != null) { 1219 mFolderName.setNextFocusDownId(lastChild.getId()); 1220 mFolderName.setNextFocusRightId(lastChild.getId()); 1221 mFolderName.setNextFocusLeftId(lastChild.getId()); 1222 mFolderName.setNextFocusUpId(lastChild.getId()); 1223 // Hitting TAB from the folder name wraps around to the first item on the current 1224 // folder page, and hitting SHIFT+TAB from that item wraps back to the folder name. 1225 mFolderName.setNextFocusForwardId(firstChild.getId()); 1226 // When clicking off the folder when editing the name, this Folder gains focus. When 1227 // pressing an arrow key from that state, give the focus to the first item. 1228 this.setNextFocusDownId(firstChild.getId()); 1229 this.setNextFocusRightId(firstChild.getId()); 1230 this.setNextFocusLeftId(firstChild.getId()); 1231 this.setNextFocusUpId(firstChild.getId()); 1232 // When pressing shift+tab in the above state, give the focus to the last item. 1233 setOnKeyListener(new OnKeyListener() { 1234 @Override 1235 public boolean onKey(View v, int keyCode, KeyEvent event) { 1236 boolean isShiftPlusTab = keyCode == KeyEvent.KEYCODE_TAB && 1237 event.hasModifiers(KeyEvent.META_SHIFT_ON); 1238 if (isShiftPlusTab && Folder.this.isFocused()) { 1239 return lastChild.requestFocus(); 1240 } 1241 return false; 1242 } 1243 }); 1244 } 1245 } 1246 1247 public void onDrop(DragObject d) { 1248 Runnable cleanUpRunnable = null; 1249 1250 // If we are coming from All Apps space, we defer removing the extra empty screen 1251 // until the folder closes 1252 if (d.dragSource != mLauncher.getWorkspace() && !(d.dragSource instanceof Folder)) { 1253 cleanUpRunnable = new Runnable() { 1254 @Override 1255 public void run() { 1256 mLauncher.exitSpringLoadedDragModeDelayed(true, 1257 Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, 1258 null); 1259 } 1260 }; 1261 } 1262 1263 // If the icon was dropped while the page was being scrolled, we need to compute 1264 // the target location again such that the icon is placed of the final page. 1265 if (!mContent.rankOnCurrentPage(mEmptyCellRank)) { 1266 // Reorder again. 1267 mTargetRank = getTargetRank(d, null); 1268 1269 // Rearrange items immediately. 1270 mReorderAlarmListener.onAlarm(mReorderAlarm); 1271 1272 mOnScrollHintAlarm.cancelAlarm(); 1273 mScrollPauseAlarm.cancelAlarm(); 1274 } 1275 mContent.completePendingPageChanges(); 1276 1277 View currentDragView; 1278 final ShortcutInfo si; 1279 if (d.dragInfo instanceof AppInfo) { 1280 // Came from all apps -- make a copy. 1281 si = ((AppInfo) d.dragInfo).makeShortcut(); 1282 } else { 1283 // ShortcutInfo 1284 si = (ShortcutInfo) d.dragInfo; 1285 } 1286 if (mIsExternalDrag) { 1287 currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank); 1288 // Actually move the item in the database if it was an external drag. Call this 1289 // before creating the view, so that ShortcutInfo is updated appropriately. 1290 LauncherModel.addOrMoveItemInDatabase( 1291 mLauncher, si, mInfo.id, 0, si.cellX, si.cellY); 1292 1293 // We only need to update the locations if it doesn't get handled in #onDropCompleted. 1294 if (d.dragSource != this) { 1295 updateItemLocationsInDatabaseBatch(); 1296 } 1297 mIsExternalDrag = false; 1298 } else { 1299 currentDragView = mCurrentDragView; 1300 mContent.addViewForRank(currentDragView, si, mEmptyCellRank); 1301 } 1302 1303 if (d.dragView.hasDrawn()) { 1304 1305 // Temporarily reset the scale such that the animation target gets calculated correctly. 1306 float scaleX = getScaleX(); 1307 float scaleY = getScaleY(); 1308 setScaleX(1.0f); 1309 setScaleY(1.0f); 1310 mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, currentDragView, 1311 cleanUpRunnable, null); 1312 setScaleX(scaleX); 1313 setScaleY(scaleY); 1314 } else { 1315 d.deferDragViewCleanupPostAnimation = false; 1316 currentDragView.setVisibility(VISIBLE); 1317 } 1318 mItemsInvalidated = true; 1319 rearrangeChildren(); 1320 1321 // Temporarily suppress the listener, as we did all the work already here. 1322 try (SuppressInfoChanges s = new SuppressInfoChanges()) { 1323 mInfo.add(si, false); 1324 } 1325 1326 // Clear the drag info, as it is no longer being dragged. 1327 mDragInProgress = false; 1328 1329 if (mContent.getPageCount() > 1) { 1330 // The animation has already been shown while opening the folder. 1331 mInfo.setOption(FolderInfo.FLAG_MULTI_PAGE_ANIMATION, true, mLauncher); 1332 } 1333 } 1334 1335 // This is used so the item doesn't immediately appear in the folder when added. In one case 1336 // we need to create the illusion that the item isn't added back to the folder yet, to 1337 // to correspond to the animation of the icon back into the folder. This is 1338 public void hideItem(ShortcutInfo info) { 1339 View v = getViewForInfo(info); 1340 v.setVisibility(INVISIBLE); 1341 } 1342 public void showItem(ShortcutInfo info) { 1343 View v = getViewForInfo(info); 1344 v.setVisibility(VISIBLE); 1345 } 1346 1347 @Override 1348 public void onAdd(ShortcutInfo item) { 1349 mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem()); 1350 mItemsInvalidated = true; 1351 LauncherModel.addOrMoveItemInDatabase( 1352 mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); 1353 } 1354 1355 public void onRemove(ShortcutInfo item) { 1356 mItemsInvalidated = true; 1357 View v = getViewForInfo(item); 1358 mContent.removeItem(v); 1359 if (mState == STATE_ANIMATING) { 1360 mRearrangeOnClose = true; 1361 } else { 1362 rearrangeChildren(); 1363 } 1364 if (getItemCount() <= 1) { 1365 if (mInfo.opened) { 1366 mLauncher.closeFolder(this, true); 1367 } else { 1368 replaceFolderWithFinalItem(); 1369 } 1370 } 1371 } 1372 1373 private View getViewForInfo(final ShortcutInfo item) { 1374 return mContent.iterateOverItems(new ItemOperator() { 1375 1376 @Override 1377 public boolean evaluate(ItemInfo info, View view) { 1378 return info == item; 1379 } 1380 }); 1381 } 1382 1383 @Override 1384 public void onItemsChanged(boolean animate) { 1385 updateTextViewFocus(); 1386 } 1387 1388 public void onTitleChanged(CharSequence title) { 1389 } 1390 1391 public ArrayList<View> getItemsInReadingOrder() { 1392 if (mItemsInvalidated) { 1393 mItemsInReadingOrder.clear(); 1394 mContent.iterateOverItems(new ItemOperator() { 1395 1396 @Override 1397 public boolean evaluate(ItemInfo info, View view) { 1398 mItemsInReadingOrder.add(view); 1399 return false; 1400 } 1401 }); 1402 mItemsInvalidated = false; 1403 } 1404 return mItemsInReadingOrder; 1405 } 1406 1407 public void onFocusChange(View v, boolean hasFocus) { 1408 if (v == mFolderName) { 1409 if (hasFocus) { 1410 startEditingFolderName(); 1411 } else { 1412 dismissEditingName(); 1413 } 1414 } 1415 } 1416 1417 @Override 1418 public void getHitRectRelativeToDragLayer(Rect outRect) { 1419 getHitRect(outRect); 1420 outRect.left -= mScrollAreaOffset; 1421 outRect.right += mScrollAreaOffset; 1422 } 1423 1424 @Override 1425 public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) { 1426 target.gridX = info.cellX; 1427 target.gridY = info.cellY; 1428 target.pageIndex = mContent.getCurrentPage(); 1429 targetParent.containerType = LauncherLogProto.FOLDER; 1430 } 1431 1432 private class OnScrollHintListener implements OnAlarmListener { 1433 1434 private final DragObject mDragObject; 1435 1436 OnScrollHintListener(DragObject object) { 1437 mDragObject = object; 1438 } 1439 1440 /** 1441 * Scroll hint has been shown long enough. Now scroll to appropriate page. 1442 */ 1443 @Override 1444 public void onAlarm(Alarm alarm) { 1445 if (mCurrentScrollDir == DragController.SCROLL_LEFT) { 1446 mContent.scrollLeft(); 1447 mScrollHintDir = DragController.SCROLL_NONE; 1448 } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) { 1449 mContent.scrollRight(); 1450 mScrollHintDir = DragController.SCROLL_NONE; 1451 } else { 1452 // This should not happen 1453 return; 1454 } 1455 mCurrentScrollDir = DragController.SCROLL_NONE; 1456 1457 // Pause drag event until the scrolling is finished 1458 mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject)); 1459 mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY); 1460 } 1461 } 1462 1463 private class OnScrollFinishedListener implements OnAlarmListener { 1464 1465 private final DragObject mDragObject; 1466 1467 OnScrollFinishedListener(DragObject object) { 1468 mDragObject = object; 1469 } 1470 1471 /** 1472 * Page scroll is complete. 1473 */ 1474 @Override 1475 public void onAlarm(Alarm alarm) { 1476 // Reorder immediately on page change. 1477 onDragOver(mDragObject, 1); 1478 } 1479 } 1480 1481 // Compares item position based on rank and position giving priority to the rank. 1482 public static final Comparator<ItemInfo> ITEM_POS_COMPARATOR = new Comparator<ItemInfo>() { 1483 1484 @Override 1485 public int compare(ItemInfo lhs, ItemInfo rhs) { 1486 if (lhs.rank != rhs.rank) { 1487 return lhs.rank - rhs.rank; 1488 } else if (lhs.cellY != rhs.cellY) { 1489 return lhs.cellY - rhs.cellY; 1490 } else { 1491 return lhs.cellX - rhs.cellX; 1492 } 1493 } 1494 }; 1495 1496 /** 1497 * Temporary resource held while we don't want to handle info changes 1498 */ 1499 private class SuppressInfoChanges implements AutoCloseable { 1500 1501 SuppressInfoChanges() { 1502 mInfo.removeListener(Folder.this); 1503 } 1504 1505 @Override 1506 public void close() { 1507 mInfo.addListener(Folder.this); 1508 updateTextViewFocus(); 1509 } 1510 } 1511} 1512