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.launcher3.folder; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.ValueAnimator; 23import android.animation.ValueAnimator.AnimatorUpdateListener; 24import android.content.Context; 25import android.graphics.Canvas; 26import android.graphics.Color; 27import android.graphics.Matrix; 28import android.graphics.Paint; 29import android.graphics.Path; 30import android.graphics.Point; 31import android.graphics.PorterDuff; 32import android.graphics.PorterDuffXfermode; 33import android.graphics.RadialGradient; 34import android.graphics.Rect; 35import android.graphics.Region; 36import android.graphics.Shader; 37import android.graphics.drawable.Drawable; 38import android.os.Parcelable; 39import android.util.AttributeSet; 40import android.util.DisplayMetrics; 41import android.util.Property; 42import android.view.LayoutInflater; 43import android.view.MotionEvent; 44import android.view.View; 45import android.view.ViewConfiguration; 46import android.view.ViewGroup; 47import android.view.animation.AccelerateInterpolator; 48import android.view.animation.DecelerateInterpolator; 49import android.widget.FrameLayout; 50import android.widget.TextView; 51 52import com.android.launcher3.Alarm; 53import com.android.launcher3.AppInfo; 54import com.android.launcher3.BubbleTextView; 55import com.android.launcher3.CellLayout; 56import com.android.launcher3.CheckLongPressHelper; 57import com.android.launcher3.DeviceProfile; 58import com.android.launcher3.DropTarget.DragObject; 59import com.android.launcher3.FastBitmapDrawable; 60import com.android.launcher3.FolderInfo; 61import com.android.launcher3.FolderInfo.FolderListener; 62import com.android.launcher3.ItemInfo; 63import com.android.launcher3.Launcher; 64import com.android.launcher3.LauncherAnimUtils; 65import com.android.launcher3.LauncherSettings; 66import com.android.launcher3.OnAlarmListener; 67import com.android.launcher3.R; 68import com.android.launcher3.ShortcutInfo; 69import com.android.launcher3.SimpleOnStylusPressListener; 70import com.android.launcher3.StylusEventHelper; 71import com.android.launcher3.Utilities; 72import com.android.launcher3.Workspace; 73import com.android.launcher3.badge.BadgeRenderer; 74import com.android.launcher3.badge.FolderBadgeInfo; 75import com.android.launcher3.config.FeatureFlags; 76import com.android.launcher3.dragndrop.DragLayer; 77import com.android.launcher3.dragndrop.DragView; 78import com.android.launcher3.graphics.IconPalette; 79import com.android.launcher3.util.Thunk; 80 81import java.util.ArrayList; 82import java.util.List; 83 84/** 85 * An icon that can appear on in the workspace representing an {@link Folder}. 86 */ 87public 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 154 public FolderIcon(Context context, AttributeSet attrs) { 155 super(context, attrs); 156 init(); 157 } 158 159 public FolderIcon(Context context) { 160 super(context); 161 init(); 162 } 163 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 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 213 protected Parcelable onSaveInstanceState() { 214 sStaticValuesDirty = true; 215 return super.onSaveInstanceState(); 216 } 217 218 public Folder getFolder() { 219 return mFolder; 220 } 221 222 private void setFolder(Folder folder) { 223 mFolder = folder; 224 updateItemDrawingParams(false); 225 } 226 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 235 public boolean acceptDrop(ItemInfo dragInfo) { 236 final ItemInfo item = dragInfo; 237 return !mFolder.isDestroyed() && willAcceptItem(item); 238 } 239 240 public void addItem(ShortcutInfo item) { 241 mInfo.add(item, true); 242 } 243 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 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 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 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 304 public void onDragExit() { 305 mBackground.animateToRest(); 306 mOpenAlarm.cancelAlarm(); 307 } 308 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