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.ObjectAnimator; 22import android.animation.PropertyValuesHolder; 23import android.content.Context; 24import android.content.res.Resources; 25import android.graphics.PointF; 26import android.graphics.Rect; 27import android.graphics.drawable.Drawable; 28import android.os.SystemClock; 29import android.support.v4.widget.AutoScrollHelper; 30import android.text.InputType; 31import android.text.Selection; 32import android.text.Spannable; 33import android.util.AttributeSet; 34import android.util.DisplayMetrics; 35import android.util.Log; 36import android.util.TypedValue; 37import android.view.ActionMode; 38import android.view.KeyEvent; 39import android.view.LayoutInflater; 40import android.view.Menu; 41import android.view.MenuItem; 42import android.view.MotionEvent; 43import android.view.View; 44import android.view.accessibility.AccessibilityEvent; 45import android.view.accessibility.AccessibilityManager; 46import android.view.animation.AccelerateInterpolator; 47import android.view.animation.Interpolator; 48import android.view.inputmethod.EditorInfo; 49import android.view.inputmethod.InputMethodManager; 50import android.widget.LinearLayout; 51import android.widget.ScrollView; 52import android.widget.TextView; 53 54import com.android.launcher3.FolderInfo.FolderListener; 55 56import java.util.ArrayList; 57import java.util.Collections; 58import java.util.Comparator; 59 60/** 61 * Represents a set of icons chosen by the user or generated by the system. 62 */ 63public class Folder extends LinearLayout implements DragSource, View.OnClickListener, 64 View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, 65 View.OnFocusChangeListener { 66 private static final String TAG = "Launcher.Folder"; 67 68 protected DragController mDragController; 69 protected Launcher mLauncher; 70 protected FolderInfo mInfo; 71 72 static final int STATE_NONE = -1; 73 static final int STATE_SMALL = 0; 74 static final int STATE_ANIMATING = 1; 75 static final int STATE_OPEN = 2; 76 77 private int mExpandDuration; 78 protected CellLayout mContent; 79 private ScrollView mScrollView; 80 private final LayoutInflater mInflater; 81 private final IconCache mIconCache; 82 private int mState = STATE_NONE; 83 private static final int REORDER_ANIMATION_DURATION = 230; 84 private static final int REORDER_DELAY = 250; 85 private static final int ON_EXIT_CLOSE_DELAY = 800; 86 private boolean mRearrangeOnClose = false; 87 private FolderIcon mFolderIcon; 88 private int mMaxCountX; 89 private int mMaxCountY; 90 private int mMaxNumItems; 91 private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); 92 private Drawable mIconDrawable; 93 boolean mItemsInvalidated = false; 94 private ShortcutInfo mCurrentDragInfo; 95 private View mCurrentDragView; 96 boolean mSuppressOnAdd = false; 97 private int[] mTargetCell = new int[2]; 98 private int[] mPreviousTargetCell = new int[2]; 99 private int[] mEmptyCell = new int[2]; 100 private Alarm mReorderAlarm = new Alarm(); 101 private Alarm mOnExitAlarm = new Alarm(); 102 private int mFolderNameHeight; 103 private Rect mTempRect = new Rect(); 104 private boolean mDragInProgress = false; 105 private boolean mDeleteFolderOnDropCompleted = false; 106 private boolean mSuppressFolderDeletion = false; 107 private boolean mItemAddedBackToSelfViaIcon = false; 108 FolderEditText mFolderName; 109 private float mFolderIconPivotX; 110 private float mFolderIconPivotY; 111 112 private boolean mIsEditingName = false; 113 private InputMethodManager mInputMethodManager; 114 115 private static String sDefaultFolderName; 116 private static String sHintText; 117 118 private int DRAG_MODE_NONE = 0; 119 private int DRAG_MODE_REORDER = 1; 120 private int mDragMode = DRAG_MODE_NONE; 121 122 private boolean mDestroyed; 123 124 private AutoScrollHelper mAutoScrollHelper; 125 126 private Runnable mDeferredAction; 127 private boolean mDeferDropAfterUninstall; 128 private boolean mUninstallSuccessful; 129 130 /** 131 * Used to inflate the Workspace from XML. 132 * 133 * @param context The application's context. 134 * @param attrs The attribtues set containing the Workspace's customization values. 135 */ 136 public Folder(Context context, AttributeSet attrs) { 137 super(context, attrs); 138 139 LauncherAppState app = LauncherAppState.getInstance(); 140 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); 141 setAlwaysDrawnWithCacheEnabled(false); 142 mInflater = LayoutInflater.from(context); 143 mIconCache = app.getIconCache(); 144 145 Resources res = getResources(); 146 mMaxCountX = (int) grid.numColumns; 147 mMaxCountY = (int) grid.numRows; 148 mMaxNumItems = mMaxCountX * mMaxCountY; 149 150 mInputMethodManager = (InputMethodManager) 151 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 152 153 mExpandDuration = res.getInteger(R.integer.config_folderAnimDuration); 154 155 if (sDefaultFolderName == null) { 156 sDefaultFolderName = res.getString(R.string.folder_name); 157 } 158 if (sHintText == null) { 159 sHintText = res.getString(R.string.folder_hint_text); 160 } 161 mLauncher = (Launcher) context; 162 // We need this view to be focusable in touch mode so that when text editing of the folder 163 // name is complete, we have something to focus on, thus hiding the cursor and giving 164 // reliable behvior when clicking the text field (since it will always gain focus on click). 165 setFocusableInTouchMode(true); 166 } 167 168 @Override 169 protected void onFinishInflate() { 170 super.onFinishInflate(); 171 mScrollView = (ScrollView) findViewById(R.id.scroll_view); 172 mContent = (CellLayout) findViewById(R.id.folder_content); 173 174 LauncherAppState app = LauncherAppState.getInstance(); 175 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); 176 177 mContent.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx); 178 mContent.setGridSize(0, 0); 179 mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); 180 mContent.setInvertIfRtl(true); 181 mFolderName = (FolderEditText) findViewById(R.id.folder_name); 182 mFolderName.setFolder(this); 183 mFolderName.setOnFocusChangeListener(this); 184 185 // We find out how tall the text view wants to be (it is set to wrap_content), so that 186 // we can allocate the appropriate amount of space for it. 187 int measureSpec = MeasureSpec.UNSPECIFIED; 188 mFolderName.measure(measureSpec, measureSpec); 189 mFolderNameHeight = mFolderName.getMeasuredHeight(); 190 191 // We disable action mode for now since it messes up the view on phones 192 mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback); 193 mFolderName.setOnEditorActionListener(this); 194 mFolderName.setSelectAllOnFocus(true); 195 mFolderName.setInputType(mFolderName.getInputType() | 196 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); 197 mAutoScrollHelper = new FolderAutoScrollHelper(mScrollView); 198 } 199 200 private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { 201 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 202 return false; 203 } 204 205 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 206 return false; 207 } 208 209 public void onDestroyActionMode(ActionMode mode) { 210 } 211 212 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 213 return false; 214 } 215 }; 216 217 public void onClick(View v) { 218 Object tag = v.getTag(); 219 if (tag instanceof ShortcutInfo) { 220 mLauncher.onClick(v); 221 } 222 } 223 224 public boolean onLongClick(View v) { 225 // Return if global dragging is not enabled 226 if (!mLauncher.isDraggingEnabled()) return true; 227 228 Object tag = v.getTag(); 229 if (tag instanceof ShortcutInfo) { 230 ShortcutInfo item = (ShortcutInfo) tag; 231 if (!v.isInTouchMode()) { 232 return false; 233 } 234 235 mLauncher.dismissFolderCling(null); 236 237 mLauncher.getWorkspace().onDragStartedWithItem(v); 238 mLauncher.getWorkspace().beginDragShared(v, this); 239 mIconDrawable = ((TextView) v).getCompoundDrawables()[1]; 240 241 mCurrentDragInfo = item; 242 mEmptyCell[0] = item.cellX; 243 mEmptyCell[1] = item.cellY; 244 mCurrentDragView = v; 245 246 mContent.removeView(mCurrentDragView); 247 mInfo.remove(mCurrentDragInfo); 248 mDragInProgress = true; 249 mItemAddedBackToSelfViaIcon = false; 250 } 251 return true; 252 } 253 254 public boolean isEditingName() { 255 return mIsEditingName; 256 } 257 258 public void startEditingFolderName() { 259 mFolderName.setHint(""); 260 mIsEditingName = true; 261 } 262 263 public void dismissEditingName() { 264 mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 265 doneEditingFolderName(true); 266 } 267 268 public void doneEditingFolderName(boolean commit) { 269 mFolderName.setHint(sHintText); 270 // Convert to a string here to ensure that no other state associated with the text field 271 // gets saved. 272 String newTitle = mFolderName.getText().toString(); 273 mInfo.setTitle(newTitle); 274 LauncherModel.updateItemInDatabase(mLauncher, mInfo); 275 276 if (commit) { 277 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 278 String.format(getContext().getString(R.string.folder_renamed), newTitle)); 279 } 280 // In order to clear the focus from the text field, we set the focus on ourself. This 281 // ensures that every time the field is clicked, focus is gained, giving reliable behavior. 282 requestFocus(); 283 284 Selection.setSelection((Spannable) mFolderName.getText(), 0, 0); 285 mIsEditingName = false; 286 } 287 288 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 289 if (actionId == EditorInfo.IME_ACTION_DONE) { 290 dismissEditingName(); 291 return true; 292 } 293 return false; 294 } 295 296 public View getEditTextRegion() { 297 return mFolderName; 298 } 299 300 public Drawable getDragDrawable() { 301 return mIconDrawable; 302 } 303 304 /** 305 * We need to handle touch events to prevent them from falling through to the workspace below. 306 */ 307 @Override 308 public boolean onTouchEvent(MotionEvent ev) { 309 return true; 310 } 311 312 public void setDragController(DragController dragController) { 313 mDragController = dragController; 314 } 315 316 void setFolderIcon(FolderIcon icon) { 317 mFolderIcon = icon; 318 } 319 320 @Override 321 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 322 // When the folder gets focus, we don't want to announce the list of items. 323 return true; 324 } 325 326 /** 327 * @return the FolderInfo object associated with this folder 328 */ 329 FolderInfo getInfo() { 330 return mInfo; 331 } 332 333 private class GridComparator implements Comparator<ShortcutInfo> { 334 int mNumCols; 335 public GridComparator(int numCols) { 336 mNumCols = numCols; 337 } 338 339 @Override 340 public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { 341 int lhIndex = lhs.cellY * mNumCols + lhs.cellX; 342 int rhIndex = rhs.cellY * mNumCols + rhs.cellX; 343 return (lhIndex - rhIndex); 344 } 345 } 346 347 private void placeInReadingOrder(ArrayList<ShortcutInfo> items) { 348 int maxX = 0; 349 int count = items.size(); 350 for (int i = 0; i < count; i++) { 351 ShortcutInfo item = items.get(i); 352 if (item.cellX > maxX) { 353 maxX = item.cellX; 354 } 355 } 356 357 GridComparator gridComparator = new GridComparator(maxX + 1); 358 Collections.sort(items, gridComparator); 359 final int countX = mContent.getCountX(); 360 for (int i = 0; i < count; i++) { 361 int x = i % countX; 362 int y = i / countX; 363 ShortcutInfo item = items.get(i); 364 item.cellX = x; 365 item.cellY = y; 366 } 367 } 368 369 void bind(FolderInfo info) { 370 mInfo = info; 371 ArrayList<ShortcutInfo> children = info.contents; 372 ArrayList<ShortcutInfo> overflow = new ArrayList<ShortcutInfo>(); 373 setupContentForNumItems(children.size()); 374 placeInReadingOrder(children); 375 int count = 0; 376 for (int i = 0; i < children.size(); i++) { 377 ShortcutInfo child = (ShortcutInfo) children.get(i); 378 if (!createAndAddShortcut(child)) { 379 overflow.add(child); 380 } else { 381 count++; 382 } 383 } 384 385 // We rearrange the items in case there are any empty gaps 386 setupContentForNumItems(count); 387 388 // If our folder has too many items we prune them from the list. This is an issue 389 // when upgrading from the old Folders implementation which could contain an unlimited 390 // number of items. 391 for (ShortcutInfo item: overflow) { 392 mInfo.remove(item); 393 LauncherModel.deleteItemFromDatabase(mLauncher, item); 394 } 395 396 mItemsInvalidated = true; 397 updateTextViewFocus(); 398 mInfo.addListener(this); 399 400 if (!sDefaultFolderName.contentEquals(mInfo.title)) { 401 mFolderName.setText(mInfo.title); 402 } else { 403 mFolderName.setText(""); 404 } 405 updateItemLocationsInDatabase(); 406 } 407 408 /** 409 * Creates a new UserFolder, inflated from R.layout.user_folder. 410 * 411 * @param context The application's context. 412 * 413 * @return A new UserFolder. 414 */ 415 static Folder fromXml(Context context) { 416 return (Folder) LayoutInflater.from(context).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 public void animateOpen() { 432 positionAndSizeAsIcon(); 433 434 if (!(getParent() instanceof DragLayer)) return; 435 centerAboutIcon(); 436 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); 437 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); 438 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); 439 final ObjectAnimator oa = 440 LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); 441 442 oa.addListener(new AnimatorListenerAdapter() { 443 @Override 444 public void onAnimationStart(Animator animation) { 445 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 446 String.format(getContext().getString(R.string.folder_opened), 447 mContent.getCountX(), mContent.getCountY())); 448 mState = STATE_ANIMATING; 449 } 450 @Override 451 public void onAnimationEnd(Animator animation) { 452 mState = STATE_OPEN; 453 setLayerType(LAYER_TYPE_NONE, null); 454 Cling cling = mLauncher.showFirstRunFoldersCling(); 455 if (cling != null) { 456 cling.bringScrimToFront(); 457 bringToFront(); 458 cling.bringToFront(); 459 } 460 setFocusOnFirstChild(); 461 } 462 }); 463 oa.setDuration(mExpandDuration); 464 setLayerType(LAYER_TYPE_HARDWARE, null); 465 oa.start(); 466 } 467 468 private void sendCustomAccessibilityEvent(int type, String text) { 469 AccessibilityManager accessibilityManager = (AccessibilityManager) 470 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 471 if (accessibilityManager.isEnabled()) { 472 AccessibilityEvent event = AccessibilityEvent.obtain(type); 473 onInitializeAccessibilityEvent(event); 474 event.getText().add(text); 475 accessibilityManager.sendAccessibilityEvent(event); 476 } 477 } 478 479 private void setFocusOnFirstChild() { 480 View firstChild = mContent.getChildAt(0, 0); 481 if (firstChild != null) { 482 firstChild.requestFocus(); 483 } 484 } 485 486 public void animateClosed() { 487 if (!(getParent() instanceof DragLayer)) return; 488 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); 489 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); 490 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); 491 final ObjectAnimator oa = 492 LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); 493 494 oa.addListener(new AnimatorListenerAdapter() { 495 @Override 496 public void onAnimationEnd(Animator animation) { 497 onCloseComplete(); 498 setLayerType(LAYER_TYPE_NONE, null); 499 mState = STATE_SMALL; 500 } 501 @Override 502 public void onAnimationStart(Animator animation) { 503 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 504 getContext().getString(R.string.folder_closed)); 505 mState = STATE_ANIMATING; 506 } 507 }); 508 oa.setDuration(mExpandDuration); 509 setLayerType(LAYER_TYPE_HARDWARE, null); 510 oa.start(); 511 } 512 513 public boolean acceptDrop(DragObject d) { 514 final ItemInfo item = (ItemInfo) d.dragInfo; 515 final int itemType = item.itemType; 516 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 517 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && 518 !isFull()); 519 } 520 521 protected boolean findAndSetEmptyCells(ShortcutInfo item) { 522 int[] emptyCell = new int[2]; 523 if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) { 524 item.cellX = emptyCell[0]; 525 item.cellY = emptyCell[1]; 526 return true; 527 } else { 528 return false; 529 } 530 } 531 532 protected boolean createAndAddShortcut(ShortcutInfo item) { 533 final BubbleTextView textView = 534 (BubbleTextView) mInflater.inflate(R.layout.application, this, false); 535 textView.setCompoundDrawables(null, 536 Utilities.createIconDrawable(item.getIcon(mIconCache)), null, null); 537 textView.setText(item.title); 538 textView.setTag(item); 539 textView.setTextColor(getResources().getColor(R.color.folder_items_text_color)); 540 textView.setShadowsEnabled(false); 541 542 textView.setOnClickListener(this); 543 textView.setOnLongClickListener(this); 544 545 // We need to check here to verify that the given item's location isn't already occupied 546 // by another item. 547 if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0 548 || item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) { 549 // This shouldn't happen, log it. 550 Log.e(TAG, "Folder order not properly persisted during bind"); 551 if (!findAndSetEmptyCells(item)) { 552 return false; 553 } 554 } 555 556 CellLayout.LayoutParams lp = 557 new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); 558 boolean insert = false; 559 textView.setOnKeyListener(new FolderKeyEventListener()); 560 mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); 561 return true; 562 } 563 564 public void onDragEnter(DragObject d) { 565 mPreviousTargetCell[0] = -1; 566 mPreviousTargetCell[1] = -1; 567 mOnExitAlarm.cancelAlarm(); 568 } 569 570 OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { 571 public void onAlarm(Alarm alarm) { 572 realTimeReorder(mEmptyCell, mTargetCell); 573 } 574 }; 575 576 boolean readingOrderGreaterThan(int[] v1, int[] v2) { 577 if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) { 578 return true; 579 } else { 580 return false; 581 } 582 } 583 584 private void realTimeReorder(int[] empty, int[] target) { 585 boolean wrap; 586 int startX; 587 int endX; 588 int startY; 589 int delay = 0; 590 float delayAmount = 30; 591 if (readingOrderGreaterThan(target, empty)) { 592 wrap = empty[0] >= mContent.getCountX() - 1; 593 startY = wrap ? empty[1] + 1 : empty[1]; 594 for (int y = startY; y <= target[1]; y++) { 595 startX = y == empty[1] ? empty[0] + 1 : 0; 596 endX = y < target[1] ? mContent.getCountX() - 1 : target[0]; 597 for (int x = startX; x <= endX; x++) { 598 View v = mContent.getChildAt(x,y); 599 if (mContent.animateChildToPosition(v, empty[0], empty[1], 600 REORDER_ANIMATION_DURATION, delay, true, true)) { 601 empty[0] = x; 602 empty[1] = y; 603 delay += delayAmount; 604 delayAmount *= 0.9; 605 } 606 } 607 } 608 } else { 609 wrap = empty[0] == 0; 610 startY = wrap ? empty[1] - 1 : empty[1]; 611 for (int y = startY; y >= target[1]; y--) { 612 startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1; 613 endX = y > target[1] ? 0 : target[0]; 614 for (int x = startX; x >= endX; x--) { 615 View v = mContent.getChildAt(x,y); 616 if (mContent.animateChildToPosition(v, empty[0], empty[1], 617 REORDER_ANIMATION_DURATION, delay, true, true)) { 618 empty[0] = x; 619 empty[1] = y; 620 delay += delayAmount; 621 delayAmount *= 0.9; 622 } 623 } 624 } 625 } 626 } 627 628 public boolean isLayoutRtl() { 629 return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); 630 } 631 632 public void onDragOver(DragObject d) { 633 final DragView dragView = d.dragView; 634 final int scrollOffset = mScrollView.getScrollY(); 635 final float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, dragView, null); 636 r[0] -= getPaddingLeft(); 637 r[1] -= getPaddingTop(); 638 639 final long downTime = SystemClock.uptimeMillis(); 640 final MotionEvent translatedEv = MotionEvent.obtain( 641 downTime, downTime, MotionEvent.ACTION_MOVE, d.x, d.y, 0); 642 643 if (!mAutoScrollHelper.isEnabled()) { 644 mAutoScrollHelper.setEnabled(true); 645 } 646 647 final boolean handled = mAutoScrollHelper.onTouch(this, translatedEv); 648 translatedEv.recycle(); 649 650 if (handled) { 651 mReorderAlarm.cancelAlarm(); 652 } else { 653 mTargetCell = mContent.findNearestArea( 654 (int) r[0], (int) r[1] + scrollOffset, 1, 1, mTargetCell); 655 if (isLayoutRtl()) { 656 mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1; 657 } 658 if (mTargetCell[0] != mPreviousTargetCell[0] 659 || mTargetCell[1] != mPreviousTargetCell[1]) { 660 mReorderAlarm.cancelAlarm(); 661 mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); 662 mReorderAlarm.setAlarm(REORDER_DELAY); 663 mPreviousTargetCell[0] = mTargetCell[0]; 664 mPreviousTargetCell[1] = mTargetCell[1]; 665 mDragMode = DRAG_MODE_REORDER; 666 } else { 667 mDragMode = DRAG_MODE_NONE; 668 } 669 } 670 } 671 672 // This is used to compute the visual center of the dragView. The idea is that 673 // the visual center represents the user's interpretation of where the item is, and hence 674 // is the appropriate point to use when determining drop location. 675 private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, 676 DragView dragView, float[] recycle) { 677 float res[]; 678 if (recycle == null) { 679 res = new float[2]; 680 } else { 681 res = recycle; 682 } 683 684 // These represent the visual top and left of drag view if a dragRect was provided. 685 // If a dragRect was not provided, then they correspond to the actual view left and 686 // top, as the dragRect is in that case taken to be the entire dragView. 687 // R.dimen.dragViewOffsetY. 688 int left = x - xOffset; 689 int top = y - yOffset; 690 691 // In order to find the visual center, we shift by half the dragRect 692 res[0] = left + dragView.getDragRegion().width() / 2; 693 res[1] = top + dragView.getDragRegion().height() / 2; 694 695 return res; 696 } 697 698 OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { 699 public void onAlarm(Alarm alarm) { 700 completeDragExit(); 701 } 702 }; 703 704 public void completeDragExit() { 705 mLauncher.closeFolder(); 706 mCurrentDragInfo = null; 707 mCurrentDragView = null; 708 mSuppressOnAdd = false; 709 mRearrangeOnClose = true; 710 } 711 712 public void onDragExit(DragObject d) { 713 // Exiting folder; stop the auto scroller. 714 mAutoScrollHelper.setEnabled(false); 715 // We only close the folder if this is a true drag exit, ie. not because 716 // a drop has occurred above the folder. 717 if (!d.dragComplete) { 718 mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener); 719 mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); 720 } 721 mReorderAlarm.cancelAlarm(); 722 mDragMode = DRAG_MODE_NONE; 723 } 724 725 public void onDropCompleted(final View target, final DragObject d, 726 final boolean isFlingToDelete, final boolean success) { 727 if (mDeferDropAfterUninstall) { 728 Log.d(TAG, "Deferred handling drop because waiting for uninstall."); 729 mDeferredAction = new Runnable() { 730 public void run() { 731 onDropCompleted(target, d, isFlingToDelete, success); 732 mDeferredAction = null; 733 } 734 }; 735 return; 736 } 737 738 boolean beingCalledAfterUninstall = mDeferredAction != null; 739 boolean successfulDrop = 740 success && (!beingCalledAfterUninstall || mUninstallSuccessful); 741 742 if (successfulDrop) { 743 if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon) { 744 replaceFolderWithFinalItem(); 745 } 746 } else { 747 setupContentForNumItems(getItemCount()); 748 // The drag failed, we need to return the item to the folder 749 mFolderIcon.onDrop(d); 750 } 751 752 if (target != this) { 753 if (mOnExitAlarm.alarmPending()) { 754 mOnExitAlarm.cancelAlarm(); 755 if (!successfulDrop) { 756 mSuppressFolderDeletion = true; 757 } 758 completeDragExit(); 759 } 760 } 761 762 mDeleteFolderOnDropCompleted = false; 763 mDragInProgress = false; 764 mItemAddedBackToSelfViaIcon = false; 765 mCurrentDragInfo = null; 766 mCurrentDragView = null; 767 mSuppressOnAdd = false; 768 769 // Reordering may have occured, and we need to save the new item locations. We do this once 770 // at the end to prevent unnecessary database operations. 771 updateItemLocationsInDatabaseBatch(); 772 } 773 774 public void deferCompleteDropAfterUninstallActivity() { 775 mDeferDropAfterUninstall = true; 776 } 777 778 public void onUninstallActivityReturned(boolean success) { 779 mDeferDropAfterUninstall = false; 780 mUninstallSuccessful = success; 781 if (mDeferredAction != null) { 782 mDeferredAction.run(); 783 } 784 } 785 786 @Override 787 public boolean supportsFlingToDelete() { 788 return true; 789 } 790 791 public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { 792 // Do nothing 793 } 794 795 @Override 796 public void onFlingToDeleteCompleted() { 797 // Do nothing 798 } 799 800 private void updateItemLocationsInDatabase() { 801 ArrayList<View> list = getItemsInReadingOrder(); 802 for (int i = 0; i < list.size(); i++) { 803 View v = list.get(i); 804 ItemInfo info = (ItemInfo) v.getTag(); 805 LauncherModel.moveItemInDatabase(mLauncher, info, mInfo.id, 0, 806 info.cellX, info.cellY); 807 } 808 } 809 810 private void updateItemLocationsInDatabaseBatch() { 811 ArrayList<View> list = getItemsInReadingOrder(); 812 ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); 813 for (int i = 0; i < list.size(); i++) { 814 View v = list.get(i); 815 ItemInfo info = (ItemInfo) v.getTag(); 816 items.add(info); 817 } 818 819 LauncherModel.moveItemsInDatabase(mLauncher, items, mInfo.id, 0); 820 } 821 822 public void addItemLocationsInDatabase() { 823 ArrayList<View> list = getItemsInReadingOrder(); 824 for (int i = 0; i < list.size(); i++) { 825 View v = list.get(i); 826 ItemInfo info = (ItemInfo) v.getTag(); 827 LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0, 828 info.cellX, info.cellY, false); 829 } 830 } 831 832 public void notifyDrop() { 833 if (mDragInProgress) { 834 mItemAddedBackToSelfViaIcon = true; 835 } 836 } 837 838 public boolean isDropEnabled() { 839 return true; 840 } 841 842 private void setupContentDimensions(int count) { 843 ArrayList<View> list = getItemsInReadingOrder(); 844 845 int countX = mContent.getCountX(); 846 int countY = mContent.getCountY(); 847 boolean done = false; 848 849 while (!done) { 850 int oldCountX = countX; 851 int oldCountY = countY; 852 if (countX * countY < count) { 853 // Current grid is too small, expand it 854 if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) { 855 countX++; 856 } else if (countY < mMaxCountY) { 857 countY++; 858 } 859 if (countY == 0) countY++; 860 } else if ((countY - 1) * countX >= count && countY >= countX) { 861 countY = Math.max(0, countY - 1); 862 } else if ((countX - 1) * countY >= count) { 863 countX = Math.max(0, countX - 1); 864 } 865 done = countX == oldCountX && countY == oldCountY; 866 } 867 mContent.setGridSize(countX, countY); 868 arrangeChildren(list); 869 } 870 871 public boolean isFull() { 872 return getItemCount() >= mMaxNumItems; 873 } 874 875 private void centerAboutIcon() { 876 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 877 878 DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer); 879 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); 880 int height = getFolderHeight(); 881 882 float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect); 883 884 LauncherAppState app = LauncherAppState.getInstance(); 885 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); 886 887 int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2); 888 int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2); 889 int centeredLeft = centerX - width / 2; 890 int centeredTop = centerY - height / 2; 891 int currentPage = mLauncher.getWorkspace().getNextPage(); 892 // In case the workspace is scrolling, we need to use the final scroll to compute 893 // the folders bounds. 894 mLauncher.getWorkspace().setFinalScrollForPageChange(currentPage); 895 // We first fetch the currently visible CellLayoutChildren 896 CellLayout currentLayout = (CellLayout) mLauncher.getWorkspace().getChildAt(currentPage); 897 ShortcutAndWidgetContainer boundingLayout = currentLayout.getShortcutsAndWidgets(); 898 Rect bounds = new Rect(); 899 parent.getDescendantRectRelativeToSelf(boundingLayout, bounds); 900 // We reset the workspaces scroll 901 mLauncher.getWorkspace().resetFinalScrollForPageChange(currentPage); 902 903 // We need to bound the folder to the currently visible CellLayoutChildren 904 int left = Math.min(Math.max(bounds.left, centeredLeft), 905 bounds.left + bounds.width() - width); 906 int top = Math.min(Math.max(bounds.top, centeredTop), 907 bounds.top + bounds.height() - height); 908 if (grid.isPhone() && (grid.availableWidthPx - width) < grid.iconSizePx) { 909 // Center the folder if it is full (on phones only) 910 left = (grid.availableWidthPx - width) / 2; 911 } else if (width >= bounds.width()) { 912 // If the folder doesn't fit within the bounds, center it about the desired bounds 913 left = bounds.left + (bounds.width() - width) / 2; 914 } 915 if (height >= bounds.height()) { 916 top = bounds.top + (bounds.height() - height) / 2; 917 } 918 919 int folderPivotX = width / 2 + (centeredLeft - left); 920 int folderPivotY = height / 2 + (centeredTop - top); 921 setPivotX(folderPivotX); 922 setPivotY(folderPivotY); 923 mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() * 924 (1.0f * folderPivotX / width)); 925 mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * 926 (1.0f * folderPivotY / height)); 927 928 lp.width = width; 929 lp.height = height; 930 lp.x = left; 931 lp.y = top; 932 } 933 934 float getPivotXForIconAnimation() { 935 return mFolderIconPivotX; 936 } 937 float getPivotYForIconAnimation() { 938 return mFolderIconPivotY; 939 } 940 941 private void setupContentForNumItems(int count) { 942 setupContentDimensions(count); 943 944 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 945 if (lp == null) { 946 lp = new DragLayer.LayoutParams(0, 0); 947 lp.customPosition = true; 948 setLayoutParams(lp); 949 } 950 centerAboutIcon(); 951 } 952 953 private int getContentAreaHeight() { 954 LauncherAppState app = LauncherAppState.getInstance(); 955 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); 956 Rect workspacePadding = grid.getWorkspacePadding(grid.isLandscape ? 957 CellLayout.LANDSCAPE : CellLayout.PORTRAIT); 958 int maxContentAreaHeight = grid.availableHeightPx - 959 4 * grid.edgeMarginPx - 960 workspacePadding.top - workspacePadding.bottom - 961 getPaddingTop() - getPaddingBottom() - 962 mFolderNameHeight; 963 return Math.min(maxContentAreaHeight, 964 mContent.getDesiredHeight()); 965 } 966 967 private int getFolderHeight() { 968 int height = getPaddingTop() + getPaddingBottom() 969 + getContentAreaHeight() + mFolderNameHeight; 970 return height; 971 } 972 973 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 974 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); 975 int height = getFolderHeight(); 976 int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(), 977 MeasureSpec.EXACTLY); 978 int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(getContentAreaHeight(), 979 MeasureSpec.EXACTLY); 980 mContent.setFixedSize(mContent.getDesiredWidth(), mContent.getDesiredHeight()); 981 mScrollView.measure(contentAreaWidthSpec, contentAreaHeightSpec); 982 mFolderName.measure(contentAreaWidthSpec, 983 MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); 984 setMeasuredDimension(width, height); 985 } 986 987 private void arrangeChildren(ArrayList<View> list) { 988 int[] vacant = new int[2]; 989 if (list == null) { 990 list = getItemsInReadingOrder(); 991 } 992 mContent.removeAllViews(); 993 994 for (int i = 0; i < list.size(); i++) { 995 View v = list.get(i); 996 mContent.getVacantCell(vacant, 1, 1); 997 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); 998 lp.cellX = vacant[0]; 999 lp.cellY = vacant[1]; 1000 ItemInfo info = (ItemInfo) v.getTag(); 1001 if (info.cellX != vacant[0] || info.cellY != vacant[1]) { 1002 info.cellX = vacant[0]; 1003 info.cellY = vacant[1]; 1004 LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, 1005 info.cellX, info.cellY); 1006 } 1007 boolean insert = false; 1008 mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); 1009 } 1010 mItemsInvalidated = true; 1011 } 1012 1013 public int getItemCount() { 1014 return mContent.getShortcutsAndWidgets().getChildCount(); 1015 } 1016 1017 public View getItemAt(int index) { 1018 return mContent.getShortcutsAndWidgets().getChildAt(index); 1019 } 1020 1021 private void onCloseComplete() { 1022 DragLayer parent = (DragLayer) getParent(); 1023 if (parent != null) { 1024 parent.removeView(this); 1025 } 1026 mDragController.removeDropTarget((DropTarget) this); 1027 clearFocus(); 1028 mFolderIcon.requestFocus(); 1029 1030 if (mRearrangeOnClose) { 1031 setupContentForNumItems(getItemCount()); 1032 mRearrangeOnClose = false; 1033 } 1034 if (getItemCount() <= 1) { 1035 if (!mDragInProgress && !mSuppressFolderDeletion) { 1036 replaceFolderWithFinalItem(); 1037 } else if (mDragInProgress) { 1038 mDeleteFolderOnDropCompleted = true; 1039 } 1040 } 1041 mSuppressFolderDeletion = false; 1042 } 1043 1044 private void replaceFolderWithFinalItem() { 1045 // Add the last remaining child to the workspace in place of the folder 1046 Runnable onCompleteRunnable = new Runnable() { 1047 @Override 1048 public void run() { 1049 CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screenId); 1050 1051 View child = null; 1052 // Move the item from the folder to the workspace, in the position of the folder 1053 if (getItemCount() == 1) { 1054 ShortcutInfo finalItem = mInfo.contents.get(0); 1055 child = mLauncher.createShortcut(R.layout.application, cellLayout, 1056 finalItem); 1057 LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, 1058 mInfo.screenId, mInfo.cellX, mInfo.cellY); 1059 } 1060 if (getItemCount() <= 1) { 1061 // Remove the folder 1062 LauncherModel.deleteItemFromDatabase(mLauncher, mInfo); 1063 cellLayout.removeView(mFolderIcon); 1064 if (mFolderIcon instanceof DropTarget) { 1065 mDragController.removeDropTarget((DropTarget) mFolderIcon); 1066 } 1067 mLauncher.removeFolder(mInfo); 1068 } 1069 // We add the child after removing the folder to prevent both from existing at 1070 // the same time in the CellLayout. We need to add the new item with addInScreenFromBind() 1071 // to ensure that hotseat items are placed correctly. 1072 if (child != null) { 1073 mLauncher.getWorkspace().addInScreenFromBind(child, mInfo.container, mInfo.screenId, 1074 mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY); 1075 } 1076 } 1077 }; 1078 View finalChild = getItemAt(0); 1079 if (finalChild != null) { 1080 mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); 1081 } 1082 mDestroyed = true; 1083 } 1084 1085 boolean isDestroyed() { 1086 return mDestroyed; 1087 } 1088 1089 // This method keeps track of the last item in the folder for the purposes 1090 // of keyboard focus 1091 private void updateTextViewFocus() { 1092 View lastChild = getItemAt(getItemCount() - 1); 1093 getItemAt(getItemCount() - 1); 1094 if (lastChild != null) { 1095 mFolderName.setNextFocusDownId(lastChild.getId()); 1096 mFolderName.setNextFocusRightId(lastChild.getId()); 1097 mFolderName.setNextFocusLeftId(lastChild.getId()); 1098 mFolderName.setNextFocusUpId(lastChild.getId()); 1099 } 1100 } 1101 1102 public void onDrop(DragObject d) { 1103 ShortcutInfo item; 1104 if (d.dragInfo instanceof AppInfo) { 1105 // Came from all apps -- make a copy 1106 item = ((AppInfo) d.dragInfo).makeShortcut(); 1107 item.spanX = 1; 1108 item.spanY = 1; 1109 } else { 1110 item = (ShortcutInfo) d.dragInfo; 1111 } 1112 // Dragged from self onto self, currently this is the only path possible, however 1113 // we keep this as a distinct code path. 1114 if (item == mCurrentDragInfo) { 1115 ShortcutInfo si = (ShortcutInfo) mCurrentDragView.getTag(); 1116 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mCurrentDragView.getLayoutParams(); 1117 si.cellX = lp.cellX = mEmptyCell[0]; 1118 si.cellX = lp.cellY = mEmptyCell[1]; 1119 mContent.addViewToCellLayout(mCurrentDragView, -1, (int)item.id, lp, true); 1120 if (d.dragView.hasDrawn()) { 1121 mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, mCurrentDragView); 1122 } else { 1123 d.deferDragViewCleanupPostAnimation = false; 1124 mCurrentDragView.setVisibility(VISIBLE); 1125 } 1126 mItemsInvalidated = true; 1127 setupContentDimensions(getItemCount()); 1128 mSuppressOnAdd = true; 1129 } 1130 mInfo.add(item); 1131 } 1132 1133 // This is used so the item doesn't immediately appear in the folder when added. In one case 1134 // we need to create the illusion that the item isn't added back to the folder yet, to 1135 // to correspond to the animation of the icon back into the folder. This is 1136 public void hideItem(ShortcutInfo info) { 1137 View v = getViewForInfo(info); 1138 v.setVisibility(INVISIBLE); 1139 } 1140 public void showItem(ShortcutInfo info) { 1141 View v = getViewForInfo(info); 1142 v.setVisibility(VISIBLE); 1143 } 1144 1145 public void onAdd(ShortcutInfo item) { 1146 mItemsInvalidated = true; 1147 // If the item was dropped onto this open folder, we have done the work associated 1148 // with adding the item to the folder, as indicated by mSuppressOnAdd being set 1149 if (mSuppressOnAdd) return; 1150 if (!findAndSetEmptyCells(item)) { 1151 // The current layout is full, can we expand it? 1152 setupContentForNumItems(getItemCount() + 1); 1153 findAndSetEmptyCells(item); 1154 } 1155 createAndAddShortcut(item); 1156 LauncherModel.addOrMoveItemInDatabase( 1157 mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); 1158 } 1159 1160 public void onRemove(ShortcutInfo item) { 1161 mItemsInvalidated = true; 1162 // If this item is being dragged from this open folder, we have already handled 1163 // the work associated with removing the item, so we don't have to do anything here. 1164 if (item == mCurrentDragInfo) return; 1165 View v = getViewForInfo(item); 1166 mContent.removeView(v); 1167 if (mState == STATE_ANIMATING) { 1168 mRearrangeOnClose = true; 1169 } else { 1170 setupContentForNumItems(getItemCount()); 1171 } 1172 if (getItemCount() <= 1) { 1173 replaceFolderWithFinalItem(); 1174 } 1175 } 1176 1177 private View getViewForInfo(ShortcutInfo item) { 1178 for (int j = 0; j < mContent.getCountY(); j++) { 1179 for (int i = 0; i < mContent.getCountX(); i++) { 1180 View v = mContent.getChildAt(i, j); 1181 if (v.getTag() == item) { 1182 return v; 1183 } 1184 } 1185 } 1186 return null; 1187 } 1188 1189 public void onItemsChanged() { 1190 updateTextViewFocus(); 1191 } 1192 1193 public void onTitleChanged(CharSequence title) { 1194 } 1195 1196 public ArrayList<View> getItemsInReadingOrder() { 1197 if (mItemsInvalidated) { 1198 mItemsInReadingOrder.clear(); 1199 for (int j = 0; j < mContent.getCountY(); j++) { 1200 for (int i = 0; i < mContent.getCountX(); i++) { 1201 View v = mContent.getChildAt(i, j); 1202 if (v != null) { 1203 mItemsInReadingOrder.add(v); 1204 } 1205 } 1206 } 1207 mItemsInvalidated = false; 1208 } 1209 return mItemsInReadingOrder; 1210 } 1211 1212 public void getLocationInDragLayer(int[] loc) { 1213 mLauncher.getDragLayer().getLocationInDragLayer(this, loc); 1214 } 1215 1216 public void onFocusChange(View v, boolean hasFocus) { 1217 if (v == mFolderName && hasFocus) { 1218 startEditingFolderName(); 1219 } 1220 } 1221 1222 @Override 1223 public void getHitRectRelativeToDragLayer(Rect outRect) { 1224 getHitRect(outRect); 1225 } 1226} 1227