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