From 3bdfc3cd2d77715164fada62fdf27a57369646f5 Mon Sep 17 00:00:00 2001 From: Cyrus Boadway Date: Thu, 29 Apr 2021 11:35:04 +0000 Subject: [PATCH] Create return-to-home app widget animation If an app has been most recently launched from an app widget, when swiped away, the app animates to the widget's position. This is done by attributing the app launch to the widget through the ActivityOptions's launch cookies, and using a FloatingWidgetView throughout the animation. Bug: 169042867 Test: manual Change-Id: I24c2623b5b3407504a4768b076849c47f73cbae0 --- .../launcher3/BaseQuickstepLauncher.java | 3 +- .../launcher3/QuickstepTransitionManager.java | 5 +- .../QuickstepInteractionHandler.java | 5 + .../quickstep/LauncherSwipeHandlerV2.java | 341 ++++++++++-------- .../quickstep/SwipeUpAnimationLogic.java | 12 +- .../quickstep/views/FloatingWidgetView.java | 14 +- 6 files changed, 224 insertions(+), 156 deletions(-) diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java index 7aae38c7f1..9dbe9c225a 100644 --- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java @@ -447,7 +447,8 @@ public abstract class BaseQuickstepLauncher extends Launcher case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: - // Fall through and continue if it's an app or shortcut + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + // Fall through and continue if it's an app, shortcut, or widget break; default: return; diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index 1a464b14b2..6b190ed4b4 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -60,6 +60,7 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemProperties; import android.util.Pair; +import android.util.Size; import android.view.View; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; @@ -781,7 +782,9 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener final float finalWindowRadius = mDeviceProfile.isMultiWindowMode ? 0 : getWindowCornerRadius(mLauncher.getResources()); final FloatingWidgetView floatingView = FloatingWidgetView.getFloatingWidgetView(mLauncher, - v, widgetBackgroundBounds, windowTargetBounds, finalWindowRadius); + v, widgetBackgroundBounds, + new Size(windowTargetBounds.width(), windowTargetBounds.height()), + finalWindowRadius); final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources()) ? floatingView.getInitialCornerRadius() : 0; diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java index 66e4f4c61c..5c19ab8bf4 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepInteractionHandler.java @@ -23,6 +23,7 @@ import android.util.Pair; import android.view.View; import android.widget.RemoteViews; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.ActivityOptionsWrapper; import com.android.launcher3.widget.LauncherAppWidgetHostView; @@ -49,6 +50,10 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler { Pair options = remoteResponse.getLaunchOptions(hostView); ActivityOptionsWrapper activityOptions = mLauncher.getAppTransitionManager() .getActivityLaunchOptions(mLauncher, hostView); + Object itemInfo = hostView.getTag(); + if (itemInfo instanceof ItemInfo) { + mLauncher.addLaunchCookie((ItemInfo) itemInfo, activityOptions.options); + } options = Pair.create(options.first, activityOptions.options); return RemoteViews.startPendingIntent(hostView, pendingIntent, options); } diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java index dd35d68dcf..99c5baf37d 100644 --- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java +++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java @@ -30,9 +30,11 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Rect; import android.graphics.RectF; import android.os.IBinder; import android.os.UserHandle; +import android.util.Size; import android.view.View; import androidx.annotation.NonNull; @@ -50,9 +52,11 @@ import com.android.launcher3.states.StateAnimationConfig; import com.android.launcher3.util.DynamicResource; import com.android.launcher3.util.ObjectWrapper; import com.android.launcher3.views.FloatingIconView; +import com.android.launcher3.widget.LauncherAppWidgetHostView; import com.android.quickstep.util.AppCloseConfig; import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.StaggeredWorkspaceAnim; +import com.android.quickstep.views.FloatingWidgetView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.plugins.ResourceProvider; @@ -77,155 +81,206 @@ public class LauncherSwipeHandlerV2 extends @Override protected HomeAnimationFactory createHomeAnimationFactory(ArrayList launchCookies, long duration) { - HomeAnimationFactory homeAnimFactory; - if (mActivity != null) { - final View workspaceView = findWorkspaceView(launchCookies, - mRecentsView.getRunningTaskView()); - boolean canUseWorkspaceView = - workspaceView != null && workspaceView.isAttachedToWindow(); - - mActivity.getRootView().setForceHideBackArrow(true); - mActivity.setHintUserWillBeActive(); - - if (canUseWorkspaceView) { - final ResourceProvider rp = DynamicResource.provider(mActivity); - final float transY = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp)); - float dpPerSecond = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp_per_s)); - final float launcherAlphaMax = - rp.getFloat(R.dimen.swipe_up_launcher_alpha_max_progress); - - RectF iconLocation = new RectF(); - FloatingIconView floatingIconView = getFloatingIconView(mActivity, workspaceView, - true /* hideOriginal */, iconLocation, false /* isOpening */); - - // 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. - float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION; - homeAnimFactory = new LauncherHomeAnimationFactory() { - - // There is a delay in loading the icon, so we need to keep the window - // opaque until it is ready. - private boolean mIsFloatingIconReady = false; - - private @Nullable ValueAnimator mBounceBackAnimator; - - @Override - public RectF getWindowTargetRect() { - if (PROTOTYPE_APP_CLOSE.get()) { - // We want the target rect to be at this offset position, so that all - // launcher content can spring back upwards. - floatingIconView.setPositionOffsetY(transY); - } - return iconLocation; - } - - @Override - public void setAnimation(RectFSpringAnim anim) { - anim.addAnimatorListener(floatingIconView); - floatingIconView.setOnTargetChangeListener(anim::onTargetPositionChanged); - floatingIconView.setFastFinishRunnable(anim::end); - if (PROTOTYPE_APP_CLOSE.get()) { - mBounceBackAnimator = bounceBackToRestingPosition(); - // Use a spring to put drag layer translation back to 0. - anim.addAnimatorListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - floatingIconView.setPositionOffsetY(0); - mBounceBackAnimator.start(); - } - }); - - Workspace workspace = mActivity.getWorkspace(); - workspace.setPivotToScaleWithSelf(mActivity.getHotseat()); - } - } - - private ValueAnimator bounceBackToRestingPosition() { - DragLayer dl = mActivity.getDragLayer(); - Workspace workspace = mActivity.getWorkspace(); - Hotseat hotseat = mActivity.getHotseat(); - - final float startValue = transY; - final float endValue = 0; - // Ensures the velocity is always aligned with the direction. - float pixelPerSecond = Math.abs(dpPerSecond) - * Math.signum(endValue - transY); - - ValueAnimator springTransY = new SpringAnimationBuilder(dl.getContext()) - .setStiffness(rp.getFloat(R.dimen.swipe_up_trans_y_stiffness)) - .setDampingRatio(rp.getFloat(R.dimen.swipe_up_trans_y_damping)) - .setMinimumVisibleChange(1f) - .setStartValue(startValue) - .setEndValue(endValue) - .setStartVelocity(pixelPerSecond) - .build(dl, VIEW_TRANSLATE_Y); - springTransY.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - dl.setTranslationY(0f); - dl.setAlpha(1f); - SCALE_PROPERTY.set(workspace, 1f); - SCALE_PROPERTY.set(hotseat, 1f); - } - }); - return springTransY; - } - - @Override - public boolean keepWindowOpaque() { - if (mIsFloatingIconReady || floatingIconView.isVisibleToUser()) { - mIsFloatingIconReady = true; - return false; - } - return true; - } - - @Override - public void update(@Nullable AppCloseConfig config, RectF currentRect, - float progress, float radius) { - int fgAlpha = 255; - if (config != null && PROTOTYPE_APP_CLOSE.get()) { - DragLayer dl = mActivity.getDragLayer(); - float translationY = config.getWorkspaceTransY(); - dl.setTranslationY(translationY); - - float alpha = mapToRange(progress, 0, launcherAlphaMax, 0, 1f, LINEAR); - dl.setAlpha(Math.min(alpha, 1f)); - - float scale = Math.min(1f, config.getWorkspaceScale()); - SCALE_PROPERTY.set(mActivity.getWorkspace(), scale); - SCALE_PROPERTY.set(mActivity.getHotseat(), scale); - SCALE_PROPERTY.set(mActivity.getAppsView(), scale); - - progress = config.getInterpolatedProgress(); - fgAlpha = config.getFgAlpha(); - } - floatingIconView.update(1f, fgAlpha, currentRect, progress, - windowAlphaThreshold, radius, false); - } - - @Override - public void onCancel() { - floatingIconView.fastFinish(); - if (mBounceBackAnimator != null) { - mBounceBackAnimator.cancel(); - } - } - }; - } else { - homeAnimFactory = new LauncherHomeAnimationFactory(); - } - } else { - homeAnimFactory = new HomeAnimationFactory() { + if (mActivity == null) { + mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, + isPresent -> mRecentsView.startHome()); + return new HomeAnimationFactory() { @Override public AnimatorPlaybackController createActivityAnimationToHome() { return AnimatorPlaybackController.wrap(new AnimatorSet(), duration); } }; - mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, - isPresent -> mRecentsView.startHome()); } - return homeAnimFactory; + + final View workspaceView = findWorkspaceView(launchCookies, + mRecentsView.getRunningTaskView()); + boolean canUseWorkspaceView = workspaceView != null && workspaceView.isAttachedToWindow(); + + mActivity.getRootView().setForceHideBackArrow(true); + mActivity.setHintUserWillBeActive(); + + if (!canUseWorkspaceView) { + return new LauncherHomeAnimationFactory(); + } + if (workspaceView instanceof LauncherAppWidgetHostView) { + return createWidgetHomeAnimationFactory((LauncherAppWidgetHostView) workspaceView); + } + return createIconHomeAnimationFactory(workspaceView); + } + + private HomeAnimationFactory createIconHomeAnimationFactory(View workspaceView) { + final ResourceProvider rp = DynamicResource.provider(mActivity); + final float transY = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp)); + float dpPerSecond = dpToPx(rp.getFloat(R.dimen.swipe_up_trans_y_dp_per_s)); + final float launcherAlphaMax = + rp.getFloat(R.dimen.swipe_up_launcher_alpha_max_progress); + + RectF iconLocation = new RectF(); + FloatingIconView floatingIconView = getFloatingIconView(mActivity, workspaceView, + true /* hideOriginal */, iconLocation, false /* isOpening */); + + // 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. + float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION; + return new LauncherHomeAnimationFactory() { + + // There is a delay in loading the icon, so we need to keep the window + // opaque until it is ready. + private boolean mIsFloatingIconReady = false; + + private @Nullable ValueAnimator mBounceBackAnimator; + + @Override + public RectF getWindowTargetRect() { + if (PROTOTYPE_APP_CLOSE.get()) { + // We want the target rect to be at this offset position, so that all + // launcher content can spring back upwards. + floatingIconView.setPositionOffsetY(transY); + } + return iconLocation; + } + + @Override + public void setAnimation(RectFSpringAnim anim) { + anim.addAnimatorListener(floatingIconView); + floatingIconView.setOnTargetChangeListener(anim::onTargetPositionChanged); + floatingIconView.setFastFinishRunnable(anim::end); + if (PROTOTYPE_APP_CLOSE.get()) { + mBounceBackAnimator = bounceBackToRestingPosition(); + // Use a spring to put drag layer translation back to 0. + anim.addAnimatorListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + floatingIconView.setPositionOffsetY(0); + mBounceBackAnimator.start(); + } + }); + + Workspace workspace = mActivity.getWorkspace(); + workspace.setPivotToScaleWithSelf(mActivity.getHotseat()); + } + } + + private ValueAnimator bounceBackToRestingPosition() { + DragLayer dl = mActivity.getDragLayer(); + Workspace workspace = mActivity.getWorkspace(); + Hotseat hotseat = mActivity.getHotseat(); + + final float startValue = transY; + final float endValue = 0; + // Ensures the velocity is always aligned with the direction. + float pixelPerSecond = Math.abs(dpPerSecond) * Math.signum(endValue - transY); + + ValueAnimator springTransY = new SpringAnimationBuilder(dl.getContext()) + .setStiffness(rp.getFloat(R.dimen.swipe_up_trans_y_stiffness)) + .setDampingRatio(rp.getFloat(R.dimen.swipe_up_trans_y_damping)) + .setMinimumVisibleChange(1f) + .setStartValue(startValue) + .setEndValue(endValue) + .setStartVelocity(pixelPerSecond) + .build(dl, VIEW_TRANSLATE_Y); + springTransY.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + dl.setTranslationY(0f); + dl.setAlpha(1f); + SCALE_PROPERTY.set(workspace, 1f); + SCALE_PROPERTY.set(hotseat, 1f); + } + }); + return springTransY; + } + + @Override + public boolean keepWindowOpaque() { + if (mIsFloatingIconReady || floatingIconView.isVisibleToUser()) { + mIsFloatingIconReady = true; + return false; + } + return true; + } + + @Override + public void update(@Nullable AppCloseConfig config, RectF currentRect, + float progress, float radius) { + int fgAlpha = 255; + if (config != null && PROTOTYPE_APP_CLOSE.get()) { + DragLayer dl = mActivity.getDragLayer(); + float translationY = config.getWorkspaceTransY(); + dl.setTranslationY(translationY); + + float alpha = mapToRange(progress, 0, launcherAlphaMax, 0, 1f, LINEAR); + dl.setAlpha(Math.min(alpha, 1f)); + + float scale = Math.min(1f, config.getWorkspaceScale()); + SCALE_PROPERTY.set(mActivity.getWorkspace(), scale); + SCALE_PROPERTY.set(mActivity.getHotseat(), scale); + SCALE_PROPERTY.set(mActivity.getAppsView(), scale); + + progress = config.getInterpolatedProgress(); + fgAlpha = config.getFgAlpha(); + } + floatingIconView.update(1f, fgAlpha, currentRect, progress, + windowAlphaThreshold, radius, false); + } + + @Override + public void onCancel() { + floatingIconView.fastFinish(); + if (mBounceBackAnimator != null) { + mBounceBackAnimator.cancel(); + } + } + }; + } + + private HomeAnimationFactory createWidgetHomeAnimationFactory( + LauncherAppWidgetHostView hostView) { + + RectF backgroundLocation = new RectF(); + Rect crop = new Rect(); + mTaskViewSimulator.getCurrentCropRect().roundOut(crop); + Size windowSize = new Size(crop.width(), crop.height()); + FloatingWidgetView floatingWidgetView = FloatingWidgetView.getFloatingWidgetView(mActivity, + hostView, backgroundLocation, windowSize, + mTaskViewSimulator.getCurrentCornerRadius()); + + return new LauncherHomeAnimationFactory() { + + @Override + public RectF getWindowTargetRect() { + return backgroundLocation; + } + + @Override + public float getEndRadius(RectF cropRectF) { + return floatingWidgetView.getInitialCornerRadius(); + } + + @Override + public void setAnimation(RectFSpringAnim anim) { + anim.addAnimatorListener(floatingWidgetView); + floatingWidgetView.setFastFinishRunnable(anim::end); + } + + @Override + public boolean keepWindowOpaque() { + return false; + } + + @Override + public void update(@Nullable AppCloseConfig config, RectF currentRect, + float progress, float radius) { + floatingWidgetView.update(currentRect, 1 /* floatingWidgetAlpha */, + config != null ? config.getFgAlpha() : 1f /* foregroundAlpha */, + 0 /* fallbackBackgroundAlpha */, 1 - progress); + } + + @Override + public void onCancel() { + floatingWidgetView.fastFinish(); + } + }; } /** diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java index 0f34a72ad4..29a00d16b0 100644 --- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java +++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java @@ -145,6 +145,11 @@ public abstract class SwipeUpAnimationLogic { targetX + halfIconSize, targetY + halfIconSize); } + /** Returns the corner radius of the window at the end of the animation. */ + public float getEndRadius(RectF cropRectF) { + return cropRectF.width() / 2f; + } + public abstract @NonNull AnimatorPlaybackController createActivityAnimationToHome(); public void playAtomicAnimation(float velocity) { @@ -197,8 +202,7 @@ public abstract class SwipeUpAnimationLogic { final RectF targetRect = homeAnimationFactory.getWindowTargetRect(); Matrix homeToWindowPositionMap = new Matrix(); - final RectF startRect = updateProgressForStartRect( - homeToWindowPositionMap, startProgress); + final RectF startRect = updateProgressForStartRect(homeToWindowPositionMap, startProgress); RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect()); // Move the startRect to Launcher space as floatingIconView runs in Launcher @@ -210,7 +214,7 @@ public abstract class SwipeUpAnimationLogic { if (PROTOTYPE_APP_CLOSE.get()) { anim = new RectFSpringAnim2(startRect, targetRect, mContext, mTaskViewSimulator.getCurrentCornerRadius(), - cropRectF.width() / 2f); + homeAnimationFactory.getEndRadius(cropRectF)); } else { anim = new RectFSpringAnim(startRect, targetRect, mContext); } @@ -269,7 +273,7 @@ public abstract class SwipeUpAnimationLogic { // End on a "round-enough" radius so that the shape reveal doesn't have to do too much // rounding at the end of the animation. mStartRadius = mTaskViewSimulator.getCurrentCornerRadius(); - mEndRadius = cropRectF.width() / 2f; + mEndRadius = factory.getEndRadius(cropRectF); } @Override diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java index d23884c9ef..8499902f23 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java +++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java @@ -20,10 +20,10 @@ import android.animation.Animator.AnimatorListener; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Matrix; -import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; import android.util.AttributeSet; +import android.util.Size; import android.view.GhostView; import android.view.View; import android.view.ViewGroup; @@ -113,7 +113,7 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener } private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView, - RectF widgetBackgroundPosition, Rect windowTargetBounds, float windowCornerRadius) { + RectF widgetBackgroundPosition, Size windowSize, float windowCornerRadius) { mAppWidgetView = originalView; mAppWidgetView.beginDeferringUpdates(); mBackgroundPosition = widgetBackgroundPosition; @@ -128,7 +128,7 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener getRelativePosition(mAppWidgetBackgroundView, mAppWidgetView, mBackgroundOffset); mBackgroundView.init(mAppWidgetView, mAppWidgetBackgroundView, windowCornerRadius); // Layout call before GhostView creation so that the overlaid view isn't clipped - layout(0, 0, windowTargetBounds.width(), windowTargetBounds.height()); + layout(0, 0, windowSize.getWidth(), windowSize.getHeight()); mForegroundOverlayView = GhostView.addGhost(mAppWidgetView, this); positionViews(); @@ -219,19 +219,19 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener * * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's * background bounds - * @param windowTargetBounds the bounds of the window when launched + * @param windowSize the size of the window when launched * @param windowCornerRadius the corner radius of the window */ public static FloatingWidgetView getFloatingWidgetView(Launcher launcher, LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition, - Rect windowTargetBounds, float windowCornerRadius) { + Size windowSize, float windowCornerRadius) { final DragLayer dragLayer = launcher.getDragLayer(); ViewGroup parent = (ViewGroup) dragLayer.getParent(); FloatingWidgetView floatingView = launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent); floatingView.recycle(); - floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowTargetBounds, + floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowSize, windowCornerRadius); parent.addView(floatingView); return floatingView; @@ -240,7 +240,7 @@ public class FloatingWidgetView extends FrameLayout implements AnimatorListener private static void getRelativePosition(View descendant, View ancestor, RectF position) { float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()}; Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points, - false /* includeRootScroll */); + false /* includeRootScroll */, true /* ignoreTransform */); position.set( Math.min(points[0], points[2]), Math.min(points[1], points[3]),