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