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