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