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