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