Transform closing window to icon on workspace using adaptive icons.
With adaptive icons, we can have FloatingIconView match the shape
and size of the closing window, regardless of the icon shape.
FloatingIconView starts off as a rounded rect (same corners as task view)
and then morphs into the icon shape using FolderShape#createRevealAnimator
in reverse.
Decided to add FeatureFlag.ADAPTIVE_ICON_WINDOW_ANIM since there are still
some issues with folders, badges, and a visible jump when swapping the
FloatingIconView with the original icon.
Bug: 123900446
Change-Id: I94969eea6d5f4b932a84a11eb403611276042b46
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 01535b0..3b054c2 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -611,6 +611,12 @@
outBounds.right = outBounds.left + (getCellSize().x * spanX);
}
+ public float getAspectRatioWithInsets() {
+ int w = widthPx - mInsets.left - mInsets.right;
+ int h = heightPx - mInsets.top - mInsets.bottom;
+ return ((float) Math.max(w, h)) / Math.min(w, h);
+ }
+
private static Context getContext(Context c, int orientation) {
Configuration context = new Configuration(c.getResources().getConfiguration());
context.orientation = orientation;
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 832e9c9..c3edfe5 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -27,6 +27,7 @@
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -37,6 +38,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.DeadObjectException;
@@ -55,14 +57,24 @@
import android.view.View;
import android.view.animation.Interpolator;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.ShortcutConfigActivityInfo;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.IntArray;
+import com.android.launcher3.widget.PendingAddShortcutInfo;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
@@ -556,6 +568,7 @@
public static void getLocationBoundsForView(Launcher launcher, View v, Rect outRect) {
final DragLayer dragLayer = launcher.getDragLayer();
final boolean isBubbleTextView = v instanceof BubbleTextView;
+ final boolean isFolderIcon = v instanceof FolderIcon;
final Rect rect = new Rect();
final boolean fromDeepShortcutView = v.getParent() instanceof DeepShortcutView;
@@ -563,15 +576,14 @@
// Deep shortcut views have their icon drawn in a separate view.
DeepShortcutView view = (DeepShortcutView) v.getParent();
dragLayer.getDescendantRectRelativeToSelf(view.getIconView(), rect);
- } else if (isBubbleTextView && v.getTag() instanceof ItemInfo
+ } else if ((isBubbleTextView || isFolderIcon) && v.getTag() instanceof ItemInfo
&& (((ItemInfo) v.getTag()).container == CONTAINER_DESKTOP
|| ((ItemInfo) v.getTag()).container == CONTAINER_HOTSEAT)) {
- BubbleTextView btv = (BubbleTextView) v;
- CellLayout pageViewIsOn = ((CellLayout) btv.getParent().getParent());
+ CellLayout pageViewIsOn = ((CellLayout) v.getParent().getParent());
int pageNum = launcher.getWorkspace().indexOfChild(pageViewIsOn);
DeviceProfile dp = launcher.getDeviceProfile();
- ItemInfo info = ((ItemInfo) btv.getTag());
+ ItemInfo info = ((ItemInfo) v.getTag());
dp.getItemLocation(info.cellX, info.cellY, info.spanX, info.spanY,
info.container, pageNum - launcher.getCurrentWorkspaceScreen(), rect);
} else {
@@ -595,6 +607,51 @@
public static void unregisterReceiverSafely(Context context, BroadcastReceiver receiver) {
try {
context.unregisterReceiver(receiver);
- } catch (IllegalArgumentException e) { }
+ } catch (IllegalArgumentException e) {}
+ }
+
+ /**
+ * Returns the full drawable for {@param info}.
+ * @param outObj this is set to the internal data associated with {@param info},
+ * eg {@link LauncherActivityInfo} or {@link ShortcutInfoCompat}.
+ */
+ public static Drawable getFullDrawable(Launcher launcher, ItemInfo info, int width, int height,
+ Object[] outObj) {
+ LauncherAppState appState = LauncherAppState.getInstance(launcher);
+ if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
+ LauncherActivityInfo activityInfo = LauncherAppsCompat.getInstance(launcher)
+ .resolveActivity(info.getIntent(), info.user);
+ outObj[0] = activityInfo;
+ return (activityInfo != null) ? appState.getIconCache()
+ .getFullResIcon(activityInfo, false) : null;
+ } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ if (info instanceof PendingAddShortcutInfo) {
+ ShortcutConfigActivityInfo activityInfo =
+ ((PendingAddShortcutInfo) info).activityInfo;
+ outObj[0] = activityInfo;
+ return activityInfo.getFullResIcon(appState.getIconCache());
+ }
+ ShortcutKey key = ShortcutKey.fromItemInfo(info);
+ DeepShortcutManager sm = DeepShortcutManager.getInstance(launcher);
+ List<ShortcutInfoCompat> si = sm.queryForFullDetails(
+ key.componentName.getPackageName(), Arrays.asList(key.getId()), key.user);
+ if (si.isEmpty()) {
+ return null;
+ } else {
+ outObj[0] = si.get(0);
+ return sm.getShortcutIconDrawable(si.get(0),
+ appState.getInvariantDeviceProfile().fillResIconDpi);
+ }
+ } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+ FolderAdaptiveIcon icon = FolderAdaptiveIcon.createFolderAdaptiveIcon(
+ launcher, info.id, new Point(width, height));
+ if (icon == null) {
+ return null;
+ }
+ outObj[0] = icon;
+ return icon;
+ } else {
+ return null;
+ }
}
}
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 882529d..a6fe4ee 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -101,6 +101,10 @@
public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
false, "Enable springs for quickstep animations");
+ public static final TogglableFlag ADAPTIVE_ICON_WINDOW_ANIM = new TogglableFlag(
+ "ADAPTIVE_ICON_WINDOW_ANIM", false,
+ "Use adaptive icons for window animations.");
+
public static final TogglableFlag ENABLE_QUICKSTEP_LIVE_TILE = new TogglableFlag(
"ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 8f223a3..8a27f9d 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -24,7 +24,6 @@
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
-import android.content.pm.LauncherActivityInfo;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -54,18 +53,12 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.FirstFrameAnimatorHelper;
import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.compat.LauncherAppsCompat;
-import com.android.launcher3.compat.ShortcutConfigActivityInfo;
import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
import com.android.launcher3.shortcuts.ShortcutInfoCompat;
-import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.Thunk;
-import com.android.launcher3.widget.PendingAddShortcutInfo;
import java.util.Arrays;
-import java.util.List;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
@@ -204,11 +197,11 @@
public void run() {
LauncherAppState appState = LauncherAppState.getInstance(mLauncher);
Object[] outObj = new Object[1];
- Drawable dr = getFullDrawable(info, appState, outObj);
+ int w = mBitmap.getWidth();
+ int h = mBitmap.getHeight();
+ Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
if (dr instanceof AdaptiveIconDrawable) {
- int w = mBitmap.getWidth();
- int h = mBitmap.getHeight();
int blurMargin = (int) mLauncher.getResources()
.getDimension(R.dimen.blur_size_medium_outline) / 2;
@@ -314,49 +307,6 @@
}
/**
- * Returns the full drawable for {@param info}.
- * @param outObj this is set to the internal data associated with {@param info},
- * eg {@link LauncherActivityInfo} or {@link ShortcutInfoCompat}.
- */
- private Drawable getFullDrawable(ItemInfo info, LauncherAppState appState, Object[] outObj) {
- if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
- LauncherActivityInfo activityInfo = LauncherAppsCompat.getInstance(mLauncher)
- .resolveActivity(info.getIntent(), info.user);
- outObj[0] = activityInfo;
- return (activityInfo != null) ? appState.getIconCache()
- .getFullResIcon(activityInfo, false) : null;
- } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
- if (info instanceof PendingAddShortcutInfo) {
- ShortcutConfigActivityInfo activityInfo =
- ((PendingAddShortcutInfo) info).activityInfo;
- outObj[0] = activityInfo;
- return activityInfo.getFullResIcon(appState.getIconCache());
- }
- ShortcutKey key = ShortcutKey.fromItemInfo(info);
- DeepShortcutManager sm = DeepShortcutManager.getInstance(mLauncher);
- List<ShortcutInfoCompat> si = sm.queryForFullDetails(
- key.componentName.getPackageName(), Arrays.asList(key.getId()), key.user);
- if (si.isEmpty()) {
- return null;
- } else {
- outObj[0] = si.get(0);
- return sm.getShortcutIconDrawable(si.get(0),
- appState.getInvariantDeviceProfile().fillResIconDpi);
- }
- } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
- FolderAdaptiveIcon icon = FolderAdaptiveIcon.createFolderAdaptiveIcon(
- mLauncher, info.id, new Point(mBitmap.getWidth(), mBitmap.getHeight()));
- if (icon == null) {
- return null;
- }
- outObj[0] = icon;
- return icon;
- } else {
- return null;
- }
- }
-
- /**
* For apps icons and shortcut icons that have badges, this method creates a drawable that can
* later on be rendered on top of the layers for the badges. For app icons, work profile badges
* can only be applied. For deep shortcuts, when dragged from the pop up container, there's no
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 6fc81c9..7a14b36 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -74,6 +74,7 @@
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ClipPathView;
import com.android.launcher3.widget.PendingAddShortcutInfo;
import java.util.ArrayList;
@@ -84,7 +85,7 @@
/**
* Represents a set of icons chosen by the user or generated by the system.
*/
-public class Folder extends AbstractFloatingView implements DragSource,
+public class Folder extends AbstractFloatingView implements ClipPathView, DragSource,
View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
View.OnFocusChangeListener, DragListener, ExtendedEditText.OnBackKeyListener {
private static final String TAG = "Launcher.Folder";
@@ -1460,6 +1461,7 @@
* Alternative to using {@link #getClipToOutline()} as it only works with derivatives of
* rounded rect.
*/
+ @Override
public void setClipPath(Path clipPath) {
mClipPath = clipPath;
invalidate();
diff --git a/src/com/android/launcher3/folder/FolderShape.java b/src/com/android/launcher3/folder/FolderShape.java
index 61db6ff..ec6078e 100644
--- a/src/com/android/launcher3/folder/FolderShape.java
+++ b/src/com/android/launcher3/folder/FolderShape.java
@@ -39,6 +39,7 @@
import android.util.SparseArray;
import android.util.TypedValue;
import android.util.Xml;
+import android.view.View;
import android.view.ViewOutlineProvider;
import com.android.launcher3.R;
@@ -46,6 +47,7 @@
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ClipPathView;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -74,8 +76,8 @@
public abstract void addShape(Path path, float offsetX, float offsetY, float radius);
- public abstract Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect,
- float endRadius, boolean isReversed);
+ public abstract <T extends View & ClipPathView> Animator createRevealAnimator(T target,
+ Rect startRect, Rect endRect, float endRadius, boolean isReversed);
@Nullable
public TypedValue getAttrValue(int attr) {
@@ -88,8 +90,8 @@
private static abstract class SimpleRectShape extends FolderShape {
@Override
- public final Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect,
- float endRadius, boolean isReversed) {
+ public final <T extends View & ClipPathView> Animator createRevealAnimator(T target,
+ Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
return new RoundedRectRevealOutlineProvider(
getStartRadius(startRect), endRadius, startRect, endRect) {
@Override
@@ -121,8 +123,8 @@
Rect startRect, Rect endRect, float endRadius, Path outPath);
@Override
- public final Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect,
- float endRadius, boolean isReversed) {
+ public final <T extends View & ClipPathView> Animator createRevealAnimator(T target,
+ Rect startRect, Rect endRect, float endRadius, boolean isReversed) {
Path path = new Path();
AnimatorUpdateListener listener =
newUpdateListener(startRect, endRect, endRadius, path);
diff --git a/src/com/android/launcher3/views/ClipPathView.java b/src/com/android/launcher3/views/ClipPathView.java
new file mode 100644
index 0000000..2152e1d
--- /dev/null
+++ b/src/com/android/launcher3/views/ClipPathView.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.views;
+
+import android.graphics.Path;
+import android.view.View;
+
+/**
+ * Alternative to using {@link View#getClipToOutline()} as it only works with derivatives of
+ * rounded rect.
+ */
+public interface ClipPathView {
+ void setClipPath(Path clipPath);
+}
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 07318c9..2b9e7b6 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -16,41 +16,86 @@
package com.android.launcher3.views;
import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Outline;
+import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
-import com.android.launcher3.BubbleTextView;
import com.android.launcher3.InsettableFrameLayout.LayoutParams;
-import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.DrawableFactory;
+import com.android.launcher3.folder.FolderShape;
+import com.android.launcher3.icons.LauncherIcons;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
/**
* A view that is created to look like another view with the purpose of creating fluid animations.
*/
-public class FloatingIconView extends View implements Animator.AnimatorListener {
+
+public class FloatingIconView extends View implements Animator.AnimatorListener, ClipPathView {
private Runnable mStartRunnable;
private Runnable mEndRunnable;
- public FloatingIconView(Context context) {
- super(context);
- }
+ private Drawable mDrawable;
+ private int mOriginalHeight;
+ private final int mBlurSizeOutline;
- public void setRunnables(Runnable startRunnable, Runnable endRunnable) {
- mStartRunnable = startRunnable;
- mEndRunnable = endRunnable;
+ private boolean mIsAdaptiveIcon = false;
+
+ private @Nullable Drawable mForeground;
+ private @Nullable Drawable mBackground;
+ private ValueAnimator mRevealAnimator;
+ private final Rect mStartRevealRect = new Rect();
+ private final Rect mEndRevealRect = new Rect();
+ private Path mClipPath;
+ protected final Rect mOutline = new Rect();
+ private final float mTaskCornerRadius;
+
+ private final Rect mFinalDrawableBounds = new Rect();
+ private final Rect mBgDrawableBounds = new Rect();
+ private final float mBgDrawableStartScale = 5f; // Magic number that can be tuned later.
+
+ private FloatingIconView(Context context) {
+ super(context);
+
+ mBlurSizeOutline = context.getResources().getDimensionPixelSize(
+ R.dimen.blur_size_medium_outline);
+
+ mTaskCornerRadius = 0; // TODO
}
/**
* Positions this view to match the size and location of {@param rect}.
+ *
+ * @param alpha The alpha to set this view.
+ * @param progress A value from [0, 1] that represents the animation progress.
+ * @param windowAlphaThreshold The value at which the window alpha is 0.
*/
- public void update(RectF rect, float alpha) {
+ public void update(RectF rect, float alpha, float progress, float windowAlphaThreshold) {
setAlpha(alpha);
LayoutParams lp = (LayoutParams) getLayoutParams();
@@ -59,13 +104,44 @@
setTranslationX(dX);
setTranslationY(dY);
- float scaleX = rect.width() / (float) getWidth();
- float scaleY = rect.height() / (float) getHeight();
- float scale = Math.min(scaleX, scaleY);
+ float scaleX = rect.width() / (float) lp.width;
+ float scaleY = rect.height() / (float) lp.height;
+ float scale = mIsAdaptiveIcon ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
setPivotX(0);
setPivotY(0);
setScaleX(scale);
setScaleY(scale);
+
+ // Wait until the window is no longer visible before morphing the icon into its final shape.
+ float shapeRevealProgress = Utilities.mapToRange(Math.max(windowAlphaThreshold, progress),
+ windowAlphaThreshold, 1f, 0f, 1, Interpolators.LINEAR);
+ if (mIsAdaptiveIcon && shapeRevealProgress > 0) {
+ if (mRevealAnimator == null) {
+ mEndRevealRect.set(mOutline);
+ // We play the reveal animation in reverse so that we end with the icon shape.
+ mRevealAnimator = (ValueAnimator) FolderShape.getShape().createRevealAnimator(this,
+ mStartRevealRect, mEndRevealRect, mTaskCornerRadius / scale, true);
+ mRevealAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mRevealAnimator = null;
+ }
+ });
+ mRevealAnimator.start();
+ // We pause here so we can set the current fraction ourselves.
+ mRevealAnimator.pause();
+ }
+
+ float bgScale = shapeRevealProgress + mBgDrawableStartScale * (1 - shapeRevealProgress);
+ setBackgroundDrawableBounds(bgScale);
+
+ mRevealAnimator.setCurrentFraction(shapeRevealProgress);
+ if (Float.compare(shapeRevealProgress, 1f) >= 0f) {
+ mRevealAnimator.end();
+ }
+ }
+ invalidate();
+ invalidateOutline();
}
@Override
@@ -82,25 +158,17 @@
}
}
- @Override
- public void onAnimationCancel(Animator animator) {
- }
-
- @Override
- public void onAnimationRepeat(Animator animator) {
- }
-
/**
* Sets the size and position of this view to match {@param v}.
*
* @param v The view to copy
- * @param hideOriginal If true, it will hide {@param v} while this view is visible.
* @param positionOut Rect that will hold the size and position of v.
*/
- public void matchPositionOf(Launcher launcher, View v, boolean hideOriginal, Rect positionOut) {
+ private void matchPositionOf(Launcher launcher, View v, Rect positionOut) {
Utilities.getLocationBoundsForView(launcher, v, positionOut);
final LayoutParams lp = new LayoutParams(positionOut.width(), positionOut.height());
lp.ignoreInsets = true;
+ mOriginalHeight = lp.height;
// Position the floating view exactly on top of the original
lp.leftMargin = positionOut.left;
@@ -110,29 +178,173 @@
// animation frame.
layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin
+ lp.height);
+ }
- if (v instanceof BubbleTextView && v.getTag() instanceof ItemInfoWithIcon ) {
- // Create a copy of the app icon
- setBackground(DrawableFactory.INSTANCE.get(launcher)
- .newIcon(v.getContext(), (ItemInfoWithIcon) v.getTag()));
+ @WorkerThread
+ private void getIcon(Launcher launcher, ItemInfo info, boolean useDrawableAsIs,
+ float aspectRatio) {
+ final LayoutParams lp = (LayoutParams) getLayoutParams();
+ mDrawable = Utilities.getFullDrawable(launcher, info, lp.width, lp.height, new Object[1]);
+
+ if (ADAPTIVE_ICON_WINDOW_ANIM.get() && !useDrawableAsIs
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+ && mDrawable instanceof AdaptiveIconDrawable) {
+ mIsAdaptiveIcon = true;
+
+ AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) mDrawable;
+ Drawable background = adaptiveIcon.getBackground();
+ if (background == null) {
+ background = new ColorDrawable(Color.TRANSPARENT);
+ }
+ mBackground = background;
+ Drawable foreground = adaptiveIcon.getForeground();
+ if (foreground == null) {
+ foreground = new ColorDrawable(Color.TRANSPARENT);
+ }
+ mForeground = foreground;
+
+ int offset = getOffsetForAdaptiveIconBounds();
+ mFinalDrawableBounds.set(offset, offset, lp.width - offset, mOriginalHeight - offset);
+ mForeground.setBounds(mFinalDrawableBounds);
+ mBackground.setBounds(mFinalDrawableBounds);
+
+ int blurMargin = mBlurSizeOutline / 2;
+ mStartRevealRect.set(blurMargin, blurMargin , lp.width - blurMargin,
+ mOriginalHeight - blurMargin);
+
+ if (aspectRatio > 0) {
+ lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
+ layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin
+ + lp.height);
+ setBackgroundDrawableBounds(mBgDrawableStartScale);
+ }
+
+ // Set up outline
+ mOutline.set(0, 0, lp.width, lp.height);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(mOutline, mTaskCornerRadius);
+ }
+ });
+ setClipToOutline(true);
+ } else {
+ setBackground(mDrawable);
+ }
+
+ new Handler(Looper.getMainLooper()).post(() -> {
+ invalidate();
+ invalidateOutline();
+ });
+ }
+
+ private void setBackgroundDrawableBounds(float scale) {
+ mBgDrawableBounds.set(mFinalDrawableBounds);
+ Utilities.scaleRectAboutCenter(mBgDrawableBounds, scale);
+ mBackground.setBounds(mBgDrawableBounds);
+ }
+
+ private int getOffsetForAdaptiveIconBounds() {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O ||
+ !(mDrawable instanceof AdaptiveIconDrawable)) {
+ return 0;
+ }
+
+ final LayoutParams lp = (LayoutParams) getLayoutParams();
+ Rect bounds = new Rect(0, 0, lp.width + mBlurSizeOutline, lp.height + mBlurSizeOutline);
+ bounds.inset(mBlurSizeOutline / 2, mBlurSizeOutline / 2);
+
+ try (LauncherIcons li = LauncherIcons.obtain(Launcher.fromContext(getContext()))) {
+ Utilities.scaleRectAboutCenter(bounds, li.getNormalizer().getScale(mDrawable, null));
+ }
+
+ bounds.inset(
+ (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
+ (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
+ );
+
+ return bounds.left;
+ }
+
+ @Override
+ public void setClipPath(Path clipPath) {
+ mClipPath = clipPath;
+ invalidate();
+ }
+
+ private void drawAdaptiveIconIfExists(Canvas canvas) {
+ if (mBackground != null) {
+ mBackground.draw(canvas);
+ }
+ if (mForeground != null) {
+ mForeground.draw(canvas);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mClipPath == null) {
+ super.draw(canvas);
+ drawAdaptiveIconIfExists(canvas);
+ } else {
+ int count = canvas.save();
+ canvas.clipPath(mClipPath);
+ super.draw(canvas);
+ drawAdaptiveIconIfExists(canvas);
+ canvas.restoreToCount(count);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {}
+
+ /**
+ * Creates a floating icon view for {@param originalView}.
+ *
+ * @param originalView The view to copy
+ * @param hideOriginal If true, it will hide {@param originalView} while this view is visible.
+ * @param useDrawableAsIs If true, we do not separate the foreground/background of adaptive
+ * icons. TODO(b/122843905): We can remove this once app opening uses new animation.
+ * @param aspectRatio If >= 0, we will use this aspect ratio for the initial adaptive icon size.
+ * @param positionOut Rect that will hold the size and position of v.
+ */
+ public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView,
+ boolean hideOriginal, boolean useDrawableAsIs, float aspectRatio, Rect positionOut,
+ FloatingIconView recycle) {
+ FloatingIconView view = recycle != null ? recycle : new FloatingIconView(launcher);
+
+ // Match the position of the original view.
+ view.matchPositionOf(launcher, originalView, positionOut);
+
+ // Get the drawable on the background thread
+ // Must be called after matchPositionOf so that we know what size to load.
+ if (originalView.getTag() instanceof ItemInfo) {
+ new Handler(LauncherModel.getWorkerLooper()).postAtFrontOfQueue(() -> {
+ view.getIcon(launcher, (ItemInfo) originalView.getTag(), useDrawableAsIs,
+ aspectRatio);
+ });
}
// We need to add it to the overlay, but keep it invisible until animation starts..
final DragLayer dragLayer = launcher.getDragLayer();
- setVisibility(INVISIBLE);
- ((ViewGroup) dragLayer.getParent()).getOverlay().add(this);
+ view.setVisibility(INVISIBLE);
+ ((ViewGroup) dragLayer.getParent()).getOverlay().add(view);
- setRunnables(() -> {
- setVisibility(VISIBLE);
- if (hideOriginal) {
- v.setVisibility(INVISIBLE);
- }
- },
- () -> {
- ((ViewGroup) dragLayer.getParent()).getOverlay().remove(this);
- if (hideOriginal) {
- v.setVisibility(VISIBLE);
- }
- });
+ view.mStartRunnable = () -> {
+ view.setVisibility(VISIBLE);
+ if (hideOriginal) {
+ originalView.setVisibility(INVISIBLE);
+ }
+ };
+ view.mEndRunnable = () -> {
+ ((ViewGroup) dragLayer.getParent()).getOverlay().remove(view);
+ if (hideOriginal) {
+ originalView.setVisibility(VISIBLE);
+ }
+ };
+ return view;
}
}