From beb3053f3bbdb99afcd635baa24a66128a21325e Mon Sep 17 00:00:00 2001 From: Alex Chau Date: Tue, 30 Jan 2024 12:37:09 +0000 Subject: [PATCH] Use bigger task size in app to overview carousel - Introduced carouselTaskSize that represent size of TaskView in app to overview carousel, that is bigger than size of TaskView in Overview state - Use nonGridScale and translation to scale TaskView to the desired carousel size - For current task, inroduced carouselScale and translation to apply similar transformation at the carousel. They will be reset in `onPrepareGestureEndAnimation` after gesture is released and form the grid. Carousel translation can be invalidated and aniamte to 0. - Fixed current task left/right wiggle that is caused by task shrinks and translate in different direction. Pivot is now moved to top right (or top left for RTL), to align with movement of current task. - To compensate for the pivot change, current task is translated back to the carousel position by taskTranslation, and again translated by carouselTranslation for carousel -> fullScreen scaling. A complex interpolator is introduced to make current task moves in a vertical straight line rather than a curve. - Fixed a bug in AnimatorControllerWithResistance when scaleStartResist==scaleMaxResist that causes division by 0. For grid overview, resistance kicks in after reaching carousel size, and current task size won't reduce further - Added PendingAnimation#addAnimatedFloat that uses animator provided by AnimatedFloat, so the animator can be canceled and reaniamte from AnimatedFloat side; AnimatedFloat now clears the property values during cancel so the canceled animator still referenced by PendingAnimation can no longer change the values Fix: 318352235 Fix: 308643507 Flag: ACONFIG com.android.launcher3.enable_grid_only_overview TEAMFOOD Test: presubmit Change-Id: I2872d8b2204798fe5e05c10d08480a81e60bb498 --- quickstep/res/values/dimens.xml | 5 +- .../android/quickstep/AbsSwipeUpHandler.java | 7 +- .../quickstep/BaseActivityInterface.java | 21 +++- .../AnimatorControllerWithResistance.java | 10 +- .../quickstep/util/TaskViewSimulator.java | 108 ++++++++++++++++-- .../android/quickstep/views/RecentsView.java | 35 ++++-- .../com/android/quickstep/views/TaskView.java | 9 +- .../android/launcher3/anim/AnimatedFloat.java | 7 ++ .../launcher3/anim/PendingAnimation.java | 14 +++ 9 files changed, 188 insertions(+), 28 deletions(-) diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 68bad5c85f..853ac7415c 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -32,8 +32,11 @@ 50dp - + 0.7 + + 0.46 48dp diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 6698600f90..4752225ec6 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -30,6 +30,7 @@ import static com.android.launcher3.BaseActivity.EVENT_DESTROYED; import static com.android.launcher3.BaseActivity.EVENT_STARTED; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; +import static com.android.launcher3.Flags.enableGridOnlyOverview; import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD; import static com.android.launcher3.PagedView.INVALID_PAGE; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; @@ -2561,9 +2562,11 @@ public abstract class AbsSwipeUpHandler, } float scrollOffset = Math.abs(mRecentsView.getScrollOffset(mRecentsView.getCurrentPage())); + Rect carouselTaskSize = enableGridOnlyOverview() + ? mRecentsView.getLastComputedCarouselTaskSize() + : mRecentsView.getLastComputedTaskSize(); int maxScrollOffset = mRecentsView.getPagedOrientationHandler().getPrimaryValue( - mRecentsView.getLastComputedTaskSize().width(), - mRecentsView.getLastComputedTaskSize().height()); + carouselTaskSize.width(), carouselTaskSize.height()); maxScrollOffset += mRecentsView.getPageSpacing(); float maxScaleProgress = diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java index b89d20ca97..879312d032 100644 --- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java +++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java @@ -261,6 +261,23 @@ public abstract class BaseActivityInterface { - if (t < startResist) { + if (t <= startResist) { return t; } - if (t > stopResist) { + if (t >= stopResist) { return maxResist; } float resistProgress = Utilities.getProgress(t, startResist, stopResist); @@ -304,7 +306,9 @@ public class AnimatorControllerWithResistance { resistanceParams = recentsOrientedState.getActivityInterface().allowAllAppsFromOverview() ? RecentsResistanceParams.FROM_APP_TO_ALL_APPS_TABLET - : RecentsResistanceParams.FROM_APP_TABLET; + : enableGridOnlyOverview() + ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY + : RecentsResistanceParams.FROM_APP_TABLET; } else { resistanceParams = recentsOrientedState.getActivityInterface().allowAllAppsFromOverview() diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index 0bb6b23dca..1152de2750 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -38,10 +38,12 @@ import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; import android.view.RemoteAnimationTarget; +import android.view.animation.Interpolator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.app.animation.Interpolators; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; @@ -76,6 +78,8 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { private final Rect mTaskRect = new Rect(); private final Rect mFullTaskSize = new Rect(); + private final Rect mCarouselTaskSize = new Rect(); + private PointF mPivotOverride = null; private final PointF mPivot = new PointF(); private DeviceProfile mDp; @StagePosition @@ -95,6 +99,11 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat(); public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat(); + // Carousel properties + public final AnimatedFloat carouselScale = new AnimatedFloat(); + public final AnimatedFloat carouselPrimaryTranslation = new AnimatedFloat(); + public final AnimatedFloat carouselSecondaryTranslation = new AnimatedFloat(); + // RecentsView properties public final AnimatedFloat recentsViewScale = new AnimatedFloat(); public final AnimatedFloat fullScreenProgress = new AnimatedFloat(); @@ -109,9 +118,9 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { private Boolean mDrawsBelowRecents = null; private boolean mIsGridTask; private boolean mIsDesktopTask; + private boolean mScaleToCarouselTaskSize = false; private int mTaskRectTranslationX; private int mTaskRectTranslationY; - private int mPivotOffsetX; public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) { mContext = context; @@ -124,6 +133,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mOrientationStateId = mOrientationState.getStateId(); Resources resources = context.getResources(); mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources); + carouselScale.value = 1f; } /** @@ -149,6 +159,11 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mOrientationState.getOrientationHandler()); } + if (enableGridOnlyOverview()) { + mSizeStrategy.calculateCarouselTaskSize(mContext, mDp, mCarouselTaskSize, + mOrientationState.getOrientationHandler()); + } + if (mSplitBounds != null) { // The task rect changes according to the staged split task sizes, but recents // fullscreen scale and pivot remains the same since the task fits into the existing @@ -193,9 +208,18 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { } // Copy mFullTaskSize instead of updating it directly so it could be reused next time // without recalculating - Rect scaleRect = new Rect(mFullTaskSize); - scaleRect.offset(mTaskRectTranslationX + mPivotOffsetX, mTaskRectTranslationY); - return mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot); + Rect scaleRect = new Rect(); + if (mScaleToCarouselTaskSize) { + scaleRect.set(mCarouselTaskSize); + } else { + scaleRect.set(mFullTaskSize); + } + scaleRect.offset(mTaskRectTranslationX, mTaskRectTranslationY); + float scale = mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot); + if (mPivotOverride != null) { + mPivot.set(mPivotOverride); + } + return scale; } /** @@ -278,14 +302,64 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { /** * Adds animation for all the components corresponding to transition from an app to overview. */ - public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) { + public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) { pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator); - if (enableGridOnlyOverview() && mDp.isTablet) { - int translationXToMiddle = mDp.widthPx / 2 - mFullTaskSize.centerX(); - taskPrimaryTranslation.value = translationXToMiddle; - mPivotOffsetX = translationXToMiddle; + float fullScreenScale; + if (enableGridOnlyOverview() && mDp.isTablet && mDp.isGestureMode) { + // Move pivot to top right edge of the screen, to avoid task scaling down in opposite + // direction of app window movement, otherwise the animation will wiggle left and right. + // Also translate the app window to top right edge of the screen to simplify + // calculations. + taskPrimaryTranslation.value = mIsRecentsRtl + ? mDp.widthPx - mFullTaskSize.right + : -mFullTaskSize.left; + taskSecondaryTranslation.value = -mFullTaskSize.top; + mPivotOverride = new PointF(mIsRecentsRtl ? mDp.widthPx : 0, 0); + + // Scale down to the carousel and use the carousel Rect to calculate fullScreenScale. + mScaleToCarouselTaskSize = true; + carouselScale.value = mCarouselTaskSize.width() / (float) mFullTaskSize.width(); + fullScreenScale = getFullScreenScale(); + + float carouselPrimaryTranslationTarget = mIsRecentsRtl + ? mCarouselTaskSize.right - mDp.widthPx + : mCarouselTaskSize.left; + float carouselSecondaryTranslationTarget = mCarouselTaskSize.top; + + // Expected carousel position's center is in the middle, and invariant of + // recentsViewScale. + float exceptedCarouselCenterX = mCarouselTaskSize.centerX(); + // Animating carousel translations linearly will result in a curved path, therefore + // we'll need to calculate the expected translation at each recentsView scale. Luckily + // primary and secondary follow the same translation, and primary is used here due to + // it being simpler. + Interpolator carouselTranslationInterpolator = t -> { + // recentsViewScale is calculated rather than using recentsViewScale.value, so that + // this interpolator works independently even if recentsViewScale don't animate. + float recentsViewScale = + Utilities.mapToRange(t, 0, 1, fullScreenScale, 1, Interpolators.LINEAR); + // Without the translation, the app window will animate from fullscreen into top + // right corner. + float expectedTaskCenterX = mIsRecentsRtl + ? mDp.widthPx - mCarouselTaskSize.width() * recentsViewScale / 2f + : mCarouselTaskSize.width() * recentsViewScale / 2f; + // Calculate the expected translation, then work back the animatedFraction that + // results in this value. + float carouselPrimaryTranslation = + (exceptedCarouselCenterX - expectedTaskCenterX) / recentsViewScale; + return carouselPrimaryTranslation / carouselPrimaryTranslationTarget; + }; + + // Use addAnimatedFloat so this animation can later be canceled and animate to a + // different value in RecentsView.onPrepareGestureEndAnimation. + pa.addAnimatedFloat(carouselPrimaryTranslation, 0, carouselPrimaryTranslationTarget, + carouselTranslationInterpolator); + pa.addAnimatedFloat(carouselSecondaryTranslation, 0, carouselSecondaryTranslationTarget, + carouselTranslationInterpolator); + } else { + fullScreenScale = getFullScreenScale(); } - pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1, interpolator); + pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, fullScreenScale, 1, interpolator); } /** @@ -382,7 +456,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1); mCurrentFullscreenParams.setProgress(fullScreenProgress, recentsViewScale.value, - /* taskViewScale= */1f); + carouselScale.value); // Apply thumbnail matrix float taskWidth = mTaskRect.width(); @@ -396,6 +470,13 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { taskPrimaryTranslation.value); mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE, taskSecondaryTranslation.value); + + mMatrix.postScale(carouselScale.value, carouselScale.value, mPivot.x, mPivot.y); + mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE, + carouselPrimaryTranslation.value); + mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE, + carouselSecondaryTranslation.value); + mOrientationState.getOrientationHandler().setPrimary( mMatrix, MATRIX_POST_TRANSLATE, recentsViewScroll.value); @@ -420,15 +501,18 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { return; } Log.d(TAG, "progress: " + fullScreenProgress + + " carouselScale: " + carouselScale.value + " recentsViewScale: " + recentsViewScale.value + " crop: " + mTmpCropRect + " radius: " + getCurrentCornerRadius() + " taskW: " + taskWidth + " H: " + taskHeight + " taskRect: " + mTaskRect + " taskPrimaryT: " + taskPrimaryTranslation.value + + " taskSecondaryT: " + taskSecondaryTranslation.value + + " carouselPrimaryT: " + carouselPrimaryTranslation.value + + " carouselSecondaryT: " + carouselSecondaryTranslation.value + " recentsPrimaryT: " + recentsViewPrimaryTranslation.value + " recentsSecondaryT: " + recentsViewSecondaryTranslation.value - + " taskSecondaryT: " + taskSecondaryTranslation.value + " recentsScroll: " + recentsViewScroll.value + " pivot: " + mPivot ); diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 9884d8dd1d..f6afaf0043 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -459,6 +459,7 @@ public abstract class RecentsView lastTaskScroll)) { pageScroll = lastTaskScroll; diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index 5057c383e5..55da1601b6 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -23,6 +23,7 @@ import static android.widget.Toast.LENGTH_SHORT; import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.Flags.enableCursorHoverStates; +import static com.android.launcher3.Flags.enableGridOnlyOverview; import static com.android.launcher3.Flags.enableOverviewIconMenu; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor; @@ -1750,7 +1751,13 @@ public class TaskView extends FrameLayout implements Reusable { expectedHeight = boxHeight + thumbnailPadding; // Scale to to fit task Rect. - nonGridScale = taskWidth / (float) boxWidth; + if (enableGridOnlyOverview()) { + final Rect lastComputedCarouselTaskSize = + getRecentsView().getLastComputedCarouselTaskSize(); + nonGridScale = lastComputedCarouselTaskSize.width() / (float) taskWidth; + } else { + nonGridScale = taskWidth / (float) boxWidth; + } // Align to top of task Rect. boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f; diff --git a/src/com/android/launcher3/anim/AnimatedFloat.java b/src/com/android/launcher3/anim/AnimatedFloat.java index 2f3fa63321..b414ab6e35 100644 --- a/src/com/android/launcher3/anim/AnimatedFloat.java +++ b/src/com/android/launcher3/anim/AnimatedFloat.java @@ -109,6 +109,13 @@ public class AnimatedFloat { public void cancelAnimation() { if (mValueAnimator != null) { mValueAnimator.cancel(); + // Clears the property values, so further ObjectAnimator#setCurrentFraction from e.g. + // AnimatorPlaybackController calls would do nothing. The null check is necessary to + // avoid mValueAnimator being set to null in onAnimationEnd. + if (mValueAnimator != null) { + mValueAnimator.setValues(); + mValueAnimator = null; + } } } diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java index 586beb2d5e..e58890f7ca 100644 --- a/src/com/android/launcher3/anim/PendingAnimation.java +++ b/src/com/android/launcher3/anim/PendingAnimation.java @@ -83,6 +83,20 @@ public class PendingAnimation extends AnimatedPropertySetter { add(anim); } + /** + * Add an {@link AnimatedFloat} to the animation. + *

+ * Different from {@link #addFloat}, this method use animator provided by + * {@link AnimatedFloat#animateToValue}, which tracks the animator inside the AnimatedFloat, + * allowing the animation to be canceled and animate again from AnimatedFloat side. + */ + public void addAnimatedFloat(AnimatedFloat target, float from, float to, + TimeInterpolator interpolator) { + Animator anim = target.animateToValue(from, to); + anim.setInterpolator(interpolator); + add(anim); + } + /** If trace is enabled, add counter to trace animation progress. */ public void logAnimationProgressToTrace(String counterName) { if (Trace.isEnabled()) {