Folder.java revision 76078c4ef35b6e95fb6d8129a32cf52a36625e28
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.graphics.Rect; 27import android.graphics.drawable.Drawable; 28import android.util.AttributeSet; 29import android.view.LayoutInflater; 30import android.view.MotionEvent; 31import android.view.View; 32import android.view.View.OnClickListener; 33import android.view.animation.AccelerateInterpolator; 34import android.view.animation.DecelerateInterpolator; 35import android.widget.AdapterView; 36import android.widget.LinearLayout; 37import android.widget.TextView; 38import android.widget.AdapterView.OnItemClickListener; 39import android.widget.AdapterView.OnItemLongClickListener; 40 41import com.android.launcher.R; 42import com.android.launcher2.FolderInfo.FolderListener; 43 44import java.util.ArrayList; 45 46/** 47 * Represents a set of icons chosen by the user or generated by the system. 48 */ 49public class Folder extends LinearLayout implements DragSource, OnItemLongClickListener, 50 OnItemClickListener, OnClickListener, View.OnLongClickListener, DropTarget, FolderListener { 51 52 protected DragController mDragController; 53 54 protected Launcher mLauncher; 55 56 protected FolderInfo mInfo; 57 58 /** 59 * Which item is being dragged 60 */ 61 protected ShortcutInfo mDragItem; 62 63 private static final String TAG = "Launcher.Folder"; 64 65 static final int STATE_NONE = -1; 66 static final int STATE_SMALL = 0; 67 static final int STATE_ANIMATING = 1; 68 static final int STATE_OPEN = 2; 69 70 private int mExpandDuration; 71 protected CellLayout mContent; 72 private final LayoutInflater mInflater; 73 private final IconCache mIconCache; 74 private int mState = STATE_NONE; 75 private int[] mDragItemPosition = new int[2]; 76 private static final int FULL_GROW = 0; 77 private static final int PARTIAL_GROW = 1; 78 private int mMode = PARTIAL_GROW; 79 private boolean mRearrangeOnClose = false; 80 private FolderIcon mFolderIcon; 81 private int mMaxCountX; 82 private int mMaxCountY; 83 private Rect mNewSize = new Rect(); 84 private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); 85 private Drawable mIconDrawable; 86 boolean mItemsInvalidated = false; 87 ShortcutInfo mCurrentDragInfo; 88 89 /** 90 * Used to inflate the Workspace from XML. 91 * 92 * @param context The application's context. 93 * @param attrs The attribtues set containing the Workspace's customization values. 94 */ 95 public Folder(Context context, AttributeSet attrs) { 96 super(context, attrs); 97 setAlwaysDrawnWithCacheEnabled(false); 98 mInflater = LayoutInflater.from(context); 99 mIconCache = ((LauncherApplication)context.getApplicationContext()).getIconCache(); 100 mExpandDuration = getResources().getInteger(R.integer.config_folderAnimDuration); 101 102 mMaxCountX = LauncherModel.getCellCountX() - 1; 103 mMaxCountY = LauncherModel.getCellCountY() - 1; 104 } 105 106 @Override 107 protected void onFinishInflate() { 108 super.onFinishInflate(); 109 mContent = (CellLayout) findViewById(R.id.folder_content); 110 mContent.setGridSize(0, 0); 111 mContent.enableHardwareLayers(); 112 } 113 114 public void onItemClick(AdapterView parent, View v, int position, long id) { 115 ShortcutInfo app = (ShortcutInfo) parent.getItemAtPosition(position); 116 int[] pos = new int[2]; 117 v.getLocationOnScreen(pos); 118 app.intent.setSourceBounds(new Rect(pos[0], pos[1], 119 pos[0] + v.getWidth(), pos[1] + v.getHeight())); 120 mLauncher.startActivitySafely(app.intent, app); 121 } 122 123 public void onClick(View v) { 124 Object tag = v.getTag(); 125 if (tag instanceof ShortcutInfo) { 126 // refactor this code from Folder 127 ShortcutInfo item = (ShortcutInfo) tag; 128 int[] pos = new int[2]; 129 v.getLocationOnScreen(pos); 130 item.intent.setSourceBounds(new Rect(pos[0], pos[1], 131 pos[0] + v.getWidth(), pos[1] + v.getHeight())); 132 mLauncher.startActivitySafely(item.intent, item); 133 } 134 } 135 136 public boolean onLongClick(View v) { 137 Object tag = v.getTag(); 138 if (tag instanceof ShortcutInfo) { 139 mLauncher.closeFolder(this); 140 141 ShortcutInfo item = (ShortcutInfo) tag; 142 if (!v.isInTouchMode()) { 143 return false; 144 } 145 146 mLauncher.getWorkspace().onDragStartedWithItem(v); 147 mDragController.startDrag(v, this, item, DragController.DRAG_ACTION_COPY); 148 mDragItemPosition[0] = item.cellX; 149 mDragItemPosition[1] = item.cellY; 150 mIconDrawable = ((TextView) v).getCompoundDrawables()[1]; 151 152 mCurrentDragInfo = item; 153 mItemsInvalidated = true; 154 mInfo.itemsChanged(); 155 156 mDragItem = item; 157 } else { 158 mLauncher.closeFolder(this); 159 mLauncher.showRenameDialog(mInfo); 160 } 161 return true; 162 } 163 164 public Drawable getDragDrawable() { 165 return mIconDrawable; 166 } 167 168 /** 169 * We need to handle touch events to prevent them from falling through to the workspace below. 170 */ 171 @Override 172 public boolean onTouchEvent(MotionEvent ev) { 173 return true; 174 } 175 176 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 177 if (!view.isInTouchMode()) { 178 return false; 179 } 180 181 ShortcutInfo app = (ShortcutInfo) parent.getItemAtPosition(position); 182 183 mDragController.startDrag(view, this, app, DragController.DRAG_ACTION_COPY); 184 mLauncher.closeFolder(this); 185 mDragItem = app; 186 187 return true; 188 } 189 190 public void setDragController(DragController dragController) { 191 mDragController = dragController; 192 } 193 194 public void onDragViewVisible() { 195 } 196 197 void setLauncher(Launcher launcher) { 198 mLauncher = launcher; 199 } 200 201 void setFolderIcon(FolderIcon icon) { 202 mFolderIcon = icon; 203 } 204 205 /** 206 * @return the FolderInfo object associated with this folder 207 */ 208 FolderInfo getInfo() { 209 return mInfo; 210 } 211 212 void onOpen() { 213 // When the folder opens, we need to refresh the GridView's selection by 214 // forcing a layout 215 // TODO: find out if this is still necessary 216 mContent.requestLayout(); 217 requestFocus(); 218 } 219 220 void onClose() { 221 final Workspace workspace = mLauncher.getWorkspace(); 222 workspace.getChildAt(workspace.getCurrentPage()).requestFocus(); 223 } 224 225 void bind(FolderInfo info) { 226 mInfo = info; 227 ArrayList<ShortcutInfo> children = info.contents; 228 setupContentForNumItems(children.size()); 229 for (int i = 0; i < children.size(); i++) { 230 ShortcutInfo child = (ShortcutInfo) children.get(i); 231 createAndAddShortcut(child); 232 } 233 mItemsInvalidated = true; 234 mInfo.addListener(this); 235 } 236 237 /** 238 * Creates a new UserFolder, inflated from R.layout.user_folder. 239 * 240 * @param context The application's context. 241 * 242 * @return A new UserFolder. 243 */ 244 static Folder fromXml(Context context) { 245 return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null); 246 } 247 248 /** 249 * This method is intended to make the UserFolder to be visually identical in size and position 250 * to its associated FolderIcon. This allows for a seamless transition into the expanded state. 251 */ 252 private void positionAndSizeAsIcon() { 253 if (!(getParent() instanceof CellLayoutChildren)) return; 254 255 CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams(); 256 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 257 258 if (mMode == PARTIAL_GROW) { 259 setScaleX(0.8f); 260 setScaleY(0.8f); 261 setAlpha(0f); 262 } else { 263 lp.width = iconLp.width; 264 lp.height = iconLp.height; 265 lp.x = iconLp.x; 266 lp.y = iconLp.y; 267 mContent.setAlpha(0); 268 } 269 mState = STATE_SMALL; 270 } 271 272 public void animateOpen() { 273 if (mState != STATE_SMALL) { 274 positionAndSizeAsIcon(); 275 } 276 if (!(getParent() instanceof CellLayoutChildren)) return; 277 278 ObjectAnimator oa; 279 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 280 281 centerAboutIcon(); 282 if (mMode == PARTIAL_GROW) { 283 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); 284 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); 285 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); 286 oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); 287 } else { 288 PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", mNewSize.width()); 289 PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", mNewSize.height()); 290 PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", mNewSize.left); 291 PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", mNewSize.top); 292 oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y); 293 oa.addUpdateListener(new AnimatorUpdateListener() { 294 public void onAnimationUpdate(ValueAnimator animation) { 295 requestLayout(); 296 } 297 }); 298 299 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f); 300 ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha); 301 alphaOa.setDuration(mExpandDuration); 302 alphaOa.setInterpolator(new AccelerateInterpolator(2.0f)); 303 alphaOa.start(); 304 } 305 306 oa.addListener(new AnimatorListenerAdapter() { 307 @Override 308 public void onAnimationStart(Animator animation) { 309 mState = STATE_ANIMATING; 310 } 311 @Override 312 public void onAnimationEnd(Animator animation) { 313 mState = STATE_OPEN; 314 } 315 }); 316 oa.setDuration(mExpandDuration); 317 oa.start(); 318 } 319 320 public void animateClosed() { 321 if (!(getParent() instanceof CellLayoutChildren)) return; 322 323 CellLayoutChildren clc = (CellLayoutChildren) getParent(); 324 final CellLayout cellLayout = (CellLayout) clc.getParent(); 325 ObjectAnimator oa; 326 327 if (mMode == PARTIAL_GROW) { 328 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); 329 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); 330 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); 331 oa = ObjectAnimator.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); 332 } else { 333 CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams(); 334 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 335 336 PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", iconLp.width); 337 PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", iconLp.height); 338 PropertyValuesHolder x = PropertyValuesHolder.ofInt("x",iconLp.x); 339 PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", iconLp.y); 340 oa = ObjectAnimator.ofPropertyValuesHolder(lp, width, height, x, y); 341 oa.addUpdateListener(new AnimatorUpdateListener() { 342 public void onAnimationUpdate(ValueAnimator animation) { 343 requestLayout(); 344 } 345 }); 346 347 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f); 348 ObjectAnimator alphaOa = ObjectAnimator.ofPropertyValuesHolder(mContent, alpha); 349 alphaOa.setDuration(mExpandDuration); 350 alphaOa.setInterpolator(new DecelerateInterpolator(2.0f)); 351 alphaOa.start(); 352 } 353 354 oa.addListener(new AnimatorListenerAdapter() { 355 @Override 356 public void onAnimationEnd(Animator animation) { 357 onCloseComplete(); 358 cellLayout.removeViewWithoutMarkingCells(Folder.this); 359 mState = STATE_SMALL; 360 } 361 @Override 362 public void onAnimationStart(Animator animation) { 363 mState = STATE_ANIMATING; 364 } 365 }); 366 oa.setDuration(mExpandDuration); 367 oa.start(); 368 } 369 370 void notifyDataSetChanged() { 371 // recreate all the children if the data set changes under us. We may want to do this more 372 // intelligently (ie just removing the views that should no longer exist) 373 mContent.removeAllViewsInLayout(); 374 bind(mInfo); 375 } 376 377 public boolean acceptDrop(DragObject d) { 378 final ItemInfo item = (ItemInfo) d.dragInfo; 379 final int itemType = item.itemType; 380 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 381 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && 382 !isFull()); 383 } 384 385 public void onDrop(DragObject d) { 386 ShortcutInfo item; 387 if (d.dragInfo instanceof ApplicationInfo) { 388 // Came from all apps -- make a copy 389 item = ((ApplicationInfo) d.dragInfo).makeShortcut(); 390 item.spanX = 1; 391 item.spanY = 1; 392 } else { 393 item = (ShortcutInfo) d.dragInfo; 394 } 395 396 // Dragged from self onto self 397 if (item == mCurrentDragInfo) { 398 mInfo.remove(item); 399 } 400 401 mInfo.add(item); 402 } 403 404 protected boolean findAndSetEmptyCells(ShortcutInfo item) { 405 int[] emptyCell = new int[2]; 406 if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) { 407 item.cellX = emptyCell[0]; 408 item.cellY = emptyCell[1]; 409 return true; 410 } else { 411 return false; 412 } 413 } 414 415 protected void createAndAddShortcut(ShortcutInfo item) { 416 final TextView textView = 417 (TextView) mInflater.inflate(R.layout.application_boxed, this, false); 418 textView.setCompoundDrawablesWithIntrinsicBounds(null, 419 new FastBitmapDrawable(item.getIcon(mIconCache)), null, null); 420 textView.setText(item.title); 421 textView.setTag(item); 422 423 textView.setOnClickListener(this); 424 textView.setOnLongClickListener(this); 425 426 CellLayout.LayoutParams lp = 427 new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); 428 boolean insert = false; 429 mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); 430 } 431 432 public void onDragEnter(DragObject d) { 433 mContent.onDragEnter(); 434 } 435 436 public void onDragOver(DragObject d) { 437 float[] r = mapPointFromScreenToContent(d.x, d.y, null); 438 mContent.visualizeDropLocation(null, null, (int) r[0], (int) r[1], 1, 1); 439 } 440 441 public void onDragExit(DragObject d) { 442 mContent.onDragExit(); 443 } 444 445 public float[] mapPointFromScreenToContent(int x, int y, float[] r) { 446 if (r == null) { 447 r = new float[2]; 448 } 449 450 int[] screenLocation = new int[2]; 451 mContent.getLocationOnScreen(screenLocation); 452 453 r[0] = x - screenLocation[0]; 454 r[1] = y - screenLocation[1]; 455 return r; 456 } 457 458 public void onDropCompleted(View target, DragObject d, boolean success) { 459 if (!success) { 460 if (d.dragView != null) { 461 if (target instanceof CellLayout) { 462 // TODO: we should animate the item back to the folder in this case 463 } 464 } 465 mCurrentDragInfo = null; 466 mItemsInvalidated = true; 467 mInfo.itemsChanged(); 468 } else { 469 if (target != this) { 470 mInfo.remove(mCurrentDragInfo); 471 mCurrentDragInfo = null; 472 } 473 } 474 } 475 476 public boolean isDropEnabled() { 477 return true; 478 } 479 480 public DropTarget getDropTargetDelegate(DragObject d) { 481 return null; 482 } 483 484 private void setupContentDimension(int count) { 485 ArrayList<View> list = getItemsInReadingOrder(); 486 487 int countX = mContent.getCountX(); 488 int countY = mContent.getCountY(); 489 boolean done = false; 490 491 while (!done) { 492 int oldCountX = countX; 493 int oldCountY = countY; 494 if (countX * countY < count) { 495 // Current grid is too small, expand it 496 if (countX <= countY && countX < mMaxCountX) { 497 countX++; 498 } else if (countY < mMaxCountY) { 499 countY++; 500 } 501 if (countY == 0) countY++; 502 } else if ((countY - 1) * countX >= count && countY >= countX) { 503 countY = Math.max(0, countY - 1); 504 } else if ((countX - 1) * countY >= count) { 505 countX = Math.max(0, countX - 1); 506 } 507 done = countX == oldCountX && countY == oldCountY; 508 } 509 mContent.setGridSize(countX, countY); 510 arrangeChildren(list); 511 } 512 513 public boolean isFull() { 514 return getItemCount() >= mMaxCountX * mMaxCountY; 515 } 516 517 private void centerAboutIcon() { 518 CellLayout.LayoutParams iconLp = (CellLayout.LayoutParams) mFolderIcon.getLayoutParams(); 519 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 520 521 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); 522 int height = getPaddingTop() + getPaddingBottom() + mContent.getDesiredHeight(); 523 524 int centerX = iconLp.x + iconLp.width / 2; 525 int centerY = iconLp.y + iconLp.height / 2; 526 int centeredLeft = centerX - width / 2; 527 int centeredTop = centerY - height / 2; 528 529 CellLayoutChildren clc = (CellLayoutChildren) getParent(); 530 int parentWidth = 0; 531 int parentHeight = 0; 532 if (clc != null) { 533 parentWidth = clc.getMeasuredWidth(); 534 parentHeight = clc.getMeasuredHeight(); 535 } 536 537 int left = Math.min(Math.max(0, centeredLeft), parentWidth - width); 538 int top = Math.min(Math.max(0, centeredTop), parentHeight - height); 539 540 int folderPivotX = width / 2 + (centeredLeft - left); 541 int folderPivotY = height / 2 + (centeredTop - top); 542 setPivotX(folderPivotX); 543 setPivotY(folderPivotY); 544 int folderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() * 545 (1.0f * folderPivotX / width)); 546 int folderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * 547 (1.0f * folderPivotY / height)); 548 mFolderIcon.setPivotX(folderIconPivotX); 549 mFolderIcon.setPivotY(folderIconPivotY); 550 551 if (mMode == PARTIAL_GROW) { 552 lp.width = width; 553 lp.height = height; 554 lp.x = left; 555 lp.y = top; 556 } else { 557 mNewSize.set(left, top, left + width, top + height); 558 } 559 } 560 561 private void setupContentForNumItems(int count) { 562 setupContentDimension(count); 563 564 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 565 if (lp == null) { 566 lp = new CellLayout.LayoutParams(0, 0, -1, -1); 567 lp.isLockedToGrid = false; 568 setLayoutParams(lp); 569 } 570 centerAboutIcon(); 571 } 572 573 private void arrangeChildren(ArrayList<View> list) { 574 int[] vacant = new int[2]; 575 if (list == null) { 576 list = getItemsInReadingOrder(); 577 } 578 mContent.removeAllViews(); 579 580 for (int i = 0; i < list.size(); i++) { 581 View v = list.get(i); 582 mContent.getVacantCell(vacant, 1, 1); 583 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); 584 lp.cellX = vacant[0]; 585 lp.cellY = vacant[1]; 586 ItemInfo info = (ItemInfo) v.getTag(); 587 info.cellX = vacant[0]; 588 info.cellY = vacant[1]; 589 boolean insert = false; 590 mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); 591 LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, 592 info.cellX, info.cellY); 593 } 594 mItemsInvalidated = true; 595 } 596 597 public void onAdd(ShortcutInfo item) { 598 mItemsInvalidated = true; 599 if (!findAndSetEmptyCells(item)) { 600 // The current layout is full, can we expand it? 601 setupContentForNumItems(getItemCount() + 1); 602 findAndSetEmptyCells(item); 603 } 604 createAndAddShortcut(item); 605 LauncherModel.addOrMoveItemInDatabase( 606 mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); 607 } 608 609 public int getItemCount() { 610 return mContent.getChildrenLayout().getChildCount(); 611 } 612 613 public View getItemAt(int index) { 614 return mContent.getChildrenLayout().getChildAt(index); 615 } 616 617 private void onCloseComplete() { 618 if (mRearrangeOnClose) { 619 setupContentForNumItems(getItemCount()); 620 mRearrangeOnClose = false; 621 } 622 } 623 624 public void onRemove(ShortcutInfo item) { 625 mItemsInvalidated = true; 626 View v = mContent.getChildAt(mDragItemPosition[0], mDragItemPosition[1]); 627 mContent.removeView(v); 628 if (mState == STATE_ANIMATING) { 629 mRearrangeOnClose = true; 630 } else { 631 setupContentForNumItems(getItemCount()); 632 } 633 } 634 635 public void onItemsChanged() { 636 } 637 638 public ArrayList<View> getItemsInReadingOrder() { 639 return getItemsInReadingOrder(true); 640 } 641 642 public ArrayList<View> getItemsInReadingOrder(boolean includeCurrentDragItem) { 643 if (mItemsInvalidated) { 644 mItemsInReadingOrder.clear(); 645 for (int j = 0; j < mContent.getCountY(); j++) { 646 for (int i = 0; i < mContent.getCountX(); i++) { 647 View v = mContent.getChildAt(i, j); 648 if (v != null) { 649 ShortcutInfo info = (ShortcutInfo) v.getTag(); 650 if (info != mCurrentDragInfo || includeCurrentDragItem) { 651 mItemsInReadingOrder.add(v); 652 } 653 } 654 } 655 } 656 mItemsInvalidated = false; 657 } 658 return mItemsInReadingOrder; 659 } 660} 661