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