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