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