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