From 1a49fb573373f0ce1fdd02f45fd95236344373d7 Mon Sep 17 00:00:00 2001 From: Jon Miranda Date: Tue, 19 Feb 2019 15:34:41 -0800 Subject: [PATCH] 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 --- .../LauncherActivityControllerHelper.java | 10 +- .../WindowTransformSwipeHandler.java | 23 +- .../QuickstepAppTransitionManagerImpl.java | 7 +- src/com/android/launcher3/DeviceProfile.java | 6 + src/com/android/launcher3/Utilities.java | 67 +++- .../android/launcher3/config/BaseFlags.java | 4 + .../android/launcher3/dragndrop/DragView.java | 56 +--- src/com/android/launcher3/folder/Folder.java | 4 +- .../android/launcher3/folder/FolderShape.java | 14 +- .../android/launcher3/views/ClipPathView.java | 27 ++ .../launcher3/views/FloatingIconView.java | 296 +++++++++++++++--- 11 files changed, 390 insertions(+), 124 deletions(-) create mode 100644 src/com/android/launcher3/views/ClipPathView.java diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java index ffd3b4b06e..408b749a6e 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java @@ -104,13 +104,11 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe .sourceComponent; final View workspaceView = activity.getWorkspace().getFirstMatchForAppClose(component); - final FloatingIconView floatingView = workspaceView == null ? null - : new FloatingIconView(activity); final Rect iconLocation = new Rect(); - if (floatingView != null) { - floatingView.matchPositionOf(activity, workspaceView, true /* hideOriginal */, - iconLocation); - } + final FloatingIconView floatingView = workspaceView == null ? null + : FloatingIconView.getFloatingIconView(activity, workspaceView, + true /* hideOriginal */, false /* useDrawableAsIs */, + activity.getDeviceProfile().getAspectRatioWithInsets(), iconLocation, null); return new HomeAnimationFactory() { @Nullable diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java index 4d0136e0d5..fa827e793f 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java @@ -972,27 +972,36 @@ public class WindowTransformSwipeHandler final RectF currentRect = new RectF(); final View floatingView = homeAnimationFactory.getFloatingView(); + final boolean isFloatingIconView = floatingView instanceof FloatingIconView; + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); - if (floatingView instanceof FloatingIconView) { + if (isFloatingIconView) { anim.addListener((FloatingIconView) floatingView); } + + // We want the window alpha to be 0 once this threshold is met, so that the + // FolderIconView can be seen morphing into the icon shape. + final float windowAlphaThreshold = isFloatingIconView ? 0.75f : 1f; anim.addUpdateListener(animation -> { float progress = animation.getAnimatedFraction(); - float interpolatedProgress = Interpolators.ACCEL_2.getInterpolation(progress); + float interpolatedProgress = Interpolators.ACCEL_1_5.getInterpolation(progress); // Initially go towards original target (task view in recents), // but accelerate towards the final target. // TODO: This is technically not correct. Instead, motion should continue at // the released velocity but accelerate towards the target. targetRect.set(rectFEvaluator.evaluate(interpolatedProgress, originalTarget, finalTarget)); - currentRect.set(rectFEvaluator.evaluate(progress, startRect, targetRect)); - float alpha = 1 - interpolatedProgress; - mTransformParams.setCurrentRectAndTargetAlpha(currentRect, alpha) + currentRect.set(rectFEvaluator.evaluate(interpolatedProgress, startRect, targetRect)); + + float iconAlpha = Utilities.mapToRange(interpolatedProgress, 0, + windowAlphaThreshold, 0f, 1f, Interpolators.LINEAR); + mTransformParams.setCurrentRectAndTargetAlpha(currentRect, 1f - iconAlpha) .setSyncTransactionApplier(mSyncTransactionApplier); mClipAnimationHelper.applyTransform(targetSet, mTransformParams); - if (floatingView instanceof FloatingIconView) { - ((FloatingIconView) floatingView).update(currentRect, 1f - alpha); + if (isFloatingIconView) { + ((FloatingIconView) floatingView).update(currentRect, iconAlpha, progress, + windowAlphaThreshold); } }); anim.addListener(new AnimationSuccessListener() { diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java index 9a7c194ec4..f8b167b48c 100644 --- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java +++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java @@ -403,9 +403,7 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans private void playIconAnimators(AnimatorSet appOpenAnimator, View v, Rect windowTargetBounds, boolean toggleVisibility) { final boolean isBubbleTextView = v instanceof BubbleTextView; - if (mFloatingView == null) { - mFloatingView = new FloatingIconView(mLauncher); - } else { + if (mFloatingView != null) { mFloatingView.setTranslationX(0); mFloatingView.setTranslationY(0); mFloatingView.setScaleX(1); @@ -414,7 +412,8 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans mFloatingView.setBackground(null); } Rect rect = new Rect(); - mFloatingView.matchPositionOf(mLauncher, v, toggleVisibility, rect); + mFloatingView = FloatingIconView.getFloatingIconView(mLauncher, v, toggleVisibility, + true /* useDrawableAsIs */, -1 /* aspectRatio */, rect, mFloatingView); int viewLocationStart = mIsRtl ? windowTargetBounds.width() - rect.right : rect.left; LayoutParams lp = (LayoutParams) mFloatingView.getLayoutParams(); diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java index 01535b0d87..3b054c2be8 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -611,6 +611,12 @@ public class DeviceProfile { 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 832e9c94f6..c3edfe5d20 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -27,6 +27,7 @@ import android.content.Context; 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.Paint; 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.util.TypedValue; 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 final class Utilities { 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 @@ public final class Utilities { // 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 final class Utilities { 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 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 882529dc3c..a6fe4ee6c0 100644 --- a/src/com/android/launcher3/config/BaseFlags.java +++ b/src/com/android/launcher3/config/BaseFlags.java @@ -101,6 +101,10 @@ abstract class BaseFlags { 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 8f223a37e0..8a27f9daff 100644 --- a/src/com/android/launcher3/dragndrop/DragView.java +++ b/src/com/android/launcher3/dragndrop/DragView.java @@ -24,7 +24,6 @@ import android.animation.FloatArrayEvaluator; 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.R; 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 class DragView extends View { 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; @@ -313,49 +306,6 @@ public class DragView extends View { invalidate(); } - /** - * 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 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 diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index 6fc81c958b..7a14b36b03 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.pageindicators.PageIndicatorDots; 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 @@ import java.util.List; /** * 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 @@ public class Folder extends AbstractFloatingView implements DragSource, * 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 61db6ffad7..ec6078e83b 100644 --- a/src/com/android/launcher3/folder/FolderShape.java +++ b/src/com/android/launcher3/folder/FolderShape.java @@ -39,6 +39,7 @@ import android.util.AttributeSet; 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.Utilities; 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 class FolderShape { 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 Animator createRevealAnimator(T target, + Rect startRect, Rect endRect, float endRadius, boolean isReversed); @Nullable public TypedValue getAttrValue(int attr) { @@ -88,8 +90,8 @@ public abstract class FolderShape { private static abstract class SimpleRectShape extends FolderShape { @Override - public final Animator createRevealAnimator(Folder target, Rect startRect, Rect endRect, - float endRadius, boolean isReversed) { + public final 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 @@ public abstract class FolderShape { 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 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 0000000000..2152e1d818 --- /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 07318c91ba..2b9e7b6e12 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 @@ public class FloatingIconView extends View implements Animator.AnimatorListener 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 @@ public class FloatingIconView extends View implements Animator.AnimatorListener } } - @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 @@ public class FloatingIconView extends View implements Animator.AnimatorListener // 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; } }