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; } }