1 /* 2 * Copyright (C) 2019 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 package com.android.launcher3.views; 17 18 import static android.view.Gravity.LEFT; 19 20 import static com.android.app.animation.Interpolators.LINEAR; 21 import static com.android.launcher3.Flags.enableAdditionalHomeAnimations; 22 import static com.android.launcher3.Utilities.getFullDrawable; 23 import static com.android.launcher3.Utilities.mapToRange; 24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 25 import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; 26 27 import android.animation.Animator; 28 import android.content.Context; 29 import android.graphics.Canvas; 30 import android.graphics.Rect; 31 import android.graphics.RectF; 32 import android.graphics.drawable.AdaptiveIconDrawable; 33 import android.graphics.drawable.Drawable; 34 import android.os.CancellationSignal; 35 import android.util.AttributeSet; 36 import android.util.Log; 37 import android.util.Pair; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.ViewTreeObserver.OnGlobalLayoutListener; 41 import android.widget.FrameLayout; 42 import android.widget.ImageView; 43 44 import androidx.annotation.Nullable; 45 import androidx.annotation.UiThread; 46 import androidx.annotation.WorkerThread; 47 48 import com.android.launcher3.BubbleTextView; 49 import com.android.launcher3.DeviceProfile; 50 import com.android.launcher3.InsettableFrameLayout; 51 import com.android.launcher3.Launcher; 52 import com.android.launcher3.R; 53 import com.android.launcher3.Utilities; 54 import com.android.launcher3.dragndrop.DragLayer; 55 import com.android.launcher3.folder.FolderIcon; 56 import com.android.launcher3.graphics.PreloadIconDrawable; 57 import com.android.launcher3.icons.FastBitmapDrawable; 58 import com.android.launcher3.icons.LauncherIcons; 59 import com.android.launcher3.model.data.ItemInfo; 60 import com.android.launcher3.model.data.ItemInfoWithIcon; 61 import com.android.launcher3.popup.SystemShortcut; 62 import com.android.launcher3.shortcuts.DeepShortcutView; 63 64 import java.util.function.Supplier; 65 66 /** 67 * A view that is created to look like another view with the purpose of creating fluid animations. 68 */ 69 public class FloatingIconView extends FrameLayout implements 70 Animator.AnimatorListener, OnGlobalLayoutListener, FloatingView { 71 72 private static final String TAG = "FloatingIconView"; 73 74 // Manages loading the icon on a worker thread 75 private static @Nullable IconLoadResult sIconLoadResult; 76 private static long sFetchIconId = 0; 77 private static long sRecycledFetchIconId = sFetchIconId; 78 79 public static final float SHAPE_PROGRESS_DURATION = 0.10f; 80 private static final RectF sTmpRectF = new RectF(); 81 82 private Runnable mEndRunnable; 83 private CancellationSignal mLoadIconSignal; 84 85 private final Launcher mLauncher; 86 private final boolean mIsRtl; 87 88 private boolean mIsOpening; 89 90 private IconLoadResult mIconLoadResult; 91 92 private View mBtvDrawable; 93 94 private ClipIconView mClipIconView; 95 private @Nullable Drawable mBadge; 96 97 // A view whose visibility should update in sync with mOriginalIcon. 98 private @Nullable View mMatchVisibilityView; 99 100 // A view that will fade out as the animation progresses. 101 private @Nullable View mFadeOutView; 102 103 private View mOriginalIcon; 104 private RectF mPositionOut; 105 private Runnable mOnTargetChangeRunnable; 106 107 private final Rect mFinalDrawableBounds = new Rect(); 108 109 private ListenerView mListenerView; 110 private Runnable mFastFinishRunnable; 111 112 private float mIconOffsetY; 113 FloatingIconView(Context context)114 public FloatingIconView(Context context) { 115 this(context, null); 116 } 117 FloatingIconView(Context context, AttributeSet attrs)118 public FloatingIconView(Context context, AttributeSet attrs) { 119 this(context, attrs, 0); 120 } 121 FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr)122 public FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr) { 123 super(context, attrs, defStyleAttr); 124 mLauncher = Launcher.getLauncher(context); 125 mIsRtl = Utilities.isRtl(getResources()); 126 mListenerView = new ListenerView(context, attrs); 127 mClipIconView = new ClipIconView(context, attrs); 128 mBtvDrawable = new ImageView(context, attrs); 129 addView(mBtvDrawable); 130 addView(mClipIconView); 131 setWillNotDraw(false); 132 } 133 134 @Override onAttachedToWindow()135 protected void onAttachedToWindow() { 136 super.onAttachedToWindow(); 137 if (!mIsOpening) { 138 getViewTreeObserver().addOnGlobalLayoutListener(this); 139 } 140 } 141 142 @Override onDetachedFromWindow()143 protected void onDetachedFromWindow() { 144 getViewTreeObserver().removeOnGlobalLayoutListener(this); 145 super.onDetachedFromWindow(); 146 } 147 148 /** 149 * Positions this view to match the size and location of {@code rect}. 150 */ update(float alpha, RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening)151 public void update(float alpha, RectF rect, float progress, float shapeProgressStart, 152 float cornerRadius, boolean isOpening) { 153 update(alpha, rect, progress, shapeProgressStart, cornerRadius, isOpening, 0); 154 } 155 156 /** 157 * Positions this view to match the size and location of {@code rect}. 158 * <p> 159 * @param alpha The alpha[0, 1] of the entire floating view. 160 * @param progress A value from [0, 1] that represents the animation progress. 161 * @param shapeProgressStart The progress value at which to start the shape reveal. 162 * @param cornerRadius The corner radius of {@code rect}. 163 * @param isOpening True if view is used for app open animation, false for app close animation. 164 * @param taskViewDrawAlpha the drawn {@link com.android.quickstep.views.TaskView} alpha 165 */ update(float alpha, RectF rect, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening, int taskViewDrawAlpha)166 public void update(float alpha, RectF rect, float progress, float shapeProgressStart, 167 float cornerRadius, boolean isOpening, int taskViewDrawAlpha) { 168 // The non-running task home animation has some very funky first few frames because this 169 // FIV hasn't fully laid out. During those frames, hide this FIV and continue drawing the 170 // TaskView directly while transforming it in the place of this FIV. However, if we fade 171 // the TaskView at all, we need to display this FIV regardless. 172 setAlpha(!enableAdditionalHomeAnimations() || isLaidOut() || taskViewDrawAlpha < 255 173 ? alpha : 0f); 174 mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, this, 175 mLauncher.getDeviceProfile(), taskViewDrawAlpha); 176 177 if (mFadeOutView != null) { 178 // The alpha goes from 1 to 0 when progress is 0 and 0.33 respectively. 179 mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.33f, 0, 1, LINEAR))); 180 } 181 } 182 183 /** 184 * Sets a {@link com.android.quickstep.views.TaskView} that will draw a 185 * {@link com.android.quickstep.views.TaskView} within the {@code mClipIconView} clip bounds 186 */ setOverlayArtist(ClipIconView.TaskViewArtist taskViewArtist)187 public void setOverlayArtist(ClipIconView.TaskViewArtist taskViewArtist) { 188 mClipIconView.setTaskViewArtist(taskViewArtist); 189 } 190 191 @Override onAnimationEnd(Animator animator)192 public void onAnimationEnd(Animator animator) { 193 if (mLoadIconSignal != null) { 194 mLoadIconSignal.cancel(); 195 } 196 if (mEndRunnable != null) { 197 mEndRunnable.run(); 198 } else { 199 // End runnable also ends the reveal animator, so we manually handle it here. 200 mClipIconView.endReveal(); 201 } 202 } 203 204 /** 205 * Sets the size and position of this view to match {@code v}. 206 * <p> 207 * @param v The view to copy 208 * @param positionOut Rect that will hold the size and position of v. 209 */ matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut)210 private void matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut) { 211 getLocationBoundsForView(launcher, v, isOpening, positionOut); 212 final InsettableFrameLayout.LayoutParams lp = new InsettableFrameLayout.LayoutParams( 213 Math.round(positionOut.width()), 214 Math.round(positionOut.height())); 215 updatePosition(positionOut, lp); 216 setLayoutParams(lp); 217 218 // For code simplicity, we always layout the child views using Gravity.LEFT 219 // and manually handle RTL for FloatingIconView when positioning it on the screen. 220 mClipIconView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height, LEFT)); 221 mBtvDrawable.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height, LEFT)); 222 } 223 updatePosition(RectF pos, InsettableFrameLayout.LayoutParams lp)224 private void updatePosition(RectF pos, InsettableFrameLayout.LayoutParams lp) { 225 mPositionOut.set(pos); 226 lp.ignoreInsets = true; 227 // Position the floating view exactly on top of the original 228 lp.topMargin = Math.round(pos.top); 229 if (mIsRtl) { 230 lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - pos.right)); 231 } else { 232 lp.setMarginStart(Math.round(pos.left)); 233 } 234 // Set the properties here already to make sure they are available when running the first 235 // animation frame. 236 int left = mIsRtl 237 ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width 238 : lp.leftMargin; 239 layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); 240 } 241 getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, RectF outRect)242 private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, 243 RectF outRect) { 244 getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect()); 245 } 246 247 /** 248 * Gets the location bounds of a view and returns the overall rotation. 249 * - For DeepShortcutView, we return the bounds of the icon view. 250 * - For BubbleTextView, we return the icon bounds. 251 */ getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, RectF outRect, Rect outViewBounds)252 public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, 253 RectF outRect, Rect outViewBounds) { 254 boolean ignoreTransform = !isOpening; 255 if (v instanceof BubbleTextHolder) { 256 v = ((BubbleTextHolder) v).getBubbleText(); 257 ignoreTransform = false; 258 } else if (v.getParent() instanceof DeepShortcutView) { 259 v = ((DeepShortcutView) v.getParent()).getIconView(); 260 ignoreTransform = false; 261 } 262 if (v == null) { 263 return; 264 } 265 266 if (v instanceof BubbleTextView) { 267 ((BubbleTextView) v).getIconBounds(outViewBounds); 268 } else if (v instanceof FolderIcon) { 269 ((FolderIcon) v).getPreviewBounds(outViewBounds); 270 } else { 271 outViewBounds.set(0, 0, v.getWidth(), v.getHeight()); 272 } 273 274 Utilities.getBoundsForViewInDragLayer(launcher.getDragLayer(), v, outViewBounds, 275 ignoreTransform, null /** recycle */, outRect); 276 } 277 278 /** 279 * Loads the icon and saves the results to {@link #sIconLoadResult}. 280 * <p> 281 * Runs onIconLoaded callback (if any), which signifies that the FloatingIconView is 282 * ready to display the icon. Otherwise, the FloatingIconView will grab the results when its 283 * initialized. 284 * <p> 285 * @param originalView The View that the FloatingIconView will replace. 286 * @param info ItemInfo of the originalView 287 * @param pos The position of the view. 288 * @param btvIcon The drawable of the BubbleTextView. May be null if original view is not a BTV 289 * @param outIconLoadResult We store the icon results into this object. 290 */ 291 @WorkerThread 292 @SuppressWarnings("WrongThread") getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos, @Nullable Drawable btvIcon, IconLoadResult outIconLoadResult)293 private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos, 294 @Nullable Drawable btvIcon, IconLoadResult outIconLoadResult) { 295 Drawable drawable; 296 boolean supportsAdaptiveIcons = !info.isDisabled(); // Use original icon for disabled icons. 297 298 Drawable badge = null; 299 if (info instanceof SystemShortcut) { 300 if (originalView instanceof ImageView) { 301 drawable = ((ImageView) originalView).getDrawable(); 302 } else if (originalView instanceof DeepShortcutView) { 303 drawable = ((DeepShortcutView) originalView).getIconView().getBackground(); 304 } else { 305 drawable = originalView.getBackground(); 306 } 307 } else if (btvIcon instanceof PreloadIconDrawable) { 308 // Force the progress bar to display. 309 drawable = btvIcon; 310 } else if (originalView instanceof ImageView) { 311 drawable = ((ImageView) originalView).getDrawable(); 312 } else { 313 int width = (int) pos.width(); 314 int height = (int) pos.height(); 315 Pair<AdaptiveIconDrawable, Drawable> fullIcon = null; 316 if (supportsAdaptiveIcons) { 317 boolean shouldThemeIcon = (btvIcon instanceof FastBitmapDrawable fbd) 318 && fbd.isCreatedForTheme(); 319 fullIcon = getFullDrawable(l, info, width, height, shouldThemeIcon); 320 } else if (!(originalView instanceof BubbleTextView)) { 321 fullIcon = getFullDrawable(l, info, width, height, true /* shouldThemeIcon */); 322 } 323 324 if (fullIcon != null) { 325 drawable = fullIcon.first; 326 badge = fullIcon.second; 327 } else { 328 drawable = btvIcon; 329 } 330 } 331 332 drawable = drawable == null ? null : drawable.getConstantState().newDrawable(); 333 int iconOffset = getOffsetForIconBounds(l, drawable, pos); 334 // Clone right away as we are on the background thread instead of blocking the 335 // main thread later 336 Drawable btvClone = btvIcon == null ? null : btvIcon.getConstantState().newDrawable(); 337 synchronized (outIconLoadResult) { 338 outIconLoadResult.btvDrawable = () -> btvClone; 339 outIconLoadResult.drawable = drawable; 340 outIconLoadResult.badge = badge; 341 outIconLoadResult.iconOffset = iconOffset; 342 if (outIconLoadResult.onIconLoaded != null) { 343 l.getMainExecutor().execute(outIconLoadResult.onIconLoaded); 344 outIconLoadResult.onIconLoaded = null; 345 } 346 outIconLoadResult.isIconLoaded = true; 347 } 348 } 349 350 /** 351 * Sets the drawables of the {@code originalView} onto this view. 352 * <p> 353 * @param drawable The drawable of the original view. 354 * @param badge The badge of the original view. 355 * @param iconOffset The amount of offset needed to match this view with the original view. 356 */ 357 @UiThread setIcon(@ullable Drawable drawable, @Nullable Drawable badge, @Nullable Supplier<Drawable> btvIcon, int iconOffset)358 private void setIcon(@Nullable Drawable drawable, @Nullable Drawable badge, 359 @Nullable Supplier<Drawable> btvIcon, int iconOffset) { 360 final DeviceProfile dp = mLauncher.getDeviceProfile(); 361 final InsettableFrameLayout.LayoutParams lp = 362 (InsettableFrameLayout.LayoutParams) getLayoutParams(); 363 mBadge = badge; 364 mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, dp); 365 if (drawable instanceof AdaptiveIconDrawable) { 366 final int originalHeight = lp.height; 367 final int originalWidth = lp.width; 368 369 mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight); 370 371 float aspectRatio = mLauncher.getDeviceProfile().aspectRatio; 372 if (dp.isLandscape) { 373 lp.width = (int) Math.max(lp.width, lp.height * aspectRatio); 374 } else { 375 lp.height = (int) Math.max(lp.height, lp.width * aspectRatio); 376 } 377 setLayoutParams(lp); 378 379 final LayoutParams clipViewLp = (LayoutParams) mClipIconView.getLayoutParams(); 380 if (mBadge != null) { 381 Rect badgeBounds = new Rect(0, 0, clipViewLp.width, clipViewLp.height); 382 FastBitmapDrawable.setBadgeBounds(mBadge, badgeBounds); 383 } 384 clipViewLp.width = lp.width; 385 clipViewLp.height = lp.height; 386 mClipIconView.setLayoutParams(clipViewLp); 387 } 388 389 setOriginalDrawableBackground(btvIcon); 390 invalidate(); 391 } 392 393 /** 394 * Draws the drawable of the BubbleTextView behind ClipIconView 395 * <p> 396 * This is used to: 397 * - Have icon displayed while Adaptive Icon is loading 398 * - Displays the built in shadow to ensure a clean handoff 399 * <p> 400 * Allows nullable as this may be cleared when drawing is deferred to ClipIconView. 401 */ setOriginalDrawableBackground(@ullable Supplier<Drawable> btvIcon)402 private void setOriginalDrawableBackground(@Nullable Supplier<Drawable> btvIcon) { 403 if (!mIsOpening) { 404 mBtvDrawable.setBackground(btvIcon == null ? null : btvIcon.get()); 405 } 406 } 407 408 /** 409 * Returns true if the icon is different from main app icon 410 */ isDifferentFromAppIcon()411 public boolean isDifferentFromAppIcon() { 412 return mIconLoadResult == null ? false : mIconLoadResult.isThemed; 413 } 414 415 /** 416 * Checks if the icon result is loaded. If true, we set the icon immediately. Else, we add a 417 * callback to set the icon once the icon result is loaded. 418 */ checkIconResult()419 private void checkIconResult() { 420 CancellationSignal cancellationSignal = new CancellationSignal(); 421 422 if (mIconLoadResult == null) { 423 Log.w(TAG, "No icon load result found in checkIconResult"); 424 return; 425 } 426 427 synchronized (mIconLoadResult) { 428 if (mIconLoadResult.isIconLoaded) { 429 setIcon(mIconLoadResult.drawable, mIconLoadResult.badge, 430 mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset); 431 setVisibility(VISIBLE); 432 updateViewsVisibility(false /* isVisible */); 433 } else { 434 mIconLoadResult.onIconLoaded = () -> { 435 if (cancellationSignal.isCanceled()) { 436 return; 437 } 438 439 setIcon(mIconLoadResult.drawable, mIconLoadResult.badge, 440 mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset); 441 442 setVisibility(VISIBLE); 443 updateViewsVisibility(false /* isVisible */); 444 }; 445 mLoadIconSignal = cancellationSignal; 446 } 447 } 448 } 449 450 @WorkerThread 451 @SuppressWarnings("WrongThread") getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position)452 private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) { 453 if (!(drawable instanceof AdaptiveIconDrawable)) { 454 return 0; 455 } 456 int blurSizeOutline = 457 l.getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline); 458 459 Rect bounds = new Rect(0, 0, (int) position.width() + blurSizeOutline, 460 (int) position.height() + blurSizeOutline); 461 bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2); 462 463 try (LauncherIcons li = LauncherIcons.obtain(l)) { 464 Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable, null, 465 null, null)); 466 } 467 468 bounds.inset( 469 (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()), 470 (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction()) 471 ); 472 473 return bounds.left; 474 } 475 476 @Override dispatchDraw(Canvas canvas)477 protected void dispatchDraw(Canvas canvas) { 478 super.dispatchDraw(canvas); 479 if (mBadge != null) { 480 mBadge.draw(canvas); 481 } 482 } 483 484 /** 485 * Sets a runnable that is called after a call to {@link #fastFinish()}. 486 */ setFastFinishRunnable(Runnable runnable)487 public void setFastFinishRunnable(Runnable runnable) { 488 mFastFinishRunnable = runnable; 489 } 490 491 @Override fastFinish()492 public void fastFinish() { 493 if (mFastFinishRunnable != null) { 494 mFastFinishRunnable.run(); 495 mFastFinishRunnable = null; 496 } 497 if (mLoadIconSignal != null) { 498 mLoadIconSignal.cancel(); 499 mLoadIconSignal = null; 500 } 501 if (mEndRunnable != null) { 502 mEndRunnable.run(); 503 mEndRunnable = null; 504 } 505 } 506 507 @Override onAnimationStart(Animator animator)508 public void onAnimationStart(Animator animator) { 509 if ((mIconLoadResult != null && mIconLoadResult.isIconLoaded) 510 || (!mIsOpening && mBtvDrawable.getBackground() != null)) { 511 // No need to wait for icon load since we can display the BubbleTextView drawable. 512 setVisibility(View.VISIBLE); 513 } 514 if (!mIsOpening) { 515 // When closing an app, we want the item on the workspace to be invisible immediately 516 updateViewsVisibility(false /* isVisible */); 517 } 518 } 519 520 @Override onAnimationCancel(Animator animator)521 public void onAnimationCancel(Animator animator) {} 522 523 @Override onAnimationRepeat(Animator animator)524 public void onAnimationRepeat(Animator animator) {} 525 526 @Override setPositionOffsetY(float y)527 public void setPositionOffsetY(float y) { 528 mIconOffsetY = y; 529 onGlobalLayout(); 530 } 531 532 @Override onGlobalLayout()533 public void onGlobalLayout() { 534 if (mOriginalIcon != null && mOriginalIcon.isAttachedToWindow() && mPositionOut != null) { 535 getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening, sTmpRectF); 536 sTmpRectF.offset(0, mIconOffsetY); 537 if (!sTmpRectF.equals(mPositionOut)) { 538 updatePosition(sTmpRectF, (InsettableFrameLayout.LayoutParams) getLayoutParams()); 539 if (mOnTargetChangeRunnable != null) { 540 mOnTargetChangeRunnable.run(); 541 } 542 } 543 } 544 } 545 setOnTargetChangeListener(Runnable onTargetChangeListener)546 public void setOnTargetChangeListener(Runnable onTargetChangeListener) { 547 mOnTargetChangeRunnable = onTargetChangeListener; 548 } 549 550 /** 551 * Loads the icon drawable on a worker thread to reduce latency between swapping views. 552 */ 553 @UiThread fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening)554 public static IconLoadResult fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening) { 555 RectF position = new RectF(); 556 getLocationBoundsForView(l, v, isOpening, position); 557 558 final FastBitmapDrawable btvIcon; 559 final Supplier<Drawable> btvDrawableSupplier; 560 if (v instanceof BubbleTextView) { 561 BubbleTextView btv = (BubbleTextView) v; 562 if (info instanceof ItemInfoWithIcon 563 && (((ItemInfoWithIcon) info).runtimeStatusFlags 564 & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { 565 btvIcon = btv.makePreloadIcon(); 566 btvDrawableSupplier = () -> btvIcon; 567 } else { 568 btvIcon = btv.getIcon(); 569 // Clone when needed 570 btvDrawableSupplier = () -> btvIcon.getConstantState().newDrawable(); 571 } 572 } else { 573 btvIcon = null; 574 btvDrawableSupplier = null; 575 } 576 577 IconLoadResult result = new IconLoadResult(info, btvIcon != null && btvIcon.isThemed()); 578 result.btvDrawable = btvDrawableSupplier; 579 580 final long fetchIconId = sFetchIconId++; 581 MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> { 582 if (fetchIconId < sRecycledFetchIconId) { 583 return; 584 } 585 getIconResult(l, v, info, position, btvIcon, result); 586 }); 587 588 sIconLoadResult = result; 589 return result; 590 } 591 592 /** 593 * Resets the static icon load result used for preloading the icon for a launching app. 594 */ resetIconLoadResult()595 public static void resetIconLoadResult() { 596 sIconLoadResult = null; 597 } 598 599 /** 600 * Creates a floating icon view for {@code originalView}. 601 * <p> 602 * @param originalView The view to copy 603 * @param visibilitySyncView A view whose visibility should update in sync with originalView. 604 * @param fadeOutView A view that will fade out as the animation progresses. 605 * @param hideOriginal If true, it will hide {@code originalView} while this view is visible. 606 * Else, we will not draw anything in this view. 607 * @param positionOut Rect that will hold the size and position of v. 608 * @param isOpening True if this view replaces the icon for app open animation. 609 */ getFloatingIconView(Launcher launcher, View originalView, @Nullable View visibilitySyncView, @Nullable View fadeOutView, boolean hideOriginal, RectF positionOut, boolean isOpening)610 public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView, 611 @Nullable View visibilitySyncView, @Nullable View fadeOutView, boolean hideOriginal, 612 RectF positionOut, boolean isOpening) { 613 final DragLayer dragLayer = launcher.getDragLayer(); 614 ViewGroup parent = (ViewGroup) dragLayer.getParent(); 615 FloatingIconView view = launcher.getViewCache().getView(R.layout.floating_icon_view, 616 launcher, parent); 617 view.recycle(); 618 619 // Init properties before getting the drawable. 620 view.mIsOpening = isOpening; 621 view.mOriginalIcon = originalView; 622 view.mMatchVisibilityView = visibilitySyncView; 623 view.mFadeOutView = fadeOutView; 624 view.mPositionOut = positionOut; 625 626 // Get the drawable on the background thread 627 boolean shouldLoadIcon = originalView.getTag() instanceof ItemInfo && hideOriginal; 628 if (shouldLoadIcon) { 629 if (sIconLoadResult != null && sIconLoadResult.itemInfo == originalView.getTag()) { 630 view.mIconLoadResult = sIconLoadResult; 631 } else { 632 view.mIconLoadResult = fetchIcon(launcher, originalView, 633 (ItemInfo) originalView.getTag(), isOpening); 634 } 635 view.setOriginalDrawableBackground(view.mIconLoadResult.btvDrawable); 636 } 637 resetIconLoadResult(); 638 639 // Match the position of the original view. 640 view.matchPositionOf(launcher, originalView, isOpening, positionOut); 641 642 // We need to add it to the overlay, but keep it invisible until animation starts.. 643 view.setVisibility(View.INVISIBLE); 644 645 parent.addView(view); 646 dragLayer.addView(view.mListenerView); 647 view.mListenerView.setListener(view::fastFinish); 648 649 view.mEndRunnable = () -> { 650 view.mEndRunnable = null; 651 652 if (view.mFadeOutView != null) { 653 view.mFadeOutView.setAlpha(1f); 654 } 655 656 if (hideOriginal) { 657 view.updateViewsVisibility(true /* isVisible */); 658 view.finish(dragLayer); 659 } else { 660 view.finish(dragLayer); 661 } 662 }; 663 664 // Must be called after matchPositionOf so that we know what size to load. 665 // Must be called after the fastFinish listener and end runnable is created so that 666 // the icon is not left in a hidden state. 667 if (shouldLoadIcon) { 668 view.checkIconResult(); 669 } 670 671 return view; 672 } 673 updateViewsVisibility(boolean isVisible)674 private void updateViewsVisibility(boolean isVisible) { 675 if (mOriginalIcon != null) { 676 setIconAndDotVisible(mOriginalIcon, isVisible); 677 } 678 if (mMatchVisibilityView != null) { 679 setIconAndDotVisible(mMatchVisibilityView, isVisible); 680 } 681 } 682 finish(DragLayer dragLayer)683 private void finish(DragLayer dragLayer) { 684 ((ViewGroup) dragLayer.getParent()).removeView(this); 685 dragLayer.removeView(mListenerView); 686 recycle(); 687 mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this); 688 } 689 recycle()690 private void recycle() { 691 setTranslationX(0); 692 setTranslationY(0); 693 setScaleX(1); 694 setScaleY(1); 695 setAlpha(1); 696 if (mLoadIconSignal != null) { 697 mLoadIconSignal.cancel(); 698 } 699 mLoadIconSignal = null; 700 mEndRunnable = null; 701 mFinalDrawableBounds.setEmpty(); 702 mIsOpening = false; 703 mPositionOut = null; 704 mListenerView.setListener(null); 705 mOriginalIcon = null; 706 mOnTargetChangeRunnable = null; 707 mBadge = null; 708 sRecycledFetchIconId = sFetchIconId; 709 mIconLoadResult = null; 710 mClipIconView.recycle(); 711 mBtvDrawable.setBackground(null); 712 mFastFinishRunnable = null; 713 mIconOffsetY = 0; 714 mMatchVisibilityView = null; 715 mFadeOutView = null; 716 } 717 718 private static class IconLoadResult { 719 final ItemInfo itemInfo; 720 final boolean isThemed; 721 Supplier<Drawable> btvDrawable; 722 Drawable drawable; 723 Drawable badge; 724 int iconOffset; 725 Runnable onIconLoaded; 726 boolean isIconLoaded; 727 IconLoadResult(ItemInfo itemInfo, boolean isThemed)728 IconLoadResult(ItemInfo itemInfo, boolean isThemed) { 729 this.itemInfo = itemInfo; 730 this.isThemed = isThemed; 731 } 732 } 733 } 734