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.ValueAnimator; 22 import android.animation.ValueAnimator.AnimatorUpdateListener; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.PorterDuff; 28 import android.graphics.Rect; 29 import android.graphics.drawable.Drawable; 30 import android.os.Parcelable; 31 import android.util.AttributeSet; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.animation.AccelerateInterpolator; 36 import android.view.animation.DecelerateInterpolator; 37 import android.widget.ImageView; 38 import android.widget.LinearLayout; 39 import android.widget.TextView; 40 41 import com.android.launcher.R; 42 import com.android.launcher2.DropTarget.DragObject; 43 import com.android.launcher2.FolderInfo.FolderListener; 44 45 import java.util.ArrayList; 46 47 /** 48 * An icon that can appear on in the workspace representing an {@link UserFolder}. 49 */ 50 public class FolderIcon extends LinearLayout implements FolderListener { 51 private Launcher mLauncher; 52 Folder mFolder; 53 FolderInfo mInfo; 54 private static boolean sStaticValuesDirty = true; 55 56 // The number of icons to display in the 57 private static final int NUM_ITEMS_IN_PREVIEW = 3; 58 private static final int CONSUMPTION_ANIMATION_DURATION = 100; 59 private static final int DROP_IN_ANIMATION_DURATION = 400; 60 private static final int INITIAL_ITEM_ANIMATION_DURATION = 350; 61 62 // The degree to which the inner ring grows when accepting drop 63 private static final float INNER_RING_GROWTH_FACTOR = 0.15f; 64 65 // The degree to which the outer ring is scaled in its natural state 66 private static final float OUTER_RING_GROWTH_FACTOR = 0.3f; 67 68 // The amount of vertical spread between items in the stack [0...1] 69 private static final float PERSPECTIVE_SHIFT_FACTOR = 0.24f; 70 71 // The degree to which the item in the back of the stack is scaled [0...1] 72 // (0 means it's not scaled at all, 1 means it's scaled to nothing) 73 private static final float PERSPECTIVE_SCALE_FACTOR = 0.35f; 74 75 public static Drawable sSharedFolderLeaveBehind = null; 76 77 private ImageView mPreviewBackground; 78 private BubbleTextView mFolderName; 79 80 FolderRingAnimator mFolderRingAnimator = null; 81 82 // These variables are all associated with the drawing of the preview; they are stored 83 // as member variables for shared usage and to avoid computation on each frame 84 private int mIntrinsicIconSize; 85 private float mBaselineIconScale; 86 private int mBaselineIconSize; 87 private int mAvailableSpaceInPreview; 88 private int mTotalWidth = -1; 89 private int mPreviewOffsetX; 90 private int mPreviewOffsetY; 91 private float mMaxPerspectiveShift; 92 boolean mAnimating = false; 93 private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0); 94 private PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0); 95 96 public FolderIcon(Context context, AttributeSet attrs) { 97 super(context, attrs); 98 } 99 100 public FolderIcon(Context context) { 101 super(context); 102 } 103 104 public boolean isDropEnabled() { 105 final ViewGroup cellLayoutChildren = (ViewGroup) getParent(); 106 final ViewGroup cellLayout = (ViewGroup) cellLayoutChildren.getParent(); 107 final Workspace workspace = (Workspace) cellLayout.getParent(); 108 return !workspace.isSmall(); 109 } 110 111 static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group, 112 FolderInfo folderInfo, IconCache iconCache) { 113 114 if (INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION) { 115 throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " + 116 "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + 117 "is dependent on this"); 118 } 119 120 FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false); 121 122 icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name); 123 icon.mFolderName.setText(folderInfo.title); 124 icon.mPreviewBackground = (ImageView) icon.findViewById(R.id.preview_background); 125 126 icon.setTag(folderInfo); 127 icon.setOnClickListener(launcher); 128 icon.mInfo = folderInfo; 129 icon.mLauncher = launcher; 130 icon.setContentDescription(String.format(launcher.getString(R.string.folder_name_format), 131 folderInfo.title)); 132 Folder folder = Folder.fromXml(launcher); 133 folder.setDragController(launcher.getDragController()); 134 folder.setFolderIcon(icon); 135 folder.bind(folderInfo); 136 icon.mFolder = folder; 137 138 icon.mFolderRingAnimator = new FolderRingAnimator(launcher, icon); 139 folderInfo.addListener(icon); 140 141 return icon; 142 } 143 144 @Override 145 protected Parcelable onSaveInstanceState() { 146 sStaticValuesDirty = true; 147 return super.onSaveInstanceState(); 148 } 149 150 public static class FolderRingAnimator { 151 public int mCellX; 152 public int mCellY; 153 private CellLayout mCellLayout; 154 public float mOuterRingSize; 155 public float mInnerRingSize; 156 public FolderIcon mFolderIcon = null; 157 public Drawable mOuterRingDrawable = null; 158 public Drawable mInnerRingDrawable = null; 159 public static Drawable sSharedOuterRingDrawable = null; 160 public static Drawable sSharedInnerRingDrawable = null; 161 public static int sPreviewSize = -1; 162 public static int sPreviewPadding = -1; 163 164 private ValueAnimator mAcceptAnimator; 165 private ValueAnimator mNeutralAnimator; 166 167 public FolderRingAnimator(Launcher launcher, FolderIcon folderIcon) { 168 mFolderIcon = folderIcon; 169 Resources res = launcher.getResources(); 170 mOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo); 171 mInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo); 172 173 // We need to reload the static values when configuration changes in case they are 174 // different in another configuration 175 if (sStaticValuesDirty) { 176 sPreviewSize = res.getDimensionPixelSize(R.dimen.folder_preview_size); 177 sPreviewPadding = res.getDimensionPixelSize(R.dimen.folder_preview_padding); 178 sSharedOuterRingDrawable = res.getDrawable(R.drawable.portal_ring_outer_holo); 179 sSharedInnerRingDrawable = res.getDrawable(R.drawable.portal_ring_inner_holo); 180 sSharedFolderLeaveBehind = res.getDrawable(R.drawable.portal_ring_rest); 181 sStaticValuesDirty = false; 182 } 183 } 184 185 public void animateToAcceptState() { 186 if (mNeutralAnimator != null) { 187 mNeutralAnimator.cancel(); 188 } 189 mAcceptAnimator = ValueAnimator.ofFloat(0f, 1f); 190 mAcceptAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); 191 mAcceptAnimator.addUpdateListener(new AnimatorUpdateListener() { 192 public void onAnimationUpdate(ValueAnimator animation) { 193 final float percent = (Float) animation.getAnimatedValue(); 194 mOuterRingSize = (1 + percent * OUTER_RING_GROWTH_FACTOR) * sPreviewSize; 195 mInnerRingSize = (1 + percent * INNER_RING_GROWTH_FACTOR) * sPreviewSize; 196 if (mCellLayout != null) { 197 mCellLayout.invalidate(); 198 } 199 } 200 }); 201 mAcceptAnimator.addListener(new AnimatorListenerAdapter() { 202 @Override 203 public void onAnimationStart(Animator animation) { 204 if (mFolderIcon != null) { 205 mFolderIcon.mPreviewBackground.setVisibility(INVISIBLE); 206 } 207 } 208 }); 209 mAcceptAnimator.start(); 210 } 211 212 public void animateToNaturalState() { 213 if (mAcceptAnimator != null) { 214 mAcceptAnimator.cancel(); 215 } 216 mNeutralAnimator = ValueAnimator.ofFloat(0f, 1f); 217 mNeutralAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); 218 mNeutralAnimator.addUpdateListener(new AnimatorUpdateListener() { 219 public void onAnimationUpdate(ValueAnimator animation) { 220 final float percent = (Float) animation.getAnimatedValue(); 221 mOuterRingSize = (1 + (1 - percent) * OUTER_RING_GROWTH_FACTOR) * sPreviewSize; 222 mInnerRingSize = (1 + (1 - percent) * INNER_RING_GROWTH_FACTOR) * sPreviewSize; 223 if (mCellLayout != null) { 224 mCellLayout.invalidate(); 225 } 226 } 227 }); 228 mNeutralAnimator.addListener(new AnimatorListenerAdapter() { 229 @Override 230 public void onAnimationEnd(Animator animation) { 231 if (mCellLayout != null) { 232 mCellLayout.hideFolderAccept(FolderRingAnimator.this); 233 } 234 if (mFolderIcon != null) { 235 mFolderIcon.mPreviewBackground.setVisibility(VISIBLE); 236 } 237 } 238 }); 239 mNeutralAnimator.start(); 240 } 241 242 // Location is expressed in window coordinates 243 public void getCell(int[] loc) { 244 loc[0] = mCellX; 245 loc[1] = mCellY; 246 } 247 248 // Location is expressed in window coordinates 249 public void setCell(int x, int y) { 250 mCellX = x; 251 mCellY = y; 252 } 253 254 public void setCellLayout(CellLayout layout) { 255 mCellLayout = layout; 256 } 257 258 public float getOuterRingSize() { 259 return mOuterRingSize; 260 } 261 262 public float getInnerRingSize() { 263 return mInnerRingSize; 264 } 265 } 266 267 private boolean willAcceptItem(ItemInfo item) { 268 final int itemType = item.itemType; 269 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 270 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && 271 !mFolder.isFull() && item != mInfo && !mInfo.opened); 272 } 273 274 public boolean acceptDrop(Object dragInfo) { 275 final ItemInfo item = (ItemInfo) dragInfo; 276 return willAcceptItem(item); 277 } 278 279 public void addItem(ShortcutInfo item) { 280 mInfo.add(item); 281 LauncherModel.addOrMoveItemInDatabase(mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); 282 } 283 284 public void onDragEnter(Object dragInfo) { 285 if (!willAcceptItem((ItemInfo) dragInfo)) return; 286 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 287 CellLayout layout = (CellLayout) getParent().getParent(); 288 mFolderRingAnimator.setCell(lp.cellX, lp.cellY); 289 mFolderRingAnimator.setCellLayout(layout); 290 mFolderRingAnimator.animateToAcceptState(); 291 layout.showFolderAccept(mFolderRingAnimator); 292 } 293 294 public void onDragOver(Object dragInfo) { 295 } 296 297 public void performCreateAnimation(final ShortcutInfo destInfo, final View destView, 298 final ShortcutInfo srcInfo, final View srcView, Rect dstRect, 299 float scaleRelativeToDragLayer, Runnable postAnimationRunnable) { 300 301 Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1]; 302 computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), destView.getMeasuredWidth()); 303 304 // This will animate the dragView (srcView) into the new folder 305 onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable); 306 307 // This will animate the first item from it's position as an icon into its 308 // position as the first item in the preview 309 animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION); 310 311 postDelayed(new Runnable() { 312 public void run() { 313 addItem(destInfo); 314 } 315 }, INITIAL_ITEM_ANIMATION_DURATION); 316 } 317 318 public void onDragExit(Object dragInfo) { 319 if (!willAcceptItem((ItemInfo) dragInfo)) return; 320 mFolderRingAnimator.animateToNaturalState(); 321 } 322 323 private void onDrop(final ShortcutInfo item, View animateView, Rect finalRect, 324 float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable) { 325 item.cellX = -1; 326 item.cellY = -1; 327 328 // Typically, the animateView corresponds to the DragView; however, if this is being done 329 // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we 330 // will not have a view to animate 331 if (animateView != null) { 332 DragLayer dragLayer = mLauncher.getDragLayer(); 333 Rect from = new Rect(); 334 dragLayer.getViewRectRelativeToSelf(animateView, from); 335 Rect to = finalRect; 336 if (to == null) { 337 to = new Rect(); 338 Workspace workspace = mLauncher.getWorkspace(); 339 // Set cellLayout and this to it's final state to compute final animation locations 340 workspace.setFinalTransitionTransform((CellLayout) getParent().getParent()); 341 float scaleX = getScaleX(); 342 float scaleY = getScaleY(); 343 setScaleX(1.0f); 344 setScaleY(1.0f); 345 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to); 346 // Finished computing final animation locations, restore current state 347 setScaleX(scaleX); 348 setScaleY(scaleY); 349 workspace.resetTransitionTransform((CellLayout) getParent().getParent()); 350 } 351 352 int[] center = new int[2]; 353 float scale = getLocalCenterForIndex(index, center); 354 center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]); 355 center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]); 356 357 to.offset(center[0] - animateView.getMeasuredWidth() / 2, 358 center[1] - animateView.getMeasuredHeight() / 2); 359 360 float finalAlpha = index < NUM_ITEMS_IN_PREVIEW ? 0.5f : 0f; 361 362 dragLayer.animateView(animateView, from, to, finalAlpha, 363 scale * scaleRelativeToDragLayer, DROP_IN_ANIMATION_DURATION, 364 new DecelerateInterpolator(2), new AccelerateInterpolator(2), 365 postAnimationRunnable, false); 366 postDelayed(new Runnable() { 367 public void run() { 368 addItem(item); 369 } 370 }, DROP_IN_ANIMATION_DURATION); 371 } else { 372 addItem(item); 373 } 374 } 375 376 public void onDrop(DragObject d) { 377 ShortcutInfo item; 378 if (d.dragInfo instanceof ApplicationInfo) { 379 // Came from all apps -- make a copy 380 item = ((ApplicationInfo) d.dragInfo).makeShortcut(); 381 } else { 382 item = (ShortcutInfo) d.dragInfo; 383 } 384 mFolder.notifyDrop(); 385 onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable); 386 } 387 388 public DropTarget getDropTargetDelegate(DragObject d) { 389 return null; 390 } 391 392 private void computePreviewDrawingParams(int drawableSize, int totalSize) { 393 if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize) { 394 mIntrinsicIconSize = drawableSize; 395 mTotalWidth = totalSize; 396 397 final int previewSize = FolderRingAnimator.sPreviewSize; 398 final int previewPadding = FolderRingAnimator.sPreviewPadding; 399 400 mAvailableSpaceInPreview = (previewSize - 2 * previewPadding); 401 // cos(45) = 0.707 + ~= 0.1) = 0.8f 402 int adjustedAvailableSpace = (int) ((mAvailableSpaceInPreview / 2) * (1 + 0.8f)); 403 404 int unscaledHeight = (int) (mIntrinsicIconSize * (1 + PERSPECTIVE_SHIFT_FACTOR)); 405 mBaselineIconScale = (1.0f * adjustedAvailableSpace / unscaledHeight); 406 407 mBaselineIconSize = (int) (mIntrinsicIconSize * mBaselineIconScale); 408 mMaxPerspectiveShift = mBaselineIconSize * PERSPECTIVE_SHIFT_FACTOR; 409 410 mPreviewOffsetX = (mTotalWidth - mAvailableSpaceInPreview) / 2; 411 mPreviewOffsetY = previewPadding; 412 } 413 } 414 415 private void computePreviewDrawingParams(Drawable d) { 416 computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth()); 417 } 418 419 class PreviewItemDrawingParams { 420 PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) { 421 this.transX = transX; 422 this.transY = transY; 423 this.scale = scale; 424 this.overlayAlpha = overlayAlpha; 425 } 426 float transX; 427 float transY; 428 float scale; 429 int overlayAlpha; 430 Drawable drawable; 431 } 432 433 private float getLocalCenterForIndex(int index, int[] center) { 434 mParams = computePreviewItemDrawingParams(Math.min(NUM_ITEMS_IN_PREVIEW, index), mParams); 435 436 mParams.transX += mPreviewOffsetX; 437 mParams.transY += mPreviewOffsetY; 438 float offsetX = mParams.transX + (mParams.scale * mIntrinsicIconSize) / 2; 439 float offsetY = mParams.transY + (mParams.scale * mIntrinsicIconSize) / 2; 440 441 center[0] = (int) Math.round(offsetX); 442 center[1] = (int) Math.round(offsetY); 443 return mParams.scale; 444 } 445 446 private PreviewItemDrawingParams computePreviewItemDrawingParams(int index, 447 PreviewItemDrawingParams params) { 448 index = NUM_ITEMS_IN_PREVIEW - index - 1; 449 float r = (index * 1.0f) / (NUM_ITEMS_IN_PREVIEW - 1); 450 float scale = (1 - PERSPECTIVE_SCALE_FACTOR * (1 - r)); 451 452 float offset = (1 - r) * mMaxPerspectiveShift; 453 float scaledSize = scale * mBaselineIconSize; 454 float scaleOffsetCorrection = (1 - scale) * mBaselineIconSize; 455 456 // We want to imagine our coordinates from the bottom left, growing up and to the 457 // right. This is natural for the x-axis, but for the y-axis, we have to invert things. 458 float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection); 459 float transX = offset + scaleOffsetCorrection; 460 float totalScale = mBaselineIconScale * scale; 461 final int overlayAlpha = (int) (80 * (1 - r)); 462 463 if (params == null) { 464 params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha); 465 } else { 466 params.transX = transX; 467 params.transY = transY; 468 params.scale = totalScale; 469 params.overlayAlpha = overlayAlpha; 470 } 471 return params; 472 } 473 474 private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) { 475 canvas.save(); 476 canvas.translate(params.transX + mPreviewOffsetX, params.transY + mPreviewOffsetY); 477 canvas.scale(params.scale, params.scale); 478 Drawable d = params.drawable; 479 480 if (d != null) { 481 d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize); 482 d.setFilterBitmap(true); 483 d.setColorFilter(Color.argb(params.overlayAlpha, 0, 0, 0), PorterDuff.Mode.SRC_ATOP); 484 d.draw(canvas); 485 d.clearColorFilter(); 486 d.setFilterBitmap(false); 487 } 488 canvas.restore(); 489 } 490 491 @Override 492 protected void dispatchDraw(Canvas canvas) { 493 super.dispatchDraw(canvas); 494 495 if (mFolder == null) return; 496 if (mFolder.getItemCount() == 0 && !mAnimating) return; 497 498 ArrayList<View> items = mFolder.getItemsInReadingOrder(false); 499 Drawable d; 500 TextView v; 501 502 // Update our drawing parameters if necessary 503 if (mAnimating) { 504 computePreviewDrawingParams(mAnimParams.drawable); 505 } else { 506 v = (TextView) items.get(0); 507 d = v.getCompoundDrawables()[1]; 508 computePreviewDrawingParams(d); 509 } 510 511 int nItemsInPreview = Math.min(items.size(), NUM_ITEMS_IN_PREVIEW); 512 if (!mAnimating) { 513 for (int i = nItemsInPreview - 1; i >= 0; i--) { 514 v = (TextView) items.get(i); 515 d = v.getCompoundDrawables()[1]; 516 517 mParams = computePreviewItemDrawingParams(i, mParams); 518 mParams.drawable = d; 519 drawPreviewItem(canvas, mParams); 520 } 521 } else { 522 drawPreviewItem(canvas, mAnimParams); 523 } 524 } 525 526 private void animateFirstItem(final Drawable d, int duration) { 527 computePreviewDrawingParams(d); 528 final PreviewItemDrawingParams finalParams = computePreviewItemDrawingParams(0, null); 529 530 final float scale0 = 1.0f; 531 final float transX0 = (mAvailableSpaceInPreview - d.getIntrinsicWidth()) / 2; 532 final float transY0 = (mAvailableSpaceInPreview - d.getIntrinsicHeight()) / 2; 533 mAnimParams.drawable = d; 534 535 ValueAnimator va = ValueAnimator.ofFloat(0f, 1.0f); 536 va.addUpdateListener(new AnimatorUpdateListener(){ 537 public void onAnimationUpdate(ValueAnimator animation) { 538 float progress = (Float) animation.getAnimatedValue(); 539 540 mAnimParams.transX = transX0 + progress * (finalParams.transX - transX0); 541 mAnimParams.transY = transY0 + progress * (finalParams.transY - transY0); 542 mAnimParams.scale = scale0 + progress * (finalParams.scale - scale0); 543 invalidate(); 544 } 545 }); 546 va.addListener(new AnimatorListenerAdapter() { 547 @Override 548 public void onAnimationStart(Animator animation) { 549 mAnimating = true; 550 } 551 @Override 552 public void onAnimationEnd(Animator animation) { 553 mAnimating = false; 554 } 555 }); 556 va.setDuration(duration); 557 va.start(); 558 } 559 560 public void setTextVisible(boolean visible) { 561 if (visible) { 562 mFolderName.setVisibility(VISIBLE); 563 } else { 564 mFolderName.setVisibility(INVISIBLE); 565 } 566 } 567 568 public boolean getTextVisible() { 569 return mFolderName.getVisibility() == VISIBLE; 570 } 571 572 public void onItemsChanged() { 573 invalidate(); 574 requestLayout(); 575 } 576 577 public void onAdd(ShortcutInfo item) { 578 invalidate(); 579 requestLayout(); 580 } 581 582 public void onRemove(ShortcutInfo item) { 583 invalidate(); 584 requestLayout(); 585 } 586 587 public void onTitleChanged(CharSequence title) { 588 mFolderName.setText(title.toString()); 589 setContentDescription(String.format(mContext.getString(R.string.folder_name_format), 590 title)); 591 } 592 } 593