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