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