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.launcher3.folder; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.animation.ValueAnimator.AnimatorUpdateListener; 24 import android.content.Context; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.Matrix; 28 import android.graphics.Paint; 29 import android.graphics.Path; 30 import android.graphics.Point; 31 import android.graphics.PorterDuff; 32 import android.graphics.PorterDuffXfermode; 33 import android.graphics.RadialGradient; 34 import android.graphics.Rect; 35 import android.graphics.Region; 36 import android.graphics.Shader; 37 import android.graphics.drawable.Drawable; 38 import android.os.Parcelable; 39 import android.util.AttributeSet; 40 import android.util.DisplayMetrics; 41 import android.util.Property; 42 import android.view.LayoutInflater; 43 import android.view.MotionEvent; 44 import android.view.View; 45 import android.view.ViewConfiguration; 46 import android.view.ViewGroup; 47 import android.view.animation.AccelerateInterpolator; 48 import android.view.animation.DecelerateInterpolator; 49 import android.widget.FrameLayout; 50 import android.widget.TextView; 51 52 import com.android.launcher3.Alarm; 53 import com.android.launcher3.AppInfo; 54 import com.android.launcher3.BubbleTextView; 55 import com.android.launcher3.CellLayout; 56 import com.android.launcher3.CheckLongPressHelper; 57 import com.android.launcher3.DeviceProfile; 58 import com.android.launcher3.DropTarget.DragObject; 59 import com.android.launcher3.FastBitmapDrawable; 60 import com.android.launcher3.FolderInfo; 61 import com.android.launcher3.FolderInfo.FolderListener; 62 import com.android.launcher3.ItemInfo; 63 import com.android.launcher3.Launcher; 64 import com.android.launcher3.LauncherAnimUtils; 65 import com.android.launcher3.LauncherSettings; 66 import com.android.launcher3.OnAlarmListener; 67 import com.android.launcher3.R; 68 import com.android.launcher3.ShortcutInfo; 69 import com.android.launcher3.SimpleOnStylusPressListener; 70 import com.android.launcher3.StylusEventHelper; 71 import com.android.launcher3.Utilities; 72 import com.android.launcher3.Workspace; 73 import com.android.launcher3.badge.BadgeRenderer; 74 import com.android.launcher3.badge.FolderBadgeInfo; 75 import com.android.launcher3.config.FeatureFlags; 76 import com.android.launcher3.dragndrop.DragLayer; 77 import com.android.launcher3.dragndrop.DragView; 78 import com.android.launcher3.graphics.IconPalette; 79 import com.android.launcher3.util.Thunk; 80 81 import java.util.ArrayList; 82 import java.util.List; 83 84 /** 85 * An icon that can appear on in the workspace representing an {@link Folder}. 86 */ 87 public class FolderIcon extends FrameLayout implements FolderListener { 88 @Thunk Launcher mLauncher; 89 @Thunk Folder mFolder; 90 private FolderInfo mInfo; 91 @Thunk static boolean sStaticValuesDirty = true; 92 93 public static final int NUM_ITEMS_IN_PREVIEW = FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON ? 94 StackFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW : 95 ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; 96 97 private CheckLongPressHelper mLongPressHelper; 98 private StylusEventHelper mStylusEventHelper; 99 100 // The number of icons to display in the 101 private static final int CONSUMPTION_ANIMATION_DURATION = 100; 102 private static final int DROP_IN_ANIMATION_DURATION = 400; 103 private static final int INITIAL_ITEM_ANIMATION_DURATION = 350; 104 private static final int FINAL_ITEM_ANIMATION_DURATION = 200; 105 106 // Flag whether the folder should open itself when an item is dragged over is enabled. 107 public static final boolean SPRING_LOADING_ENABLED = true; 108 109 // Delay when drag enters until the folder opens, in miliseconds. 110 private static final int ON_OPEN_DELAY = 800; 111 112 @Thunk BubbleTextView mFolderName; 113 114 // These variables are all associated with the drawing of the preview; they are stored 115 // as member variables for shared usage and to avoid computation on each frame 116 private int mIntrinsicIconSize = -1; 117 private int mTotalWidth = -1; 118 private int mPrevTopPadding = -1; 119 120 PreviewBackground mBackground = new PreviewBackground(); 121 122 private PreviewLayoutRule mPreviewLayoutRule; 123 124 boolean mAnimating = false; 125 private Rect mTempBounds = new Rect(); 126 127 private float mSlop; 128 129 private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0); 130 private ArrayList<PreviewItemDrawingParams> mDrawingParams = new ArrayList<PreviewItemDrawingParams>(); 131 private Drawable mReferenceDrawable = null; 132 133 private Alarm mOpenAlarm = new Alarm(); 134 135 private FolderBadgeInfo mBadgeInfo = new FolderBadgeInfo(); 136 private BadgeRenderer mBadgeRenderer; 137 private float mBadgeScale; 138 private Point mTempSpaceForBadgeOffset = new Point(); 139 140 private static final Property<FolderIcon, Float> BADGE_SCALE_PROPERTY 141 = new Property<FolderIcon, Float>(Float.TYPE, "badgeScale") { 142 @Override 143 public Float get(FolderIcon folderIcon) { 144 return folderIcon.mBadgeScale; 145 } 146 147 @Override 148 public void set(FolderIcon folderIcon, Float value) { 149 folderIcon.mBadgeScale = value; 150 folderIcon.invalidate(); 151 } 152 }; 153 FolderIcon(Context context, AttributeSet attrs)154 public FolderIcon(Context context, AttributeSet attrs) { 155 super(context, attrs); 156 init(); 157 } 158 FolderIcon(Context context)159 public FolderIcon(Context context) { 160 super(context); 161 init(); 162 } 163 init()164 private void init() { 165 mLongPressHelper = new CheckLongPressHelper(this); 166 mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this); 167 mPreviewLayoutRule = FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON ? 168 new StackFolderIconLayoutRule() : 169 new ClippedFolderIconLayoutRule(); 170 mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 171 } 172 fromXml(int resId, Launcher launcher, ViewGroup group, FolderInfo folderInfo)173 public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group, 174 FolderInfo folderInfo) { 175 @SuppressWarnings("all") // suppress dead code warning 176 final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION; 177 if (error) { 178 throw new IllegalStateException("DROP_IN_ANIMATION_DURATION must be greater than " + 179 "INITIAL_ITEM_ANIMATION_DURATION, as sequencing of adding first two items " + 180 "is dependent on this"); 181 } 182 183 DeviceProfile grid = launcher.getDeviceProfile(); 184 FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false); 185 186 icon.setClipToPadding(false); 187 icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name); 188 icon.mFolderName.setText(folderInfo.title); 189 icon.mFolderName.setCompoundDrawablePadding(0); 190 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams(); 191 lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx; 192 193 icon.setTag(folderInfo); 194 icon.setOnClickListener(launcher); 195 icon.mInfo = folderInfo; 196 icon.mLauncher = launcher; 197 icon.mBadgeRenderer = launcher.getDeviceProfile().mBadgeRenderer; 198 icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title)); 199 Folder folder = Folder.fromXml(launcher); 200 folder.setDragController(launcher.getDragController()); 201 folder.setFolderIcon(icon); 202 folder.bind(folderInfo); 203 icon.setFolder(folder); 204 icon.setAccessibilityDelegate(launcher.getAccessibilityDelegate()); 205 206 folderInfo.addListener(icon); 207 208 icon.setOnFocusChangeListener(launcher.mFocusHandler); 209 return icon; 210 } 211 212 @Override onSaveInstanceState()213 protected Parcelable onSaveInstanceState() { 214 sStaticValuesDirty = true; 215 return super.onSaveInstanceState(); 216 } 217 getFolder()218 public Folder getFolder() { 219 return mFolder; 220 } 221 setFolder(Folder folder)222 private void setFolder(Folder folder) { 223 mFolder = folder; 224 updateItemDrawingParams(false); 225 } 226 willAcceptItem(ItemInfo item)227 private boolean willAcceptItem(ItemInfo item) { 228 final int itemType = item.itemType; 229 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 230 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT || 231 itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) && 232 !mFolder.isFull() && item != mInfo && !mFolder.isOpen()); 233 } 234 acceptDrop(ItemInfo dragInfo)235 public boolean acceptDrop(ItemInfo dragInfo) { 236 final ItemInfo item = dragInfo; 237 return !mFolder.isDestroyed() && willAcceptItem(item); 238 } 239 addItem(ShortcutInfo item)240 public void addItem(ShortcutInfo item) { 241 mInfo.add(item, true); 242 } 243 onDragEnter(ItemInfo dragInfo)244 public void onDragEnter(ItemInfo dragInfo) { 245 if (mFolder.isDestroyed() || !willAcceptItem(dragInfo)) return; 246 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 247 CellLayout cl = (CellLayout) getParent().getParent(); 248 249 mBackground.animateToAccept(cl, lp.cellX, lp.cellY); 250 mOpenAlarm.setOnAlarmListener(mOnOpenListener); 251 if (SPRING_LOADING_ENABLED && 252 ((dragInfo instanceof AppInfo) || (dragInfo instanceof ShortcutInfo))) { 253 // TODO: we currently don't support spring-loading for PendingAddShortcutInfos even 254 // though widget-style shortcuts can be added to folders. The issue is that we need 255 // to deal with configuration activities which are currently handled in 256 // Workspace#onDropExternal. 257 mOpenAlarm.setAlarm(ON_OPEN_DELAY); 258 } 259 } 260 261 OnAlarmListener mOnOpenListener = new OnAlarmListener() { 262 public void onAlarm(Alarm alarm) { 263 mFolder.beginExternalDrag(); 264 mFolder.animateOpen(); 265 } 266 }; 267 prepareCreate(final View destView)268 public Drawable prepareCreate(final View destView) { 269 Drawable animateDrawable = ((TextView) destView).getCompoundDrawables()[1]; 270 computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), 271 destView.getMeasuredWidth()); 272 return animateDrawable; 273 } 274 performCreateAnimation(final ShortcutInfo destInfo, final View destView, final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect, float scaleRelativeToDragLayer, Runnable postAnimationRunnable)275 public void performCreateAnimation(final ShortcutInfo destInfo, final View destView, 276 final ShortcutInfo srcInfo, final DragView srcView, Rect dstRect, 277 float scaleRelativeToDragLayer, Runnable postAnimationRunnable) { 278 279 // These correspond two the drawable and view that the icon was dropped _onto_ 280 Drawable animateDrawable = prepareCreate(destView); 281 282 mReferenceDrawable = animateDrawable; 283 284 addItem(destInfo); 285 // This will animate the first item from it's position as an icon into its 286 // position as the first item in the preview 287 animateFirstItem(animateDrawable, INITIAL_ITEM_ANIMATION_DURATION, false, null); 288 289 // This will animate the dragView (srcView) into the new folder 290 onDrop(srcInfo, srcView, dstRect, scaleRelativeToDragLayer, 1, postAnimationRunnable); 291 } 292 performDestroyAnimation(final View finalView, Runnable onCompleteRunnable)293 public void performDestroyAnimation(final View finalView, Runnable onCompleteRunnable) { 294 Drawable animateDrawable = ((TextView) finalView).getCompoundDrawables()[1]; 295 computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), 296 finalView.getMeasuredWidth()); 297 298 // This will animate the first item from it's position as an icon into its 299 // position as the first item in the preview 300 animateFirstItem(animateDrawable, FINAL_ITEM_ANIMATION_DURATION, true, 301 onCompleteRunnable); 302 } 303 onDragExit()304 public void onDragExit() { 305 mBackground.animateToRest(); 306 mOpenAlarm.cancelAlarm(); 307 } 308 onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect, float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable)309 private void onDrop(final ShortcutInfo item, DragView animateView, Rect finalRect, 310 float scaleRelativeToDragLayer, int index, Runnable postAnimationRunnable) { 311 item.cellX = -1; 312 item.cellY = -1; 313 314 // Typically, the animateView corresponds to the DragView; however, if this is being done 315 // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we 316 // will not have a view to animate 317 if (animateView != null) { 318 DragLayer dragLayer = mLauncher.getDragLayer(); 319 Rect from = new Rect(); 320 dragLayer.getViewRectRelativeToSelf(animateView, from); 321 Rect to = finalRect; 322 if (to == null) { 323 to = new Rect(); 324 Workspace workspace = mLauncher.getWorkspace(); 325 // Set cellLayout and this to it's final state to compute final animation locations 326 workspace.setFinalTransitionTransform((CellLayout) getParent().getParent()); 327 float scaleX = getScaleX(); 328 float scaleY = getScaleY(); 329 setScaleX(1.0f); 330 setScaleY(1.0f); 331 scaleRelativeToDragLayer = dragLayer.getDescendantRectRelativeToSelf(this, to); 332 // Finished computing final animation locations, restore current state 333 setScaleX(scaleX); 334 setScaleY(scaleY); 335 workspace.resetTransitionTransform((CellLayout) getParent().getParent()); 336 } 337 338 int[] center = new int[2]; 339 float scale = getLocalCenterForIndex(index, index + 1, center); 340 center[0] = (int) Math.round(scaleRelativeToDragLayer * center[0]); 341 center[1] = (int) Math.round(scaleRelativeToDragLayer * center[1]); 342 343 to.offset(center[0] - animateView.getMeasuredWidth() / 2, 344 center[1] - animateView.getMeasuredHeight() / 2); 345 346 float finalAlpha = index < mPreviewLayoutRule.maxNumItems() ? 0.5f : 0f; 347 348 float finalScale = scale * scaleRelativeToDragLayer; 349 dragLayer.animateView(animateView, from, to, finalAlpha, 350 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION, 351 new DecelerateInterpolator(2), new AccelerateInterpolator(2), 352 postAnimationRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null); 353 addItem(item); 354 mFolder.hideItem(item); 355 356 final PreviewItemDrawingParams params = index < mDrawingParams.size() ? 357 mDrawingParams.get(index) : null; 358 if (params != null) params.hidden = true; 359 postDelayed(new Runnable() { 360 public void run() { 361 if (params != null) params.hidden = false; 362 mFolder.showItem(item); 363 invalidate(); 364 } 365 }, DROP_IN_ANIMATION_DURATION); 366 } else { 367 addItem(item); 368 } 369 } 370 371 public void onDrop(DragObject d) { 372 ShortcutInfo item; 373 if (d.dragInfo instanceof AppInfo) { 374 // Came from all apps -- make a copy 375 item = ((AppInfo) d.dragInfo).makeShortcut(); 376 } else { 377 item = (ShortcutInfo) d.dragInfo; 378 } 379 mFolder.notifyDrop(); 380 onDrop(item, d.dragView, null, 1.0f, mInfo.contents.size(), d.postAnimationRunnable); 381 } 382 383 private void computePreviewDrawingParams(int drawableSize, int totalSize) { 384 if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize || 385 mPrevTopPadding != getPaddingTop()) { 386 DeviceProfile grid = mLauncher.getDeviceProfile(); 387 388 mIntrinsicIconSize = drawableSize; 389 mTotalWidth = totalSize; 390 mPrevTopPadding = getPaddingTop(); 391 392 mBackground.setup(getResources().getDisplayMetrics(), grid, this, mTotalWidth, 393 getPaddingTop()); 394 mPreviewLayoutRule.init(mBackground.previewSize, mIntrinsicIconSize, 395 Utilities.isRtl(getResources())); 396 397 updateItemDrawingParams(false); 398 } 399 } 400 401 private void computePreviewDrawingParams(Drawable d) { 402 computePreviewDrawingParams(d.getIntrinsicWidth(), getMeasuredWidth()); 403 } 404 405 public void setBadgeInfo(FolderBadgeInfo badgeInfo) { 406 updateBadgeScale(mBadgeInfo.hasBadge(), badgeInfo.hasBadge()); 407 mBadgeInfo = badgeInfo; 408 } 409 410 /** 411 * Sets mBadgeScale to 1 or 0, animating if wasBadged or isBadged is false 412 * (the badge is being added or removed). 413 */ 414 private void updateBadgeScale(boolean wasBadged, boolean isBadged) { 415 float newBadgeScale = isBadged ? 1f : 0f; 416 // Animate when a badge is first added or when it is removed. 417 if ((wasBadged ^ isBadged) && isShown()) { 418 ObjectAnimator.ofFloat(this, BADGE_SCALE_PROPERTY, newBadgeScale).start(); 419 } else { 420 mBadgeScale = newBadgeScale; 421 invalidate(); 422 } 423 } 424 425 static class PreviewItemDrawingParams { 426 PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) { 427 this.transX = transX; 428 this.transY = transY; 429 this.scale = scale; 430 this.overlayAlpha = overlayAlpha; 431 } 432 433 public void update(float transX, float transY, float scale) { 434 // We ensure the update will not interfere with an animation on the layout params 435 // If the final values differ, we cancel the animation. 436 if (anim != null) { 437 if (anim.finalTransX == transX || anim.finalTransY == transY 438 || anim.finalScale == scale) { 439 return; 440 } 441 anim.cancel(); 442 } 443 444 this.transX = transX; 445 this.transY = transY; 446 this.scale = scale; 447 } 448 449 float transX; 450 float transY; 451 float scale; 452 public float overlayAlpha; 453 boolean hidden; 454 FolderPreviewItemAnim anim; 455 Drawable drawable; 456 } 457 458 private float getLocalCenterForIndex(int index, int curNumItems, int[] center) { 459 mTmpParams = computePreviewItemDrawingParams( 460 Math.min(mPreviewLayoutRule.maxNumItems(), index), curNumItems, mTmpParams); 461 462 mTmpParams.transX += mBackground.basePreviewOffsetX; 463 mTmpParams.transY += mBackground.basePreviewOffsetY; 464 float offsetX = mTmpParams.transX + (mTmpParams.scale * mIntrinsicIconSize) / 2; 465 float offsetY = mTmpParams.transY + (mTmpParams.scale * mIntrinsicIconSize) / 2; 466 467 center[0] = (int) Math.round(offsetX); 468 center[1] = (int) Math.round(offsetY); 469 return mTmpParams.scale; 470 } 471 472 private PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems, 473 PreviewItemDrawingParams params) { 474 // We use an index of -1 to represent an icon on the workspace for the destroy and 475 // create animations 476 if (index == -1) { 477 return getFinalIconParams(params); 478 } 479 return mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params); 480 } 481 482 private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) { 483 float iconSize = mLauncher.getDeviceProfile().iconSizePx; 484 485 final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth(); 486 final float trans = (mBackground.previewSize - iconSize) / 2; 487 488 params.update(trans, trans, scale); 489 return params; 490 } 491 492 private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params) { 493 canvas.save(Canvas.MATRIX_SAVE_FLAG); 494 canvas.translate(params.transX, params.transY); 495 canvas.scale(params.scale, params.scale); 496 Drawable d = params.drawable; 497 498 if (d != null) { 499 mTempBounds.set(d.getBounds()); 500 d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize); 501 if (d instanceof FastBitmapDrawable) { 502 FastBitmapDrawable fd = (FastBitmapDrawable) d; 503 fd.drawWithBrightness(canvas, params.overlayAlpha); 504 } else { 505 d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255), 506 PorterDuff.Mode.SRC_ATOP); 507 d.draw(canvas); 508 d.clearColorFilter(); 509 } 510 d.setBounds(mTempBounds); 511 } 512 canvas.restore(); 513 } 514 515 /** 516 * This object represents a FolderIcon preview background. It stores drawing / measurement 517 * information, handles drawing, and animation (accept state <--> rest state). 518 */ 519 public static class PreviewBackground { 520 521 private final PorterDuffXfermode mClipPorterDuffXfermode 522 = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); 523 // Create a RadialGradient such that it draws a black circle and then extends with 524 // transparent. To achieve this, we keep the gradient to black for the range [0, 1) and 525 // just at the edge quickly change it to transparent. 526 private final RadialGradient mClipShader = new RadialGradient(0, 0, 1, 527 new int[] {Color.BLACK, Color.BLACK, Color.TRANSPARENT }, 528 new float[] {0, 0.999f, 1}, 529 Shader.TileMode.CLAMP); 530 531 private final PorterDuffXfermode mShadowPorterDuffXfermode 532 = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); 533 private RadialGradient mShadowShader = null; 534 535 private final Matrix mShaderMatrix = new Matrix(); 536 private final Path mPath = new Path(); 537 538 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 539 540 private float mScale = 1f; 541 private float mColorMultiplier = 1f; 542 private float mStrokeWidth; 543 private View mInvalidateDelegate; 544 545 public int previewSize; 546 private int basePreviewOffsetX; 547 private int basePreviewOffsetY; 548 549 private CellLayout mDrawingDelegate; 550 public int delegateCellX; 551 public int delegateCellY; 552 553 // When the PreviewBackground is drawn under an icon (for creating a folder) the border 554 // should not occlude the icon 555 public boolean isClipping = true; 556 557 // Drawing / animation configurations 558 private static final float ACCEPT_SCALE_FACTOR = 1.25f; 559 private static final float ACCEPT_COLOR_MULTIPLIER = 1.5f; 560 561 // Expressed on a scale from 0 to 255. 562 private static final int BG_OPACITY = 160; 563 private static final int MAX_BG_OPACITY = 225; 564 private static final int BG_INTENSITY = 245; 565 private static final int SHADOW_OPACITY = 40; 566 567 ValueAnimator mScaleAnimator; 568 569 public void setup(DisplayMetrics dm, DeviceProfile grid, View invalidateDelegate, 570 int availableSpace, int topPadding) { 571 mInvalidateDelegate = invalidateDelegate; 572 573 final int previewSize = grid.folderIconSizePx; 574 final int previewPadding = grid.folderIconPreviewPadding; 575 576 this.previewSize = (previewSize - 2 * previewPadding); 577 578 basePreviewOffsetX = (availableSpace - this.previewSize) / 2; 579 basePreviewOffsetY = previewPadding + grid.folderBackgroundOffset + topPadding; 580 581 // Stroke width is 1dp 582 mStrokeWidth = dm.density; 583 584 float radius = getScaledRadius(); 585 float shadowRadius = radius + mStrokeWidth; 586 int shadowColor = Color.argb(SHADOW_OPACITY, 0, 0, 0); 587 mShadowShader = new RadialGradient(0, 0, 1, 588 new int[] {shadowColor, Color.TRANSPARENT}, 589 new float[] {radius / shadowRadius, 1}, 590 Shader.TileMode.CLAMP); 591 592 invalidate(); 593 } 594 595 int getRadius() { 596 return previewSize / 2; 597 } 598 599 int getScaledRadius() { 600 return (int) (mScale * getRadius()); 601 } 602 603 int getOffsetX() { 604 return basePreviewOffsetX - (getScaledRadius() - getRadius()); 605 } 606 607 int getOffsetY() { 608 return basePreviewOffsetY - (getScaledRadius() - getRadius()); 609 } 610 611 /** 612 * Returns the progress of the scale animation, where 0 means the scale is at 1f 613 * and 1 means the scale is at ACCEPT_SCALE_FACTOR. 614 */ 615 float getScaleProgress() { 616 return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f); 617 } 618 619 void invalidate() { 620 if (mInvalidateDelegate != null) { 621 mInvalidateDelegate.invalidate(); 622 } 623 624 if (mDrawingDelegate != null) { 625 mDrawingDelegate.invalidate(); 626 } 627 } 628 629 void setInvalidateDelegate(View invalidateDelegate) { 630 mInvalidateDelegate = invalidateDelegate; 631 invalidate(); 632 } 633 634 public void drawBackground(Canvas canvas) { 635 mPaint.setStyle(Paint.Style.FILL); 636 int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier); 637 mPaint.setColor(Color.argb(alpha, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY)); 638 639 drawCircle(canvas, 0 /* deltaRadius */); 640 641 // Draw shadow. 642 if (mShadowShader == null) { 643 return; 644 } 645 float radius = getScaledRadius(); 646 float shadowRadius = radius + mStrokeWidth; 647 mPaint.setColor(Color.BLACK); 648 int offsetX = getOffsetX(); 649 int offsetY = getOffsetY(); 650 final int saveCount; 651 if (canvas.isHardwareAccelerated()) { 652 saveCount = canvas.saveLayer(offsetX - mStrokeWidth, offsetY, 653 offsetX + radius + shadowRadius, offsetY + shadowRadius + shadowRadius, 654 null, Canvas.CLIP_TO_LAYER_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG); 655 656 } else { 657 saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 658 clipCanvasSoftware(canvas, Region.Op.DIFFERENCE); 659 } 660 661 mShaderMatrix.setScale(shadowRadius, shadowRadius); 662 mShaderMatrix.postTranslate(radius + offsetX, shadowRadius + offsetY); 663 mShadowShader.setLocalMatrix(mShaderMatrix); 664 mPaint.setShader(mShadowShader); 665 canvas.drawPaint(mPaint); 666 mPaint.setShader(null); 667 668 if (canvas.isHardwareAccelerated()) { 669 mPaint.setXfermode(mShadowPorterDuffXfermode); 670 canvas.drawCircle(radius + offsetX, radius + offsetY, radius, mPaint); 671 mPaint.setXfermode(null); 672 } 673 674 canvas.restoreToCount(saveCount); 675 } 676 677 public void drawBackgroundStroke(Canvas canvas) { 678 mPaint.setColor(Color.argb(255, BG_INTENSITY, BG_INTENSITY, BG_INTENSITY)); 679 mPaint.setStyle(Paint.Style.STROKE); 680 mPaint.setStrokeWidth(mStrokeWidth); 681 drawCircle(canvas, 1 /* deltaRadius */); 682 } 683 684 public void drawLeaveBehind(Canvas canvas) { 685 float originalScale = mScale; 686 mScale = 0.5f; 687 688 mPaint.setStyle(Paint.Style.FILL); 689 mPaint.setColor(Color.argb(160, 245, 245, 245)); 690 drawCircle(canvas, 0 /* deltaRadius */); 691 692 mScale = originalScale; 693 } 694 695 private void drawCircle(Canvas canvas,float deltaRadius) { 696 float radius = getScaledRadius(); 697 canvas.drawCircle(radius + getOffsetX(), radius + getOffsetY(), 698 radius - deltaRadius, mPaint); 699 } 700 701 // It is the callers responsibility to save and restore the canvas layers. 702 private void clipCanvasSoftware(Canvas canvas, Region.Op op) { 703 mPath.reset(); 704 float r = getScaledRadius(); 705 mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW); 706 canvas.clipPath(mPath, op); 707 } 708 709 // It is the callers responsibility to save and restore the canvas layers. 710 private void clipCanvasHardware(Canvas canvas) { 711 mPaint.setColor(Color.BLACK); 712 mPaint.setXfermode(mClipPorterDuffXfermode); 713 714 float radius = getScaledRadius(); 715 mShaderMatrix.setScale(radius, radius); 716 mShaderMatrix.postTranslate(radius + getOffsetX(), radius + getOffsetY()); 717 mClipShader.setLocalMatrix(mShaderMatrix); 718 mPaint.setShader(mClipShader); 719 canvas.drawPaint(mPaint); 720 mPaint.setXfermode(null); 721 mPaint.setShader(null); 722 } 723 724 private void delegateDrawing(CellLayout delegate, int cellX, int cellY) { 725 if (mDrawingDelegate != delegate) { 726 delegate.addFolderBackground(this); 727 } 728 729 mDrawingDelegate = delegate; 730 delegateCellX = cellX; 731 delegateCellY = cellY; 732 733 invalidate(); 734 } 735 736 private void clearDrawingDelegate() { 737 if (mDrawingDelegate != null) { 738 mDrawingDelegate.removeFolderBackground(this); 739 } 740 741 mDrawingDelegate = null; 742 invalidate(); 743 } 744 745 private boolean drawingDelegated() { 746 return mDrawingDelegate != null; 747 } 748 749 private void animateScale(float finalScale, float finalMultiplier, 750 final Runnable onStart, final Runnable onEnd) { 751 final float scale0 = mScale; 752 final float scale1 = finalScale; 753 754 final float bgMultiplier0 = mColorMultiplier; 755 final float bgMultiplier1 = finalMultiplier; 756 757 if (mScaleAnimator != null) { 758 mScaleAnimator.cancel(); 759 } 760 761 mScaleAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f); 762 763 mScaleAnimator.addUpdateListener(new AnimatorUpdateListener() { 764 @Override 765 public void onAnimationUpdate(ValueAnimator animation) { 766 float prog = animation.getAnimatedFraction(); 767 mScale = prog * scale1 + (1 - prog) * scale0; 768 mColorMultiplier = prog * bgMultiplier1 + (1 - prog) * bgMultiplier0; 769 invalidate(); 770 } 771 }); 772 mScaleAnimator.addListener(new AnimatorListenerAdapter() { 773 @Override 774 public void onAnimationStart(Animator animation) { 775 if (onStart != null) { 776 onStart.run(); 777 } 778 } 779 780 @Override 781 public void onAnimationEnd(Animator animation) { 782 if (onEnd != null) { 783 onEnd.run(); 784 } 785 mScaleAnimator = null; 786 } 787 }); 788 789 mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); 790 mScaleAnimator.start(); 791 } 792 793 public void animateToAccept(final CellLayout cl, final int cellX, final int cellY) { 794 Runnable onStart = new Runnable() { 795 @Override 796 public void run() { 797 delegateDrawing(cl, cellX, cellY); 798 } 799 }; 800 animateScale(ACCEPT_SCALE_FACTOR, ACCEPT_COLOR_MULTIPLIER, onStart, null); 801 } 802 803 public void animateToRest() { 804 // This can be called multiple times -- we need to make sure the drawing delegate 805 // is saved and restored at the beginning of the animation, since cancelling the 806 // existing animation can clear the delgate. 807 final CellLayout cl = mDrawingDelegate; 808 final int cellX = delegateCellX; 809 final int cellY = delegateCellY; 810 811 Runnable onStart = new Runnable() { 812 @Override 813 public void run() { 814 delegateDrawing(cl, cellX, cellY); 815 } 816 }; 817 Runnable onEnd = new Runnable() { 818 @Override 819 public void run() { 820 clearDrawingDelegate(); 821 } 822 }; 823 animateScale(1f, 1f, onStart, onEnd); 824 } 825 } 826 827 public void setFolderBackground(PreviewBackground bg) { 828 mBackground = bg; 829 mBackground.setInvalidateDelegate(this); 830 } 831 832 @Override 833 protected void dispatchDraw(Canvas canvas) { 834 super.dispatchDraw(canvas); 835 836 if (mReferenceDrawable != null) { 837 computePreviewDrawingParams(mReferenceDrawable); 838 } 839 840 if (!mBackground.drawingDelegated()) { 841 mBackground.drawBackground(canvas); 842 } 843 844 if (mFolder == null) return; 845 if (mFolder.getItemCount() == 0 && !mAnimating) return; 846 847 final int saveCount; 848 849 if (canvas.isHardwareAccelerated()) { 850 saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, 851 Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG); 852 } else { 853 saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 854 if (mPreviewLayoutRule.clipToBackground()) { 855 mBackground.clipCanvasSoftware(canvas, Region.Op.INTERSECT); 856 } 857 } 858 859 // The items are drawn in coordinates relative to the preview offset 860 canvas.translate(mBackground.basePreviewOffsetX, mBackground.basePreviewOffsetY); 861 862 // The first item should be drawn last (ie. on top of later items) 863 for (int i = mDrawingParams.size() - 1; i >= 0; i--) { 864 PreviewItemDrawingParams p = mDrawingParams.get(i); 865 if (!p.hidden) { 866 drawPreviewItem(canvas, p); 867 } 868 } 869 canvas.translate(-mBackground.basePreviewOffsetX, -mBackground.basePreviewOffsetY); 870 871 if (mPreviewLayoutRule.clipToBackground() && canvas.isHardwareAccelerated()) { 872 mBackground.clipCanvasHardware(canvas); 873 } 874 canvas.restoreToCount(saveCount); 875 876 if (mPreviewLayoutRule.clipToBackground() && !mBackground.drawingDelegated()) { 877 mBackground.drawBackgroundStroke(canvas); 878 } 879 880 if ((mBadgeInfo != null && mBadgeInfo.hasBadge()) || mBadgeScale > 0) { 881 int offsetX = mBackground.getOffsetX(); 882 int offsetY = mBackground.getOffsetY(); 883 int previewSize = (int) (mBackground.previewSize * mBackground.mScale); 884 mTempBounds.set(offsetX, offsetY, offsetX + previewSize, offsetY + previewSize); 885 886 // If we are animating to the accepting state, animate the badge out. 887 float badgeScale = Math.max(0, mBadgeScale - mBackground.getScaleProgress()); 888 mTempSpaceForBadgeOffset.set(getWidth() - mTempBounds.right, mTempBounds.top); 889 IconPalette badgePalette = IconPalette.getFolderBadgePalette(getResources()); 890 mBadgeRenderer.draw(canvas, badgePalette, mBadgeInfo, mTempBounds, 891 badgeScale, mTempSpaceForBadgeOffset); 892 } 893 } 894 895 class FolderPreviewItemAnim { 896 ValueAnimator mValueAnimator; 897 float finalScale; 898 float finalTransX; 899 float finalTransY; 900 901 /** 902 * 903 * @param params layout params to animate 904 * @param index0 original index of the item to be animated 905 * @param nItems0 original number of items in the preview 906 * @param index1 new index of the item to be animated 907 * @param nItems1 new number of items in the preview 908 * @param duration duration in ms of the animation 909 * @param onCompleteRunnable runnable to execute upon animation completion 910 */ 911 public FolderPreviewItemAnim(final PreviewItemDrawingParams params, int index0, int nItems0, 912 int index1, int nItems1, int duration, final Runnable onCompleteRunnable) { 913 914 computePreviewItemDrawingParams(index1, nItems1, mTmpParams); 915 916 finalScale = mTmpParams.scale; 917 finalTransX = mTmpParams.transX; 918 finalTransY = mTmpParams.transY; 919 920 computePreviewItemDrawingParams(index0, nItems0, mTmpParams); 921 922 final float scale0 = mTmpParams.scale; 923 final float transX0 = mTmpParams.transX; 924 final float transY0 = mTmpParams.transY; 925 926 mValueAnimator = LauncherAnimUtils.ofFloat(0f, 1.0f); 927 mValueAnimator.addUpdateListener(new AnimatorUpdateListener(){ 928 public void onAnimationUpdate(ValueAnimator animation) { 929 float progress = animation.getAnimatedFraction(); 930 931 params.transX = transX0 + progress * (finalTransX - transX0); 932 params.transY = transY0 + progress * (finalTransY - transY0); 933 params.scale = scale0 + progress * (finalScale - scale0); 934 invalidate(); 935 } 936 }); 937 938 mValueAnimator.addListener(new AnimatorListenerAdapter() { 939 @Override 940 public void onAnimationStart(Animator animation) { 941 } 942 943 @Override 944 public void onAnimationEnd(Animator animation) { 945 if (onCompleteRunnable != null) { 946 onCompleteRunnable.run(); 947 } 948 params.anim = null; 949 } 950 }); 951 mValueAnimator.setDuration(duration); 952 } 953 954 public void start() { 955 mValueAnimator.start(); 956 } 957 958 public void cancel() { 959 mValueAnimator.cancel(); 960 } 961 962 public boolean hasEqualFinalState(FolderPreviewItemAnim anim) { 963 return finalTransY == anim.finalTransY && finalTransX == anim.finalTransX && 964 finalScale == anim.finalScale; 965 966 } 967 } 968 969 private void animateFirstItem(final Drawable d, int duration, final boolean reverse, 970 final Runnable onCompleteRunnable) { 971 972 FolderPreviewItemAnim anim; 973 if (!reverse) { 974 anim = new FolderPreviewItemAnim(mDrawingParams.get(0), -1, -1, 0, 2, duration, 975 onCompleteRunnable); 976 } else { 977 anim = new FolderPreviewItemAnim(mDrawingParams.get(0), 0, 2, -1, -1, duration, 978 onCompleteRunnable); 979 } 980 anim.start(); 981 } 982 983 public void setTextVisible(boolean visible) { 984 if (visible) { 985 mFolderName.setVisibility(VISIBLE); 986 } else { 987 mFolderName.setVisibility(INVISIBLE); 988 } 989 } 990 991 public boolean getTextVisible() { 992 return mFolderName.getVisibility() == VISIBLE; 993 } 994 995 private void updateItemDrawingParams(boolean animate) { 996 List<View> items = mPreviewLayoutRule.getItemsToDisplay(mFolder); 997 int nItemsInPreview = items.size(); 998 999 int prevNumItems = mDrawingParams.size(); 1000 1001 // We adjust the size of the list to match the number of items in the preview 1002 while (nItemsInPreview < mDrawingParams.size()) { 1003 mDrawingParams.remove(mDrawingParams.size() - 1); 1004 } 1005 while (nItemsInPreview > mDrawingParams.size()) { 1006 mDrawingParams.add(new PreviewItemDrawingParams(0, 0, 0, 0)); 1007 } 1008 1009 for (int i = 0; i < mDrawingParams.size(); i++) { 1010 PreviewItemDrawingParams p = mDrawingParams.get(i); 1011 p.drawable = ((TextView) items.get(i)).getCompoundDrawables()[1]; 1012 1013 if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) { 1014 computePreviewItemDrawingParams(i, nItemsInPreview, p); 1015 if (mReferenceDrawable == null) { 1016 mReferenceDrawable = p.drawable; 1017 } 1018 } else { 1019 FolderPreviewItemAnim anim = new FolderPreviewItemAnim(p, i, prevNumItems, i, 1020 nItemsInPreview, DROP_IN_ANIMATION_DURATION, null); 1021 1022 if (p.anim != null) { 1023 if (p.anim.hasEqualFinalState(anim)) { 1024 // do nothing, let the current animation finish 1025 continue; 1026 } 1027 p.anim.cancel(); 1028 } 1029 p.anim = anim; 1030 p.anim.start(); 1031 } 1032 } 1033 } 1034 1035 @Override 1036 public void onItemsChanged(boolean animate) { 1037 updateItemDrawingParams(animate); 1038 invalidate(); 1039 requestLayout(); 1040 } 1041 1042 @Override 1043 public void prepareAutoUpdate() { 1044 } 1045 1046 @Override 1047 public void onAdd(ShortcutInfo item) { 1048 boolean wasBadged = mBadgeInfo.hasBadge(); 1049 mBadgeInfo.addBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item)); 1050 boolean isBadged = mBadgeInfo.hasBadge(); 1051 updateBadgeScale(wasBadged, isBadged); 1052 invalidate(); 1053 requestLayout(); 1054 } 1055 1056 @Override 1057 public void onRemove(ShortcutInfo item) { 1058 boolean wasBadged = mBadgeInfo.hasBadge(); 1059 mBadgeInfo.subtractBadgeInfo(mLauncher.getPopupDataProvider().getBadgeInfoForItem(item)); 1060 boolean isBadged = mBadgeInfo.hasBadge(); 1061 updateBadgeScale(wasBadged, isBadged); 1062 invalidate(); 1063 requestLayout(); 1064 } 1065 1066 @Override 1067 public void onTitleChanged(CharSequence title) { 1068 mFolderName.setText(title); 1069 setContentDescription(getContext().getString(R.string.folder_name_format, title)); 1070 } 1071 1072 @Override 1073 public boolean onTouchEvent(MotionEvent event) { 1074 // Call the superclass onTouchEvent first, because sometimes it changes the state to 1075 // isPressed() on an ACTION_UP 1076 boolean result = super.onTouchEvent(event); 1077 1078 // Check for a stylus button press, if it occurs cancel any long press checks. 1079 if (mStylusEventHelper.onMotionEvent(event)) { 1080 mLongPressHelper.cancelLongPress(); 1081 return true; 1082 } 1083 1084 switch (event.getAction()) { 1085 case MotionEvent.ACTION_DOWN: 1086 mLongPressHelper.postCheckForLongPress(); 1087 break; 1088 case MotionEvent.ACTION_CANCEL: 1089 case MotionEvent.ACTION_UP: 1090 mLongPressHelper.cancelLongPress(); 1091 break; 1092 case MotionEvent.ACTION_MOVE: 1093 if (!Utilities.pointInView(this, event.getX(), event.getY(), mSlop)) { 1094 mLongPressHelper.cancelLongPress(); 1095 } 1096 break; 1097 } 1098 return result; 1099 } 1100 1101 @Override 1102 public void cancelLongPress() { 1103 super.cancelLongPress(); 1104 mLongPressHelper.cancelLongPress(); 1105 } 1106 1107 public void removeListeners() { 1108 mInfo.removeListener(this); 1109 mInfo.removeListener(mFolder); 1110 } 1111 1112 public void shrinkAndFadeIn(boolean animate) { 1113 final CellLayout cl = (CellLayout) getParent().getParent(); 1114 ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true; 1115 1116 // We remove and re-draw the FolderIcon in-case it has changed 1117 final PreviewImageView previewImage = PreviewImageView.get(getContext()); 1118 previewImage.removeFromParent(); 1119 copyToPreview(previewImage); 1120 1121 if (cl != null) { 1122 cl.clearFolderLeaveBehind(); 1123 } 1124 1125 ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(previewImage, 1, 1, 1); 1126 oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration)); 1127 oa.addListener(new AnimatorListenerAdapter() { 1128 @Override 1129 public void onAnimationEnd(Animator animation) { 1130 if (cl != null) { 1131 // Remove the ImageView copy of the FolderIcon and make the original visible. 1132 previewImage.removeFromParent(); 1133 setVisibility(View.VISIBLE); 1134 } 1135 } 1136 }); 1137 oa.start(); 1138 if (!animate) { 1139 oa.end(); 1140 } 1141 } 1142 1143 public void growAndFadeOut() { 1144 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams(); 1145 // While the folder is open, the position of the icon cannot change. 1146 lp.canReorder = false; 1147 if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { 1148 CellLayout cl = (CellLayout) getParent().getParent(); 1149 cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY); 1150 } 1151 1152 // Push an ImageView copy of the FolderIcon into the DragLayer and hide the original 1153 PreviewImageView previewImage = PreviewImageView.get(getContext()); 1154 copyToPreview(previewImage); 1155 setVisibility(View.INVISIBLE); 1156 1157 ObjectAnimator oa = LauncherAnimUtils.ofViewAlphaAndScale(previewImage, 0, 1.5f, 1.5f); 1158 oa.setDuration(getResources().getInteger(R.integer.config_folderExpandDuration)); 1159 oa.start(); 1160 } 1161 1162 /** 1163 * This method draws the FolderIcon to an ImageView and then adds and positions that ImageView 1164 * in the DragLayer in the exact absolute location of the original FolderIcon. 1165 */ 1166 private void copyToPreview(PreviewImageView previewImageView) { 1167 previewImageView.copy(this); 1168 if (mFolder != null) { 1169 previewImageView.setPivotX(mFolder.getPivotXForIconAnimation()); 1170 previewImageView.setPivotY(mFolder.getPivotYForIconAnimation()); 1171 mFolder.bringToFront(); 1172 } 1173 } 1174 1175 public interface PreviewLayoutRule { 1176 PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems, 1177 PreviewItemDrawingParams params); 1178 void init(int availableSpace, int intrinsicIconSize, boolean rtl); 1179 float scaleForItem(int index, int totalNumItems); 1180 int maxNumItems(); 1181 boolean clipToBackground(); 1182 List<View> getItemsToDisplay(Folder folder); 1183 } 1184 } 1185