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