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.LauncherAnimUtils.DRAWABLE_ALPHA;
19 import static com.android.launcher3.Utilities.getBadge;
20 import static com.android.launcher3.Utilities.getFullDrawable;
21 import static com.android.launcher3.Utilities.mapToRange;
22 import static com.android.launcher3.anim.Interpolators.LINEAR;
23 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
24 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
25 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
26 
27 import android.animation.Animator;
28 import android.animation.AnimatorListenerAdapter;
29 import android.animation.AnimatorSet;
30 import android.animation.ObjectAnimator;
31 import android.animation.ValueAnimator;
32 import android.annotation.TargetApi;
33 import android.content.Context;
34 import android.graphics.Canvas;
35 import android.graphics.Color;
36 import android.graphics.Outline;
37 import android.graphics.Path;
38 import android.graphics.Rect;
39 import android.graphics.RectF;
40 import android.graphics.drawable.AdaptiveIconDrawable;
41 import android.graphics.drawable.ColorDrawable;
42 import android.graphics.drawable.Drawable;
43 import android.os.Build;
44 import android.os.CancellationSignal;
45 import android.util.AttributeSet;
46 import android.util.Log;
47 import android.view.View;
48 import android.view.ViewGroup;
49 import android.view.ViewOutlineProvider;
50 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
51 import android.widget.ImageView;
52 
53 import androidx.annotation.Nullable;
54 import androidx.annotation.UiThread;
55 import androidx.annotation.WorkerThread;
56 import androidx.dynamicanimation.animation.FloatPropertyCompat;
57 import androidx.dynamicanimation.animation.SpringAnimation;
58 import androidx.dynamicanimation.animation.SpringForce;
59 
60 import com.android.launcher3.BubbleTextView;
61 import com.android.launcher3.InsettableFrameLayout.LayoutParams;
62 import com.android.launcher3.ItemInfo;
63 import com.android.launcher3.Launcher;
64 import com.android.launcher3.R;
65 import com.android.launcher3.Utilities;
66 import com.android.launcher3.dragndrop.DragLayer;
67 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
68 import com.android.launcher3.folder.FolderIcon;
69 import com.android.launcher3.graphics.IconShape;
70 import com.android.launcher3.graphics.ShiftedBitmapDrawable;
71 import com.android.launcher3.icons.LauncherIcons;
72 import com.android.launcher3.popup.SystemShortcut;
73 import com.android.launcher3.shortcuts.DeepShortcutView;
74 
75 /**
76  * A view that is created to look like another view with the purpose of creating fluid animations.
77  */
78 @TargetApi(Build.VERSION_CODES.Q)
79 public class FloatingIconView extends View implements
80         Animator.AnimatorListener, ClipPathView, OnGlobalLayoutListener {
81 
82     private static final String TAG = FloatingIconView.class.getSimpleName();
83 
84     // Manages loading the icon on a worker thread
85     private static @Nullable IconLoadResult sIconLoadResult;
86 
87     public static final float SHAPE_PROGRESS_DURATION = 0.10f;
88     private static final int FADE_DURATION_MS = 200;
89     private static final Rect sTmpRect = new Rect();
90     private static final RectF sTmpRectF = new RectF();
91     private static final Object[] sTmpObjArray = new Object[1];
92 
93     // We spring the foreground drawable relative to the icon's movement in the DragLayer.
94     // We then use these two factor values to scale the movement of the fg within this view.
95     private static final int FG_TRANS_X_FACTOR = 60;
96     private static final int FG_TRANS_Y_FACTOR = 75;
97 
98     private static final FloatPropertyCompat<FloatingIconView> mFgTransYProperty
99             = new FloatPropertyCompat<FloatingIconView>("FloatingViewFgTransY") {
100         @Override
101         public float getValue(FloatingIconView view) {
102             return view.mFgTransY;
103         }
104 
105         @Override
106         public void setValue(FloatingIconView view, float transY) {
107             view.mFgTransY = transY;
108             view.invalidate();
109         }
110     };
111 
112     private static final FloatPropertyCompat<FloatingIconView> mFgTransXProperty
113             = new FloatPropertyCompat<FloatingIconView>("FloatingViewFgTransX") {
114         @Override
115         public float getValue(FloatingIconView view) {
116             return view.mFgTransX;
117         }
118 
119         @Override
120         public void setValue(FloatingIconView view, float transX) {
121             view.mFgTransX = transX;
122             view.invalidate();
123         }
124     };
125 
126     private Runnable mEndRunnable;
127     private CancellationSignal mLoadIconSignal;
128 
129     private final Launcher mLauncher;
130     private final int mBlurSizeOutline;
131     private final boolean mIsRtl;
132 
133     private boolean mIsVerticalBarLayout = false;
134     private boolean mIsAdaptiveIcon = false;
135     private boolean mIsOpening;
136 
137     private IconLoadResult mIconLoadResult;
138 
139     private @Nullable Drawable mBadge;
140     private @Nullable Drawable mForeground;
141     private @Nullable Drawable mBackground;
142     private float mRotation;
143     private ValueAnimator mRevealAnimator;
144     private final Rect mStartRevealRect = new Rect();
145     private final Rect mEndRevealRect = new Rect();
146     private Path mClipPath;
147     private float mTaskCornerRadius;
148 
149     private View mOriginalIcon;
150     private RectF mPositionOut;
151     private Runnable mOnTargetChangeRunnable;
152 
153     private final Rect mOutline = new Rect();
154     private final Rect mFinalDrawableBounds = new Rect();
155 
156     private AnimatorSet mFadeAnimatorSet;
157     private ListenerView mListenerView;
158 
159     private final SpringAnimation mFgSpringY;
160     private float mFgTransY;
161     private final SpringAnimation mFgSpringX;
162     private float mFgTransX;
163 
FloatingIconView(Context context)164     public FloatingIconView(Context context) {
165         this(context, null);
166     }
167 
FloatingIconView(Context context, AttributeSet attrs)168     public FloatingIconView(Context context, AttributeSet attrs) {
169         this(context, attrs, 0);
170     }
171 
FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr)172     public FloatingIconView(Context context, AttributeSet attrs, int defStyleAttr) {
173         super(context, attrs, defStyleAttr);
174         mLauncher = Launcher.getLauncher(context);
175         mBlurSizeOutline = getResources().getDimensionPixelSize(
176                 R.dimen.blur_size_medium_outline);
177         mIsRtl = Utilities.isRtl(getResources());
178         mListenerView = new ListenerView(context, attrs);
179 
180         mFgSpringX = new SpringAnimation(this, mFgTransXProperty)
181                 .setSpring(new SpringForce()
182                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
183                 .setStiffness(SpringForce.STIFFNESS_LOW));
184         mFgSpringY = new SpringAnimation(this, mFgTransYProperty)
185                 .setSpring(new SpringForce()
186                         .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
187                         .setStiffness(SpringForce.STIFFNESS_LOW));
188     }
189 
190     @Override
onAttachedToWindow()191     protected void onAttachedToWindow() {
192         super.onAttachedToWindow();
193         if (!mIsOpening) {
194             getViewTreeObserver().addOnGlobalLayoutListener(this);
195             mLauncher.getRotationHelper().setCurrentTransitionRequest(REQUEST_LOCK);
196         }
197     }
198 
199     @Override
onDetachedFromWindow()200     protected void onDetachedFromWindow() {
201         getViewTreeObserver().removeOnGlobalLayoutListener(this);
202         super.onDetachedFromWindow();
203     }
204 
205     /**
206      * Positions this view to match the size and location of {@param rect}.
207      * @param alpha The alpha to set this view.
208      * @param progress A value from [0, 1] that represents the animation progress.
209      * @param shapeProgressStart The progress value at which to start the shape reveal.
210      * @param cornerRadius The corner radius of {@param rect}.
211      */
update(RectF rect, float alpha, float progress, float shapeProgressStart, float cornerRadius, boolean isOpening)212     public void update(RectF rect, float alpha, float progress, float shapeProgressStart,
213             float cornerRadius, boolean isOpening) {
214         setAlpha(alpha);
215 
216         LayoutParams lp = (LayoutParams) getLayoutParams();
217         float dX = mIsRtl
218                 ? rect.left
219                 - (mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width)
220                 : rect.left - lp.getMarginStart();
221         float dY = rect.top - lp.topMargin;
222         setTranslationX(dX);
223         setTranslationY(dY);
224 
225         float minSize = Math.min(lp.width, lp.height);
226         float scaleX = rect.width() / minSize;
227         float scaleY = rect.height() / minSize;
228         float scale = Math.max(1f, Math.min(scaleX, scaleY));
229 
230         setPivotX(0);
231         setPivotY(0);
232         setScaleX(scale);
233         setScaleY(scale);
234 
235         // shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION
236         float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f;
237         float shapeRevealProgress = Utilities.boundToRange(mapToRange(
238                 Math.max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax,
239                 LINEAR), 0, 1);
240 
241         if (mIsVerticalBarLayout) {
242             mOutline.right = (int) (rect.width() / scale);
243         } else {
244             mOutline.bottom = (int) (rect.height() / scale);
245         }
246 
247         mTaskCornerRadius = cornerRadius / scale;
248         if (mIsAdaptiveIcon) {
249             if (!isOpening && progress >= shapeProgressStart) {
250                 if (mRevealAnimator == null) {
251                     mRevealAnimator = (ValueAnimator) IconShape.getShape().createRevealAnimator(
252                             this, mStartRevealRect, mOutline, mTaskCornerRadius, !isOpening);
253                     mRevealAnimator.addListener(new AnimatorListenerAdapter() {
254                         @Override
255                         public void onAnimationEnd(Animator animation) {
256                             mRevealAnimator = null;
257                         }
258                     });
259                     mRevealAnimator.start();
260                     // We pause here so we can set the current fraction ourselves.
261                     mRevealAnimator.pause();
262                 }
263                 mRevealAnimator.setCurrentFraction(shapeRevealProgress);
264             }
265 
266             float drawableScale = (mIsVerticalBarLayout ? mOutline.width() : mOutline.height())
267                     / minSize;
268             setBackgroundDrawableBounds(drawableScale);
269             if (isOpening) {
270                 // Center align foreground
271                 int height = mFinalDrawableBounds.height();
272                 int width = mFinalDrawableBounds.width();
273                 int diffY = mIsVerticalBarLayout ? 0
274                         : (int) (((height * drawableScale) - height) / 2);
275                 int diffX = mIsVerticalBarLayout ? (int) (((width * drawableScale) - width) / 2)
276                         : 0;
277                 sTmpRect.set(mFinalDrawableBounds);
278                 sTmpRect.offset(diffX, diffY);
279                 mForeground.setBounds(sTmpRect);
280             } else {
281                 // Spring the foreground relative to the icon's movement within the DragLayer.
282                 int diffX = (int) (dX / mLauncher.getDeviceProfile().availableWidthPx
283                         * FG_TRANS_X_FACTOR);
284                 int diffY = (int) (dY / mLauncher.getDeviceProfile().availableHeightPx
285                         * FG_TRANS_Y_FACTOR);
286 
287                 mFgSpringX.animateToFinalPosition(diffX);
288                 mFgSpringY.animateToFinalPosition(diffY);
289             }
290         }
291         invalidate();
292         invalidateOutline();
293     }
294 
295     @Override
onAnimationEnd(Animator animator)296     public void onAnimationEnd(Animator animator) {
297         if (mLoadIconSignal != null) {
298             mLoadIconSignal.cancel();
299         }
300         if (mEndRunnable != null) {
301             mEndRunnable.run();
302         } else {
303             // End runnable also ends the reveal animator, so we manually handle it here.
304             if (mRevealAnimator != null) {
305                 mRevealAnimator.end();
306             }
307         }
308     }
309 
310     /**
311      * Sets the size and position of this view to match {@param v}.
312      *
313      * @param v The view to copy
314      * @param positionOut Rect that will hold the size and position of v.
315      */
matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut)316     private void matchPositionOf(Launcher launcher, View v, boolean isOpening, RectF positionOut) {
317         float rotation = getLocationBoundsForView(launcher, v, isOpening, positionOut);
318         final LayoutParams lp = new LayoutParams(
319                 Math.round(positionOut.width()),
320                 Math.round(positionOut.height()));
321         updatePosition(rotation, positionOut, lp);
322         setLayoutParams(lp);
323     }
324 
updatePosition(float rotation, RectF position, LayoutParams lp)325     private void updatePosition(float rotation, RectF position, LayoutParams lp) {
326         mRotation = rotation;
327         mPositionOut.set(position);
328         lp.ignoreInsets = true;
329         // Position the floating view exactly on top of the original
330         lp.topMargin = Math.round(position.top);
331         if (mIsRtl) {
332             lp.setMarginStart(Math.round(mLauncher.getDeviceProfile().widthPx - position.right));
333         } else {
334             lp.setMarginStart(Math.round(position.left));
335         }
336         // Set the properties here already to make sure they are available when running the first
337         // animation frame.
338         int left = mIsRtl
339                 ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
340                 : lp.leftMargin;
341         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
342     }
343 
344     /**
345      * Gets the location bounds of a view and returns the overall rotation.
346      * - For DeepShortcutView, we return the bounds of the icon view.
347      * - For BubbleTextView, we return the icon bounds.
348      */
getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, RectF outRect)349     private static float getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
350             RectF outRect) {
351         boolean ignoreTransform = !isOpening;
352         if (v instanceof DeepShortcutView) {
353             v = ((DeepShortcutView) v).getBubbleText();
354             ignoreTransform = false;
355         } else if (v.getParent() instanceof DeepShortcutView) {
356             v = ((DeepShortcutView) v.getParent()).getIconView();
357             ignoreTransform = false;
358         }
359         if (v == null) {
360             return 0;
361         }
362 
363         Rect iconBounds = new Rect();
364         if (v instanceof BubbleTextView) {
365             ((BubbleTextView) v).getIconBounds(iconBounds);
366         } else if (v instanceof FolderIcon) {
367             ((FolderIcon) v).getPreviewBounds(iconBounds);
368         } else {
369             iconBounds.set(0, 0, v.getWidth(), v.getHeight());
370         }
371 
372         float[] points = new float[] {iconBounds.left, iconBounds.top, iconBounds.right,
373                 iconBounds.bottom};
374         float[] rotation = new float[] {0};
375         Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points,
376                 false, ignoreTransform, rotation);
377         outRect.set(
378                 Math.min(points[0], points[2]),
379                 Math.min(points[1], points[3]),
380                 Math.max(points[0], points[2]),
381                 Math.max(points[1], points[3]));
382         return rotation[0];
383     }
384 
385     /**
386      * Loads the icon and saves the results to {@link #sIconLoadResult}.
387      * Runs onIconLoaded callback (if any), which signifies that the FloatingIconView is
388      * ready to display the icon. Otherwise, the FloatingIconView will grab the results when its
389      * initialized.
390      *
391      * @param originalView The View that the FloatingIconView will replace.
392      * @param info ItemInfo of the originalView
393      * @param pos The position of the view.
394      */
395     @WorkerThread
396     @SuppressWarnings("WrongThread")
getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos, IconLoadResult iconLoadResult)397     private static void getIconResult(Launcher l, View originalView, ItemInfo info, RectF pos,
398             IconLoadResult iconLoadResult) {
399         Drawable drawable = null;
400         Drawable badge = null;
401         boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get()
402                 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
403         Drawable btvIcon = originalView instanceof BubbleTextView
404                 ? ((BubbleTextView) originalView).getIcon() : null;
405         if (info instanceof SystemShortcut) {
406             if (originalView instanceof ImageView) {
407                 drawable = ((ImageView) originalView).getDrawable();
408             } else if (originalView instanceof DeepShortcutView) {
409                 drawable = ((DeepShortcutView) originalView).getIconView().getBackground();
410             } else {
411                 drawable = originalView.getBackground();
412             }
413         } else {
414             boolean isFolderIcon = originalView instanceof FolderIcon;
415             int width = isFolderIcon ? originalView.getWidth() : (int) pos.width();
416             int height = isFolderIcon ? originalView.getHeight() : (int) pos.height();
417             if (supportsAdaptiveIcons) {
418                 drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray);
419                 if (drawable instanceof AdaptiveIconDrawable) {
420                     badge = getBadge(l, info, sTmpObjArray[0]);
421                 } else {
422                     // The drawable we get back is not an adaptive icon, so we need to use the
423                     // BubbleTextView icon that is already legacy treated.
424                     drawable = btvIcon;
425                 }
426             } else {
427                 if (originalView instanceof BubbleTextView) {
428                     // Similar to DragView, we simply use the BubbleTextView icon here.
429                     drawable = btvIcon;
430                 } else {
431                     drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray);
432                 }
433             }
434         }
435 
436         drawable = drawable == null ? null : drawable.getConstantState().newDrawable();
437         int iconOffset = getOffsetForIconBounds(l, drawable, pos);
438         synchronized (iconLoadResult) {
439             iconLoadResult.drawable = drawable;
440             iconLoadResult.badge = badge;
441             iconLoadResult.iconOffset = iconOffset;
442             if (iconLoadResult.onIconLoaded != null) {
443                 l.getMainExecutor().execute(iconLoadResult.onIconLoaded);
444                 iconLoadResult.onIconLoaded = null;
445             }
446             iconLoadResult.isIconLoaded = true;
447         }
448     }
449 
450     /**
451      * Sets the drawables of the {@param originalView} onto this view.
452      *
453      * @param originalView The View that the FloatingIconView will replace.
454      * @param drawable The drawable of the original view.
455      * @param badge The badge of the original view.
456      * @param iconOffset The amount of offset needed to match this view with the original view.
457      */
458     @UiThread
setIcon(View originalView, @Nullable Drawable drawable, @Nullable Drawable badge, int iconOffset)459     private void setIcon(View originalView, @Nullable Drawable drawable, @Nullable Drawable badge,
460             int iconOffset) {
461         mBadge = badge;
462 
463         mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
464         if (mIsAdaptiveIcon) {
465             boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
466 
467             AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable;
468             Drawable background = adaptiveIcon.getBackground();
469             if (background == null) {
470                 background = new ColorDrawable(Color.TRANSPARENT);
471             }
472             mBackground = background;
473             Drawable foreground = adaptiveIcon.getForeground();
474             if (foreground == null) {
475                 foreground = new ColorDrawable(Color.TRANSPARENT);
476             }
477             mForeground = foreground;
478 
479             final LayoutParams lp = (LayoutParams) getLayoutParams();
480             final int originalHeight = lp.height;
481             final int originalWidth = lp.width;
482 
483             int blurMargin = mBlurSizeOutline / 2;
484             mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
485 
486             if (!isFolderIcon) {
487                 mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin);
488             }
489             mForeground.setBounds(mFinalDrawableBounds);
490             mBackground.setBounds(mFinalDrawableBounds);
491 
492             mStartRevealRect.set(0, 0, originalWidth, originalHeight);
493 
494             if (mBadge != null) {
495                 mBadge.setBounds(mStartRevealRect);
496                 if (!mIsOpening && !isFolderIcon) {
497                     DRAWABLE_ALPHA.set(mBadge, 0);
498                 }
499             }
500 
501             if (isFolderIcon) {
502                 ((FolderIcon) originalView).getPreviewBounds(sTmpRect);
503                 float bgStroke = ((FolderIcon) originalView).getBackgroundStrokeWidth();
504                 if (mForeground instanceof ShiftedBitmapDrawable) {
505                     ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mForeground;
506                     sbd.setShiftX(sbd.getShiftX() - sTmpRect.left - bgStroke);
507                     sbd.setShiftY(sbd.getShiftY() - sTmpRect.top - bgStroke);
508                 }
509                 if (mBadge instanceof ShiftedBitmapDrawable) {
510                     ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mBadge;
511                     sbd.setShiftX(sbd.getShiftX() - sTmpRect.left - bgStroke);
512                     sbd.setShiftY(sbd.getShiftY() - sTmpRect.top - bgStroke);
513                 }
514             } else {
515                 Utilities.scaleRectAboutCenter(mStartRevealRect,
516                         IconShape.getNormalizationScale());
517             }
518 
519             float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
520             if (mIsVerticalBarLayout) {
521                 lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
522             } else {
523                 lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
524             }
525 
526             int left = mIsRtl
527                     ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
528                     : lp.leftMargin;
529             layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
530 
531             float scale = Math.max((float) lp.height / originalHeight,
532                     (float) lp.width / originalWidth);
533             float bgDrawableStartScale;
534             if (mIsOpening) {
535                 bgDrawableStartScale = 1f;
536                 mOutline.set(0, 0, originalWidth, originalHeight);
537             } else {
538                 bgDrawableStartScale = scale;
539                 mOutline.set(0, 0, lp.width, lp.height);
540             }
541             setBackgroundDrawableBounds(bgDrawableStartScale);
542             mEndRevealRect.set(0, 0, lp.width, lp.height);
543             setOutlineProvider(new ViewOutlineProvider() {
544                 @Override
545                 public void getOutline(View view, Outline outline) {
546                     outline.setRoundRect(mOutline, mTaskCornerRadius);
547                 }
548             });
549             setClipToOutline(true);
550         } else {
551             setBackground(drawable);
552             setClipToOutline(false);
553         }
554 
555         invalidate();
556         invalidateOutline();
557     }
558 
559     /**
560      * Checks if the icon result is loaded. If true, we set the icon immediately. Else, we add a
561      * callback to set the icon once the icon result is loaded.
562      */
checkIconResult(View originalView)563     private void checkIconResult(View originalView) {
564         CancellationSignal cancellationSignal = new CancellationSignal();
565 
566         if (mIconLoadResult == null) {
567             Log.w(TAG, "No icon load result found in checkIconResult");
568             return;
569         }
570 
571         synchronized (mIconLoadResult) {
572             if (mIconLoadResult.isIconLoaded) {
573                 setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge,
574                         mIconLoadResult.iconOffset);
575                 hideOriginalView(originalView);
576             } else {
577                 mIconLoadResult.onIconLoaded = () -> {
578                     if (cancellationSignal.isCanceled()) {
579                         return;
580                     }
581 
582                     setIcon(originalView, mIconLoadResult.drawable, mIconLoadResult.badge,
583                             mIconLoadResult.iconOffset);
584                     setVisibility(VISIBLE);
585                     hideOriginalView(originalView);
586                 };
587                 mLoadIconSignal = cancellationSignal;
588             }
589         }
590     }
591 
hideOriginalView(View originalView)592     private void hideOriginalView(View originalView) {
593         if (originalView instanceof IconLabelDotView) {
594             ((IconLabelDotView) originalView).setIconVisible(false);
595             ((IconLabelDotView) originalView).setForceHideDot(true);
596         } else {
597             originalView.setVisibility(INVISIBLE);
598         }
599     }
600 
setBackgroundDrawableBounds(float scale)601     private void setBackgroundDrawableBounds(float scale) {
602         sTmpRect.set(mFinalDrawableBounds);
603         Utilities.scaleRectAboutCenter(sTmpRect, scale);
604         // Since the drawable is at the top of the view, we need to offset to keep it centered.
605         if (mIsVerticalBarLayout) {
606             sTmpRect.offsetTo((int) (mFinalDrawableBounds.left * scale), sTmpRect.top);
607         } else {
608             sTmpRect.offsetTo(sTmpRect.left, (int) (mFinalDrawableBounds.top * scale));
609         }
610         mBackground.setBounds(sTmpRect);
611     }
612 
613     @WorkerThread
614     @SuppressWarnings("WrongThread")
getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position)615     private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
616         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
617                 !(drawable instanceof AdaptiveIconDrawable)) {
618             return 0;
619         }
620         int blurSizeOutline =
621                 l.getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
622 
623         Rect bounds = new Rect(0, 0, (int) position.width() + blurSizeOutline,
624                 (int) position.height() + blurSizeOutline);
625         bounds.inset(blurSizeOutline / 2, blurSizeOutline / 2);
626 
627         try (LauncherIcons li = LauncherIcons.obtain(l)) {
628             Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(drawable, null,
629                     null, null));
630         }
631 
632         bounds.inset(
633                 (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
634                 (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
635         );
636 
637         return bounds.left;
638     }
639 
640     @Override
setClipPath(Path clipPath)641     public void setClipPath(Path clipPath) {
642         mClipPath = clipPath;
643         invalidate();
644     }
645 
646     @Override
draw(Canvas canvas)647     public void draw(Canvas canvas) {
648         int count = canvas.save();
649         canvas.rotate(mRotation,
650                 mFinalDrawableBounds.exactCenterX(), mFinalDrawableBounds.exactCenterY());
651         if (mClipPath != null) {
652             canvas.clipPath(mClipPath);
653         }
654         super.draw(canvas);
655         if (mBackground != null) {
656             mBackground.draw(canvas);
657         }
658         if (mForeground != null) {
659             int count2 = canvas.save();
660             canvas.translate(mFgTransX, mFgTransY);
661             mForeground.draw(canvas);
662             canvas.restoreToCount(count2);
663         }
664         if (mBadge != null) {
665             mBadge.draw(canvas);
666         }
667         canvas.restoreToCount(count);
668     }
669 
fastFinish()670     public void fastFinish() {
671         if (mLoadIconSignal != null) {
672             mLoadIconSignal.cancel();
673         }
674         if (mEndRunnable != null) {
675             mEndRunnable.run();
676             mEndRunnable = null;
677         }
678         if (mFadeAnimatorSet != null) {
679             mFadeAnimatorSet.end();
680             mFadeAnimatorSet = null;
681         }
682     }
683 
684     @Override
onAnimationStart(Animator animator)685     public void onAnimationStart(Animator animator) {
686         if (mIconLoadResult != null && mIconLoadResult.isIconLoaded) {
687             setVisibility(View.VISIBLE);
688         }
689         if (!mIsOpening) {
690             // When closing an app, we want the item on the workspace to be invisible immediately
691             hideOriginalView(mOriginalIcon);
692         }
693     }
694 
695     @Override
onAnimationCancel(Animator animator)696     public void onAnimationCancel(Animator animator) {}
697 
698     @Override
onAnimationRepeat(Animator animator)699     public void onAnimationRepeat(Animator animator) {}
700 
701     @Override
onGlobalLayout()702     public void onGlobalLayout() {
703         if (mOriginalIcon.isAttachedToWindow() && mPositionOut != null) {
704             float rotation = getLocationBoundsForView(mLauncher, mOriginalIcon, mIsOpening,
705                     sTmpRectF);
706             if (rotation != mRotation || !sTmpRectF.equals(mPositionOut)) {
707                 updatePosition(rotation, sTmpRectF, (LayoutParams) getLayoutParams());
708                 if (mOnTargetChangeRunnable != null) {
709                     mOnTargetChangeRunnable.run();
710                 }
711             }
712         }
713     }
714 
setOnTargetChangeListener(Runnable onTargetChangeListener)715     public void setOnTargetChangeListener(Runnable onTargetChangeListener) {
716         mOnTargetChangeRunnable = onTargetChangeListener;
717     }
718 
719     /**
720      * Loads the icon drawable on a worker thread to reduce latency between swapping views.
721      */
722     @UiThread
fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening)723     public static IconLoadResult fetchIcon(Launcher l, View v, ItemInfo info, boolean isOpening) {
724         IconLoadResult result = new IconLoadResult(info);
725         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
726             RectF position = new RectF();
727             getLocationBoundsForView(l, v, isOpening, position);
728             getIconResult(l, v, info, position, result);
729         });
730 
731         sIconLoadResult = result;
732         return result;
733     }
734 
735     /**
736      * Creates a floating icon view for {@param originalView}.
737      * @param originalView The view to copy
738      * @param hideOriginal If true, it will hide {@param originalView} while this view is visible.
739      *                     Else, we will not draw anything in this view.
740      * @param positionOut Rect that will hold the size and position of v.
741      * @param isOpening True if this view replaces the icon for app open animation.
742      */
getFloatingIconView(Launcher launcher, View originalView, boolean hideOriginal, RectF positionOut, boolean isOpening)743     public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView,
744             boolean hideOriginal, RectF positionOut, boolean isOpening) {
745         final DragLayer dragLayer = launcher.getDragLayer();
746         ViewGroup parent = (ViewGroup) dragLayer.getParent();
747         FloatingIconView view = launcher.getViewCache().getView(R.layout.floating_icon_view,
748                 launcher, parent);
749         view.recycle();
750 
751         // Get the drawable on the background thread
752         boolean shouldLoadIcon = originalView.getTag() instanceof ItemInfo && hideOriginal;
753         if (shouldLoadIcon) {
754             if (sIconLoadResult != null && sIconLoadResult.itemInfo == originalView.getTag()) {
755                 view.mIconLoadResult = sIconLoadResult;
756             } else {
757                 view.mIconLoadResult = fetchIcon(launcher, originalView,
758                         (ItemInfo) originalView.getTag(), isOpening);
759             }
760         }
761         sIconLoadResult = null;
762 
763         view.mIsVerticalBarLayout = launcher.getDeviceProfile().isVerticalBarLayout();
764         view.mIsOpening = isOpening;
765         view.mOriginalIcon = originalView;
766         view.mPositionOut = positionOut;
767 
768         // Match the position of the original view.
769         view.matchPositionOf(launcher, originalView, isOpening, positionOut);
770 
771         // We need to add it to the overlay, but keep it invisible until animation starts..
772         view.setVisibility(INVISIBLE);
773         parent.addView(view);
774         dragLayer.addView(view.mListenerView);
775         view.mListenerView.setListener(view::fastFinish);
776 
777         view.mEndRunnable = () -> {
778             view.mEndRunnable = null;
779 
780             if (hideOriginal) {
781                 if (isOpening) {
782                     if (originalView instanceof BubbleTextView) {
783                         ((BubbleTextView) originalView).setIconVisible(true);
784                         ((BubbleTextView) originalView).setForceHideDot(false);
785                     } else {
786                         originalView.setVisibility(VISIBLE);
787                     }
788                     view.finish(dragLayer);
789                 } else {
790                     view.mFadeAnimatorSet = view.createFadeAnimation(originalView, dragLayer);
791                     view.mFadeAnimatorSet.start();
792                 }
793             } else {
794                 view.finish(dragLayer);
795             }
796         };
797 
798         // Must be called after matchPositionOf so that we know what size to load.
799         // Must be called after the fastFinish listener and end runnable is created so that
800         // the icon is not left in a hidden state.
801         if (shouldLoadIcon) {
802             view.checkIconResult(originalView);
803         }
804 
805         return view;
806     }
807 
createFadeAnimation(View originalView, DragLayer dragLayer)808     private AnimatorSet createFadeAnimation(View originalView, DragLayer dragLayer) {
809         AnimatorSet fade = new AnimatorSet();
810         fade.setDuration(FADE_DURATION_MS);
811         fade.addListener(new AnimatorListenerAdapter() {
812             @Override
813             public void onAnimationStart(Animator animation) {
814                 originalView.setVisibility(VISIBLE);
815             }
816 
817             @Override
818             public void onAnimationEnd(Animator animation) {
819                 finish(dragLayer);
820             }
821         });
822 
823         if (mBadge != null) {
824             ObjectAnimator badgeFade = ObjectAnimator.ofInt(mBadge, DRAWABLE_ALPHA, 255);
825             badgeFade.addUpdateListener(valueAnimator -> invalidate());
826             fade.play(badgeFade);
827         }
828 
829         if (originalView instanceof IconLabelDotView) {
830             IconLabelDotView view = (IconLabelDotView) originalView;
831             fade.addListener(new AnimatorListenerAdapter() {
832                 @Override
833                 public void onAnimationEnd(Animator animation) {
834                     view.setIconVisible(true);
835                     view.setForceHideDot(false);
836                 }
837             });
838         }
839 
840         if (originalView instanceof BubbleTextView) {
841             BubbleTextView btv = (BubbleTextView) originalView;
842             fade.addListener(new AnimatorListenerAdapter() {
843                 @Override
844                 public void onAnimationStart(Animator animation) {
845                     btv.setIconVisible(true);
846                     btv.setForceHideDot(true);
847                 }
848             });
849             fade.play(ObjectAnimator.ofInt(btv.getIcon(), DRAWABLE_ALPHA, 0, 255));
850         } else if (!(originalView instanceof FolderIcon)) {
851             fade.play(ObjectAnimator.ofFloat(originalView, ALPHA, 0f, 1f));
852         }
853 
854         return fade;
855     }
856 
finish(DragLayer dragLayer)857     private void finish(DragLayer dragLayer) {
858         ((ViewGroup) dragLayer.getParent()).removeView(this);
859         dragLayer.removeView(mListenerView);
860         recycle();
861         mLauncher.getViewCache().recycleView(R.layout.floating_icon_view, this);
862     }
863 
recycle()864     private void recycle() {
865         setTranslationX(0);
866         setTranslationY(0);
867         setScaleX(1);
868         setScaleY(1);
869         setAlpha(1);
870         setBackground(null);
871         if (mLoadIconSignal != null) {
872             mLoadIconSignal.cancel();
873         }
874         mLoadIconSignal = null;
875         mEndRunnable = null;
876         mIsAdaptiveIcon = false;
877         mForeground = null;
878         mBackground = null;
879         mClipPath = null;
880         mFinalDrawableBounds.setEmpty();
881         if (mRevealAnimator != null) {
882             mRevealAnimator.cancel();
883         }
884         mRevealAnimator = null;
885         if (mFadeAnimatorSet != null) {
886             mFadeAnimatorSet.cancel();
887         }
888         mPositionOut = null;
889         mFadeAnimatorSet = null;
890         mListenerView.setListener(null);
891         mOriginalIcon = null;
892         mOnTargetChangeRunnable = null;
893         mTaskCornerRadius = 0;
894         mOutline.setEmpty();
895         mFgTransY = 0;
896         mFgSpringX.cancel();
897         mFgTransX = 0;
898         mFgSpringY.cancel();
899         mBadge = null;
900         sTmpObjArray[0] = null;
901         mIconLoadResult = null;
902     }
903 
904     private static class IconLoadResult {
905         final ItemInfo itemInfo;
906         Drawable drawable;
907         Drawable badge;
908         int iconOffset;
909         Runnable onIconLoaded;
910         boolean isIconLoaded;
911 
IconLoadResult(ItemInfo itemInfo)912         public IconLoadResult(ItemInfo itemInfo) {
913             this.itemInfo = itemInfo;
914         }
915     }
916 }
917