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