• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.FloatingIconViewCompanion.setPropertiesVisible;
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.IconNormalizer;
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.15 respectively.
179             // This value minimizes view display time while still allowing the view to fade out.
180             mFadeOutView.setAlpha(1 - Math.min(1f, mapToRange(progress, 0, 0.15f, 0, 1, LINEAR)));
181         }
182     }
183 
184     /**
185      * Sets a {@link com.android.quickstep.views.TaskView} that will draw a
186      * {@link com.android.quickstep.views.TaskView} within the {@code mClipIconView} clip bounds
187      */
setOverlayArtist(ClipIconView.TaskViewArtist taskViewArtist)188     public void setOverlayArtist(ClipIconView.TaskViewArtist taskViewArtist) {
189         mClipIconView.setTaskViewArtist(taskViewArtist);
190     }
191 
192     @Override
onAnimationEnd(Animator animator)193     public void onAnimationEnd(Animator animator) {
194         if (mLoadIconSignal != null) {
195             mLoadIconSignal.cancel();
196         }
197         if (mEndRunnable != null) {
198             mEndRunnable.run();
199         } else {
200             // End runnable also ends the reveal animator, so we manually handle it here.
201             mClipIconView.endReveal();
202         }
203     }
204 
205     /**
206      * Sets the size and position of this view to match {@code v}.
207      * <p>
208      * @param v The view to copy
209      * @param positionOut Rect that will hold the size and position of v.
210      */
matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut)211     private void matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut) {
212         getLocationBoundsForView(launcher, v, isOpening, positionOut);
213         final InsettableFrameLayout.LayoutParams lp = new InsettableFrameLayout.LayoutParams(
214                 Math.round(positionOut.width()),
215                 Math.round(positionOut.height()));
216         updatePosition(positionOut, lp);
217         setLayoutParams(lp);
218 
219         // For code simplicity, we always layout the child views using Gravity.LEFT
220         // and manually handle RTL for FloatingIconView when positioning it on the screen.
221         mClipIconView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height, LEFT));
222         mBtvDrawable.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height, LEFT));
223     }
224 
updatePosition(RectF pos, InsettableFrameLayout.LayoutParams lp)225     private void updatePosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
226         mPositionOut.set(pos);
227         lp.ignoreInsets = true;
228         // Position the floating view exactly on top of the original
229         lp.topMargin = Math.round(pos.top);
230         if (mIsRtl) {
231             lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - pos.right));
232         } else {
233             lp.setMarginStart(Math.round(pos.left));
234         }
235         // Set the properties here already to make sure they are available when running the first
236         // animation frame.
237         int left = mIsRtl
238                 ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
239                 : lp.leftMargin;
240         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
241     }
242 
getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, RectF outRect)243     private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
244             RectF outRect) {
245         getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect());
246     }
247 
248     /**
249      * Gets the location bounds of a view and returns the overall rotation.
250      * - For DeepShortcutView, we return the bounds of the icon view.
251      * - For BubbleTextView, we return the icon bounds.
252      */
getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, RectF outRect, Rect outViewBounds)253     public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
254             RectF outRect, Rect outViewBounds) {
255         boolean ignoreTransform = !isOpening;
256         if (v instanceof DeepShortcutView dsv) {
257             v = dsv.getIconView();
258             ignoreTransform = false;
259         } else if (v.getParent() instanceof DeepShortcutView dsv) {
260             v = dsv.getIconView();
261             ignoreTransform = false;
262         } else if (v instanceof BubbleTextHolder bth) {
263             v = bth.getBubbleText();
264             ignoreTransform = false;
265         }
266         if (v == null) {
267             return;
268         }
269 
270         if (v instanceof BubbleTextView) {
271             ((BubbleTextView) v).getIconBounds(outViewBounds);
272         } else if (v instanceof FolderIcon) {
273             ((FolderIcon) v).getPreviewBounds(outViewBounds);
274         } else {
275             outViewBounds.set(0, 0, v.getWidth(), v.getHeight());
276         }
277 
278         Utilities.getBoundsForViewInDragLayer(launcher.getDragLayer(), v, outViewBounds,
279                 ignoreTransform, null /** recycle */, outRect);
280     }
281 
282     /**
283      * Loads the icon and saves the results to {@link #sIconLoadResult}.
284      * <p>
285      * Runs onIconLoaded callback (if any), which signifies that the FloatingIconView is
286      * ready to display the icon. Otherwise, the FloatingIconView will grab the results when its
287      * initialized.
288      * <p>
289      * @param originalView The View that the FloatingIconView will replace.
290      * @param info ItemInfo of the originalView
291      * @param pos The position of the view.
292      * @param btvIcon The drawable of the BubbleTextView. May be null if original view is not a BTV
293      * @param outIconLoadResult We store the icon results into this object.
294      */
295     @WorkerThread
296     @SuppressWarnings("WrongThread")
getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos, @Nullable Drawable btvIcon, IconLoadResult outIconLoadResult)297     private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos,
298             @Nullable Drawable btvIcon, IconLoadResult outIconLoadResult) {
299         Drawable drawable;
300         boolean supportsAdaptiveIcons = !info.isDisabled(); // Use original icon for disabled icons.
301 
302         Drawable badge = null;
303         if (info instanceof SystemShortcut) {
304             if (originalView instanceof ImageView iv) {
305                 drawable = iv.getDrawable();
306             } else if (originalView instanceof DeepShortcutView dsv) {
307                 drawable = dsv.getIconView().getBackground();
308             } else {
309                 drawable = originalView.getBackground();
310             }
311         } else if (btvIcon instanceof PreloadIconDrawable) {
312             // Force the progress bar to display.
313             drawable = btvIcon;
314         } else if (originalView instanceof ImageView) {
315             drawable = ((ImageView) originalView).getDrawable();
316         } else {
317             int width = (int) pos.width();
318             int height = (int) pos.height();
319             Pair<AdaptiveIconDrawable, Drawable> fullIcon = null;
320             if (supportsAdaptiveIcons) {
321                 boolean shouldThemeIcon = (btvIcon instanceof FastBitmapDrawable fbd)
322                         && fbd.isCreatedForTheme();
323                 fullIcon = getFullDrawable(l, info, width, height, shouldThemeIcon);
324             } else if (!(originalView instanceof BubbleTextView)) {
325                 fullIcon = getFullDrawable(l, info, width, height, true /* shouldThemeIcon */);
326             }
327 
328             if (fullIcon != null) {
329                 drawable = fullIcon.first;
330                 badge = fullIcon.second;
331             } else {
332                 drawable = btvIcon;
333             }
334         }
335 
336         drawable = drawable == null ? null : drawable.getConstantState().newDrawable();
337         int iconOffset = getOffsetForIconBounds(l, drawable, pos);
338         // Clone right away as we are on the background thread instead of blocking the
339         // main thread later
340         Drawable btvClone = btvIcon == null ? null : btvIcon.getConstantState().newDrawable();
341         synchronized (outIconLoadResult) {
342             outIconLoadResult.btvDrawable = () -> btvClone;
343             outIconLoadResult.drawable = drawable;
344             outIconLoadResult.badge = badge;
345             outIconLoadResult.iconOffset = iconOffset;
346             if (outIconLoadResult.onIconLoaded != null) {
347                 l.getMainExecutor().execute(outIconLoadResult.onIconLoaded);
348                 outIconLoadResult.onIconLoaded = null;
349             }
350             outIconLoadResult.isIconLoaded = true;
351         }
352     }
353 
354     /**
355      * Sets the drawables of the {@code originalView} onto this view.
356      * <p>
357      * @param drawable The drawable of the original view.
358      * @param badge The badge of the original view.
359      * @param iconOffset The amount of offset needed to match this view with the original view.
360      */
361     @UiThread
setIcon(@ullable Drawable drawable, @Nullable Drawable badge, @Nullable Supplier<Drawable> btvIcon, int iconOffset)362     private void setIcon(@Nullable Drawable drawable, @Nullable Drawable badge,
363             @Nullable Supplier<Drawable> btvIcon, int iconOffset) {
364         final DeviceProfile dp = mLauncher.getDeviceProfile();
365         final InsettableFrameLayout.LayoutParams lp =
366                 (InsettableFrameLayout.LayoutParams) getLayoutParams();
367         mBadge = badge;
368         mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, dp);
369         if (drawable instanceof AdaptiveIconDrawable) {
370             final int originalHeight = lp.height;
371             final int originalWidth = lp.width;
372 
373             mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
374 
375             float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
376             if (dp.isLandscape) {
377                 lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
378             } else {
379                 lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
380             }
381             setLayoutParams(lp);
382 
383             final LayoutParams clipViewLp = (LayoutParams) mClipIconView.getLayoutParams();
384             if (mBadge != null) {
385                 Rect badgeBounds = new Rect(0, 0, clipViewLp.width, clipViewLp.height);
386                 FastBitmapDrawable.setBadgeBounds(mBadge, badgeBounds);
387             }
388             clipViewLp.width = lp.width;
389             clipViewLp.height = lp.height;
390             mClipIconView.setLayoutParams(clipViewLp);
391         }
392 
393         setOriginalDrawableBackground(btvIcon);
394         invalidate();
395     }
396 
397     /**
398      * Draws the drawable of the BubbleTextView behind ClipIconView
399      * <p>
400      * This is used to:
401      * - Have icon displayed while Adaptive Icon is loading
402      * - Displays the built in shadow to ensure a clean handoff
403      * <p>
404      * Allows nullable as this may be cleared when drawing is deferred to ClipIconView.
405      */
setOriginalDrawableBackground(@ullable Supplier<Drawable> btvIcon)406     private void setOriginalDrawableBackground(@Nullable Supplier<Drawable> btvIcon) {
407         if (!mIsOpening) {
408             mBtvDrawable.setBackground(btvIcon == null ? null : btvIcon.get());
409         }
410     }
411 
412     /**
413      * Returns true if the icon is different from main app icon
414      */
isDifferentFromAppIcon()415     public boolean isDifferentFromAppIcon() {
416         return mIconLoadResult == null ? false : mIconLoadResult.isThemed;
417     }
418 
419     /**
420      * Checks if the icon result is loaded. If true, we set the icon immediately. Else, we add a
421      * callback to set the icon once the icon result is loaded.
422      */
checkIconResult()423     private void checkIconResult() {
424         CancellationSignal cancellationSignal = new CancellationSignal();
425 
426         if (mIconLoadResult == null) {
427             Log.w(TAG, "No icon load result found in checkIconResult");
428             return;
429         }
430 
431         synchronized (mIconLoadResult) {
432             if (mIconLoadResult.isIconLoaded) {
433                 setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
434                         mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset);
435                 setVisibility(VISIBLE);
436                 updateViewsVisibility(false  /* isVisible */);
437             } else {
438                 mIconLoadResult.onIconLoaded = () -> {
439                     if (cancellationSignal.isCanceled()) {
440                         return;
441                     }
442 
443                     setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
444                             mIconLoadResult.btvDrawable, mIconLoadResult.iconOffset);
445 
446                     setVisibility(VISIBLE);
447                     updateViewsVisibility(false  /* isVisible */);
448                 };
449                 mLoadIconSignal = cancellationSignal;
450             }
451         }
452     }
453 
454     @WorkerThread
455     @SuppressWarnings("WrongThread")
getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position)456     private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
457         if (!(drawable instanceof AdaptiveIconDrawable)) {
458             return 0;
459         }
460         int blurSizeOutline =
461                 l.getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
462 
463         Rect bounds = new Rect(0, 0, (int) position.width() + blurSizeOutline,
464                 (int) position.height() + blurSizeOutline);
465         bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2);
466         Utilities.scaleRectAboutCenter(bounds, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
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         if (mFadeOutView instanceof FloatingIconViewCompanion fivc) {
519             fivc.setForceHideDot(true);
520             fivc.setForceHideRing(true);
521         }
522     }
523 
524     @Override
onAnimationCancel(Animator animator)525     public void onAnimationCancel(Animator animator) {}
526 
527     @Override
onAnimationRepeat(Animator animator)528     public void onAnimationRepeat(Animator animator) {}
529 
530     @Override
setPositionOffsetY(float y)531     public void setPositionOffsetY(float y) {
532         mIconOffsetY = y;
533         onGlobalLayout();
534     }
535 
536     @Override
onGlobalLayout()537     public void onGlobalLayout() {
538         if (mOriginalIcon != null && mOriginalIcon.isAttachedToWindow() && mPositionOut != null) {
539             getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening, sTmpRectF);
540             sTmpRectF.offset(0, mIconOffsetY);
541             if (!sTmpRectF.equals(mPositionOut)) {
542                 updatePosition(sTmpRectF, (InsettableFrameLayout.LayoutParams) getLayoutParams());
543                 if (mOnTargetChangeRunnable != null) {
544                     mOnTargetChangeRunnable.run();
545                 }
546             }
547         }
548     }
549 
setOnTargetChangeListener(Runnable onTargetChangeListener)550     public void setOnTargetChangeListener(Runnable onTargetChangeListener) {
551         mOnTargetChangeRunnable = onTargetChangeListener;
552     }
553 
554     /**
555      * Loads the icon drawable on a worker thread to reduce latency between swapping views.
556      */
557     @UiThread
fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening)558     public static IconLoadResult fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening) {
559         RectF position = new RectF();
560         getLocationBoundsForView(l, v, isOpening, position);
561 
562         final FastBitmapDrawable btvIcon;
563         final Supplier<Drawable> btvDrawableSupplier;
564         if (v instanceof BubbleTextView) {
565             BubbleTextView btv = (BubbleTextView) v;
566             if (info instanceof ItemInfoWithIcon
567                     && (((ItemInfoWithIcon) info).runtimeStatusFlags
568                     & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
569                 btvIcon = btv.makePreloadIcon();
570                 btvDrawableSupplier = () -> btvIcon;
571             } else {
572                 btvIcon = btv.getIcon();
573                 // Clone when needed
574                 btvDrawableSupplier = () -> btvIcon.getConstantState().newDrawable();
575             }
576         } else {
577             btvIcon = null;
578             btvDrawableSupplier = null;
579         }
580 
581         IconLoadResult result = new IconLoadResult(info, btvIcon != null && btvIcon.isThemed());
582         result.btvDrawable = btvDrawableSupplier;
583 
584         final long fetchIconId = sFetchIconId++;
585         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
586             if (fetchIconId < sRecycledFetchIconId) {
587                 return;
588             }
589             getIconResult(l, v, info, position, btvIcon, result);
590         });
591 
592         sIconLoadResult = result;
593         return result;
594     }
595 
596     /**
597      * Resets the static icon load result used for preloading the icon for a launching app.
598      */
resetIconLoadResult()599     public static void resetIconLoadResult() {
600         sIconLoadResult = null;
601     }
602 
603     /**
604      * Creates a floating icon view for {@code originalView}.
605      * <p>
606      * @param originalView The view to copy
607      * @param visibilitySyncView A view whose visibility should update in sync with originalView.
608      * @param fadeOutView A view that will fade out as the animation progresses.
609      * @param hideOriginal If true, it will hide {@code originalView} while this view is visible.
610      *                     Else, we will not draw anything in this view.
611      * @param positionOut Rect that will hold the size and position of v.
612      * @param isOpening True if this view replaces the icon for app open animation.
613      */
getFloatingIconView(Launcher launcher, View originalView, @Nullable View visibilitySyncView, @Nullable View fadeOutView, boolean hideOriginal, RectF positionOut, boolean isOpening)614     public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView,
615             @Nullable View visibilitySyncView, @Nullable View fadeOutView, boolean hideOriginal,
616             RectF positionOut, boolean isOpening) {
617         final DragLayer dragLayer = launcher.getDragLayer();
618         ViewGroup parent = (ViewGroup) dragLayer.getParent();
619         FloatingIconView view = launcher.getViewCache().getView(R.layout.floating_icon_view,
620                 launcher, parent);
621         view.recycle();
622 
623         // Init properties before getting the drawable.
624         view.mIsOpening = isOpening;
625         view.mOriginalIcon = originalView;
626         view.mMatchVisibilityView = visibilitySyncView;
627         view.mFadeOutView = fadeOutView;
628         view.mPositionOut = positionOut;
629 
630         // Get the drawable on the background thread
631         boolean shouldLoadIcon = originalView.getTag() instanceof ItemInfo && hideOriginal;
632         if (shouldLoadIcon) {
633             if (sIconLoadResult != null && sIconLoadResult.itemInfo == originalView.getTag()) {
634                 view.mIconLoadResult = sIconLoadResult;
635             } else {
636                 view.mIconLoadResult = fetchIcon(launcher, originalView,
637                         (ItemInfo) originalView.getTag(), isOpening);
638             }
639             view.setOriginalDrawableBackground(view.mIconLoadResult.btvDrawable);
640         }
641         resetIconLoadResult();
642 
643         // Match the position of the original view.
644         view.matchPositionOf(launcher, originalView, isOpening, positionOut);
645 
646         // We need to add it to the overlay, but keep it invisible until animation starts..
647         view.setVisibility(View.INVISIBLE);
648 
649         parent.addView(view);
650         dragLayer.addView(view.mListenerView);
651         view.mListenerView.setListener(view::fastFinish);
652 
653         view.mEndRunnable = () -> {
654             view.mEndRunnable = null;
655 
656             if (view.mFadeOutView != null) {
657                 view.mFadeOutView.setAlpha(1f);
658             }
659             if (view.mFadeOutView instanceof FloatingIconViewCompanion fivc) {
660                 fivc.setForceHideDot(false);
661                 fivc.setForceHideRing(false);
662             }
663 
664             if (hideOriginal) {
665                 view.updateViewsVisibility(true /* isVisible */);
666                 view.finish(dragLayer);
667             } else {
668                 view.finish(dragLayer);
669             }
670         };
671 
672         // Must be called after matchPositionOf so that we know what size to load.
673         // Must be called after the fastFinish listener and end runnable is created so that
674         // the icon is not left in a hidden state.
675         if (shouldLoadIcon) {
676             view.checkIconResult();
677         }
678 
679         return view;
680     }
681 
updateViewsVisibility(boolean isVisible)682     private void updateViewsVisibility(boolean isVisible) {
683         if (mOriginalIcon != null) {
684             setPropertiesVisible(mOriginalIcon, isVisible);
685         }
686         if (mMatchVisibilityView != null) {
687             setPropertiesVisible(mMatchVisibilityView, isVisible);
688         }
689     }
690 
finish(DragLayer dragLayer)691     private void finish(DragLayer dragLayer) {
692         ((ViewGroup) dragLayer.getParent()).removeView(this);
693         dragLayer.removeView(mListenerView);
694         recycle();
695         mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this);
696     }
697 
recycle()698     private void recycle() {
699         setTranslationX(0);
700         setTranslationY(0);
701         setScaleX(1);
702         setScaleY(1);
703         setAlpha(1);
704         if (mLoadIconSignal != null) {
705             mLoadIconSignal.cancel();
706         }
707         mLoadIconSignal = null;
708         mEndRunnable = null;
709         mFinalDrawableBounds.setEmpty();
710         mIsOpening = false;
711         mPositionOut = null;
712         mListenerView.setListener(null);
713         mOriginalIcon = null;
714         mOnTargetChangeRunnable = null;
715         mBadge = null;
716         sRecycledFetchIconId = sFetchIconId;
717         mIconLoadResult = null;
718         mClipIconView.recycle();
719         mBtvDrawable.setBackground(null);
720         mFastFinishRunnable = null;
721         mIconOffsetY = 0;
722         mMatchVisibilityView = null;
723         mFadeOutView = null;
724     }
725 
726     private static class IconLoadResult {
727         final ItemInfo itemInfo;
728         final boolean isThemed;
729         Supplier<Drawable> btvDrawable;
730         Drawable drawable;
731         Drawable badge;
732         int iconOffset;
733         Runnable onIconLoaded;
734         boolean isIconLoaded;
735 
IconLoadResult(ItemInfo itemInfo, boolean isThemed)736         IconLoadResult(ItemInfo itemInfo, boolean isThemed) {
737             this.itemInfo = itemInfo;
738             this.isThemed = isThemed;
739         }
740     }
741 }
742