From 0d447c88b8649abf14feb2736d320ccc55f44d03 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 6 Mar 2019 11:11:54 -0800 Subject: [PATCH] Apply spring forces to animate to the final position for swipe home Now instead of an incorrect hack that simulated accelerating to the target, we actually apply spring forces to make it feel realistic and work no matter where the target is. Added two helper classes for this: - FlingSpringAnim handles the fling, applying friction until reaching the target, then a spring to pull towards the final position (also applies if fling wasn't in the right direction or strong enough to reach the target). - RectFSpringAnim uses 2 FlingSpringAnims (x + y) to animate from a starting rect to a target rect. It also has an animation to scale from the start rect to the target rect, sending progress update callbacks to the caller. Bug: 123900446 Change-Id: Iafa89db1d55c42816acfa9f1bb84a7519b69ff12 --- .../FallbackActivityControllerHelper.java | 8 +- .../LauncherActivityControllerHelper.java | 5 +- .../quickstep/OtherActivityInputConsumer.java | 10 +- .../WindowTransformSwipeHandler.java | 109 +++++------ .../quickstep/util/RectFSpringAnim.java | 179 ++++++++++++++++++ .../quickstep/ActivityControlHelper.java | 3 +- .../launcher3/anim/FlingSpringAnim.java | 60 ++++++ .../launcher3/views/FloatingIconView.java | 10 +- 8 files changed, 312 insertions(+), 72 deletions(-) create mode 100644 quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java create mode 100644 src/com/android/launcher3/anim/FlingSpringAnim.java diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java index d61ed724dd..ef46b3b0d9 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java @@ -21,7 +21,6 @@ import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; -import android.content.ComponentName; import android.content.Context; import android.graphics.Rect; import android.graphics.RectF; @@ -90,7 +89,7 @@ public final class FallbackActivityControllerHelper implements @NonNull @Override - public Animator createActivityAnimationToHome() { + public AnimatorPlaybackController createActivityAnimationToHome() { Animator anim = ObjectAnimator.ofFloat(recentsView, CONTENT_ALPHA, 0); anim.addListener(new AnimationSuccessListener() { @Override @@ -98,7 +97,10 @@ public final class FallbackActivityControllerHelper implements recentsView.startHome(); } }); - return anim; + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.play(anim); + long accuracy = 2 * Math.max(recentsView.getWidth(), recentsView.getHeight()); + return AnimatorPlaybackController.wrap(animatorSet, accuracy); } }; } 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 e95e2a0de5..b0bd71bf03 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java @@ -144,10 +144,9 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe @NonNull @Override - public Animator createActivityAnimationToHome() { + public AnimatorPlaybackController createActivityAnimationToHome() { long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx); - return activity.getStateManager().createAnimationToNewWorkspace( - NORMAL, accuracy).getTarget(); + return activity.getStateManager().createAnimationToNewWorkspace(NORMAL, accuracy); } }; } diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java index 4e010d25e7..4792cc7916 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OtherActivityInputConsumer.java @@ -21,7 +21,6 @@ import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.INVALID_POINTER_ID; - import static com.android.launcher3.util.RaceConditionTracker.ENTER; import static com.android.launcher3.util.RaceConditionTracker.EXIT; import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR; @@ -45,8 +44,6 @@ import android.view.VelocityTracker; import android.view.ViewConfiguration; import android.view.WindowManager; -import androidx.annotation.UiThread; - import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.RaceConditionTracker; @@ -63,6 +60,8 @@ import com.android.systemui.shared.system.WindowManagerWrapper; import java.util.function.Consumer; +import androidx.annotation.UiThread; + /** * Input consumer for handling events originating from an activity other than Launcher */ @@ -331,12 +330,13 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC mVelocityTracker.computeCurrentVelocity(1000, ViewConfiguration.get(this).getScaledMaximumFlingVelocity()); float velocityX = mVelocityTracker.getXVelocity(mActivePointerId); + float velocityY = mVelocityTracker.getYVelocity(mActivePointerId); float velocity = isNavBarOnRight() ? velocityX : isNavBarOnLeft() ? -velocityX - : mVelocityTracker.getYVelocity(mActivePointerId); + : velocityY; mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement); - mInteractionHandler.onGestureEnded(velocity, velocityX); + mInteractionHandler.onGestureEnded(velocity, new PointF(velocityX, velocityY)); } else { // Since we start touch tracking on DOWN, we may reach this state without actually // starting the gesture. In that case, just cleanup immediately. 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 fd53f9c87c..efc228b596 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java @@ -43,12 +43,12 @@ import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.graphics.Canvas; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; @@ -85,6 +85,7 @@ import com.android.quickstep.ActivityControlHelper.AnimationFactory; import com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState; import com.android.quickstep.ActivityControlHelper.HomeAnimationFactory; import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.quickstep.util.SwipeAnimationTargetSet; import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener; @@ -92,7 +93,6 @@ import com.android.quickstep.views.LiveTileOverlay; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.ThumbnailData; -import com.android.systemui.shared.recents.utilities.RectFEvaluator; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.LatencyTrackerCompat; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; @@ -677,15 +677,19 @@ public class WindowTransformSwipeHandler } } + /** + * @param endVelocity The velocity in the direction of the nav bar to the middle of the screen. + * @param velocity The x and y components of the velocity when the gesture ends. + */ @UiThread - public void onGestureEnded(float endVelocity, float velocityX) { + public void onGestureEnded(float endVelocity, PointF velocity) { float flingThreshold = mContext.getResources() .getDimension(R.dimen.quickstep_fling_threshold_velocity); boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; setStateOnUiThread(STATE_GESTURE_COMPLETED); mLogAction = isFling ? Touch.FLING : Touch.SWIPE; - handleNormalGestureEnd(endVelocity, isFling, velocityX); + handleNormalGestureEnd(endVelocity, isFling, velocity); } @UiThread @@ -703,9 +707,8 @@ public class WindowTransformSwipeHandler } @UiThread - private void handleNormalGestureEnd(float endVelocity, boolean isFling, float velocityX) { - float velocityPxPerMs = endVelocity / 1000; - float velocityXPxPerMs = velocityX / 1000; + private void handleNormalGestureEnd(float endVelocity, boolean isFling, PointF velocity) { + PointF velocityPxPerMs = new PointF(velocity.x / 1000, velocity.y / 1000); long duration = MAX_SWIPE_DURATION; float currentShift = mCurrentShift.value; final GestureEndTarget endTarget; @@ -750,7 +753,7 @@ public class WindowTransformSwipeHandler } else { if (SWIPE_HOME.get() && endVelocity < 0 && !mIsShelfPeeking) { // If swiping at a diagonal, base end target on the faster velocity. - endTarget = goingToNewTask && Math.abs(velocityX) > Math.abs(endVelocity) + endTarget = goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity) ? NEW_TASK : HOME; } else if (endVelocity < 0 && (!goingToNewTask || reachedOverviewThreshold)) { // If user scrolled to a new task, only go to recents if they already passed @@ -760,14 +763,15 @@ public class WindowTransformSwipeHandler endTarget = goingToNewTask ? NEW_TASK : LAST_TASK; } endShift = endTarget.endShift; - startShift = Utilities.boundToRange(currentShift - velocityPxPerMs + startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1); float minFlingVelocity = mContext.getResources() .getDimension(R.dimen.quickstep_fling_min_velocity); if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { if (endTarget == RECENTS) { Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams( - startShift, endShift, endShift, velocityPxPerMs, mTransitionDragLength); + startShift, endShift, endShift, velocityPxPerMs.y, + mTransitionDragLength); endShift = overshoot.end; interpolator = overshoot.interpolator; duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION, @@ -778,7 +782,7 @@ public class WindowTransformSwipeHandler // we want the page's snap velocity to approximately match the velocity at // which the user flings, so we scale the duration by a value near to the // derivative of the scroll interpolator at zero, ie. 2. - long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs)); + long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y)); duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); } } @@ -835,14 +839,14 @@ public class WindowTransformSwipeHandler /** Animates to the given progress, where 0 is the current app and 1 is overview. */ @UiThread private void animateToProgress(float start, float end, long duration, Interpolator interpolator, - GestureEndTarget target, float velocityPxPerMs) { + GestureEndTarget target, PointF velocityPxPerMs) { mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration, interpolator, target, velocityPxPerMs)); } @UiThread private void animateToProgressInternal(float start, float end, long duration, - Interpolator interpolator, GestureEndTarget target, float velocityPxPerMs) { + Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) { mGestureEndTarget = target; if (mGestureEndTarget.canBeContinued) { @@ -856,7 +860,6 @@ public class WindowTransformSwipeHandler } HomeAnimationFactory homeAnimFactory; - Animator windowAnim; if (mGestureEndTarget == HOME) { if (mActivity != null) { homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity); @@ -872,27 +875,34 @@ public class WindowTransformSwipeHandler @NonNull @Override - public Animator createActivityAnimationToHome() { - return new AnimatorSet(); + public AnimatorPlaybackController createActivityAnimationToHome() { + return AnimatorPlaybackController.wrap(new AnimatorSet(), duration); } }; mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, isPresent -> mRecentsView.startHome()); } - windowAnim = createWindowAnimationToHome(start, homeAnimFactory); + RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory); + windowAnim.addAnimatorListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + setStateOnUiThread(target.endState); + } + }); + windowAnim.start(velocityPxPerMs); mLauncherTransitionController = null; } else { - windowAnim = mCurrentShift.animateToValue(start, end); + Animator windowAnim = mCurrentShift.animateToValue(start, end); + windowAnim.setDuration(duration).setInterpolator(interpolator); + windowAnim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + setStateOnUiThread(target.endState); + } + }); + windowAnim.start(); homeAnimFactory = null; } - windowAnim.setDuration(duration).setInterpolator(interpolator); - windowAnim.addListener(new AnimationSuccessListener() { - @Override - public void onAnimationSuccess(Animator animator) { - setStateOnUiThread(target.endState); - } - }); - windowAnim.start(); // Always play the entire launcher animation when going home, since it is separate from // the animation that has been controlled thus far. if (mGestureEndTarget == HOME) { @@ -903,12 +913,6 @@ public class WindowTransformSwipeHandler // interpolate over the remaining progress (end - start). TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress( interpolator, start, end); - if (homeAnimFactory != null) { - Animator homeAnim = homeAnimFactory.createActivityAnimationToHome(); - homeAnim.setDuration(duration).setInterpolator(adjustedInterpolator); - homeAnim.start(); - mLauncherTransitionController = null; - } if (mLauncherTransitionController == null) { return; } @@ -920,50 +924,40 @@ public class WindowTransformSwipeHandler mLauncherTransitionController.getAnimationPlayer().setDuration(duration); if (QUICKSTEP_SPRINGS.get()) { - mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs); + mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y); } mLauncherTransitionController.getAnimationPlayer().start(); } } /** - * Creates an Animator that transforms the current app window into the home app. + * Creates an animation that transforms the current app window into the home app. * @param startProgress The progress of {@link #mCurrentShift} to start the window from. * @param homeAnimationFactory The home animation factory. */ - private Animator createWindowAnimationToHome(float startProgress, + private RectFSpringAnim createWindowAnimationToHome(float startProgress, HomeAnimationFactory homeAnimationFactory) { final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; - RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet, + final RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet, mTransformParams.setProgress(startProgress))); - RectF originalTarget = new RectF(mClipAnimationHelper.getTargetRect()); - final RectF finalTarget = homeAnimationFactory.getWindowTargetRect(); - - final RectFEvaluator rectFEvaluator = new RectFEvaluator(); - final RectF targetRect = new RectF(); - final RectF currentRect = new RectF(); + final RectF targetRect = homeAnimationFactory.getWindowTargetRect(); final View floatingView = homeAnimationFactory.getFloatingView(); final boolean isFloatingIconView = floatingView instanceof FloatingIconView; - ValueAnimator anim = ValueAnimator.ofFloat(0, 1); + RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect); if (isFloatingIconView) { - anim.addListener((FloatingIconView) floatingView); + anim.addAnimatorListener((FloatingIconView) floatingView); } + AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome(); // 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(); + anim.addOnUpdateListener((currentRect, 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(interpolatedProgress, startRect, targetRect)); + + homeAnim.setPlayFraction(progress); float iconAlpha = Utilities.mapToRange(interpolatedProgress, 0, windowAlphaThreshold, 0f, 1f, Interpolators.LINEAR); @@ -975,10 +969,17 @@ public class WindowTransformSwipeHandler ((FloatingIconView) floatingView).update(currentRect, iconAlpha, progress, windowAlphaThreshold); } + }); - anim.addListener(new AnimationSuccessListener() { + anim.addAnimatorListener(new AnimationSuccessListener() { + @Override + public void onAnimationStart(Animator animation) { + homeAnim.dispatchOnStart(); + } + @Override public void onAnimationSuccess(Animator animator) { + homeAnim.getAnimationPlayer().end(); if (mRecentsView != null) { mRecentsView.post(mRecentsView::resetTaskVisuals); } diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java new file mode 100644 index 0000000000..2edeb3aec2 --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java @@ -0,0 +1,179 @@ +/* + * 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.quickstep.util; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.FloatProperty; + +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.FlingSpringAnim; + +import java.util.ArrayList; +import java.util.List; + +import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener; +import androidx.dynamicanimation.animation.FloatPropertyCompat; + +/** + * Applies spring forces to animate from a starting rect to a target rect, + * while providing update callbacks to the caller. + */ +public class RectFSpringAnim { + + /** + * Although the rect position animation takes an indefinite amount of time since it depends on + * the initial velocity and applied forces, scaling from the starting rect to the target rect + * can be done in parallel at a fixed duration. Update callbacks are sent based on the progress + * of this animation, while the end callback is sent after all animations finish. + */ + private static final long RECT_SCALE_DURATION = 180; + + private static final FloatPropertyCompat RECT_CENTER_X = + new FloatPropertyCompat("rectCenterXSpring") { + @Override + public float getValue(RectFSpringAnim anim) { + return anim.mCurrentCenterX; + } + + @Override + public void setValue(RectFSpringAnim anim, float currentCenterX) { + anim.mCurrentCenterX = currentCenterX; + anim.onUpdate(); + } + }; + + private static final FloatPropertyCompat RECT_CENTER_Y = + new FloatPropertyCompat("rectCenterYSpring") { + @Override + public float getValue(RectFSpringAnim anim) { + return anim.mCurrentCenterY; + } + + @Override + public void setValue(RectFSpringAnim anim, float currentCenterY) { + anim.mCurrentCenterY = currentCenterY; + anim.onUpdate(); + } + }; + + private static final FloatProperty RECT_SCALE_PROGRESS = + new FloatProperty("rectScaleProgress") { + @Override + public Float get(RectFSpringAnim anim) { + return anim.mCurrentScaleProgress; + } + + @Override + public void setValue(RectFSpringAnim anim, float currentScaleProgress) { + anim.mCurrentScaleProgress = currentScaleProgress; + anim.onUpdate(); + } + }; + + private final RectF mStartRect; + private final RectF mTargetRect; + private final RectF mCurrentRect = new RectF(); + private final List mOnUpdateListeners = new ArrayList<>(); + private final List mAnimatorListeners = new ArrayList<>(); + + private float mCurrentCenterX; + private float mCurrentCenterY; + private float mCurrentScaleProgress; + private boolean mRectXAnimEnded; + private boolean mRectYAnimEnded; + private boolean mRectScaleAnimEnded; + + public RectFSpringAnim(RectF startRect, RectF targetRect) { + mStartRect = startRect; + mTargetRect = targetRect; + mCurrentCenterX = mStartRect.centerX(); + mCurrentCenterY = mStartRect.centerY(); + } + + public void addOnUpdateListener(OnUpdateListener onUpdateListener) { + mOnUpdateListeners.add(onUpdateListener); + } + + public void addAnimatorListener(Animator.AnimatorListener animatorListener) { + mAnimatorListeners.add(animatorListener); + } + + public void start(PointF velocityPxPerMs) { + // Only tell caller that we ended if both x and y animations have ended. + OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> { + mRectXAnimEnded = true; + maybeOnEnd(); + }); + OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> { + mRectYAnimEnded = true; + maybeOnEnd(); + }); + FlingSpringAnim rectXAnim = new FlingSpringAnim(this, RECT_CENTER_X, mCurrentCenterX, + mTargetRect.centerX(), velocityPxPerMs.x * 1000, onXEndListener); + FlingSpringAnim rectYAnim = new FlingSpringAnim(this, RECT_CENTER_Y, mCurrentCenterY, + mTargetRect.centerY(), velocityPxPerMs.y * 1000, onYEndListener); + + ValueAnimator rectScaleAnim = ObjectAnimator.ofPropertyValuesHolder(this, + PropertyValuesHolder.ofFloat(RECT_SCALE_PROGRESS, 1)) + .setDuration(RECT_SCALE_DURATION); + rectScaleAnim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + mRectScaleAnimEnded = true; + maybeOnEnd(); + } + }); + + rectXAnim.start(); + rectYAnim.start(); + rectScaleAnim.start(); + for (Animator.AnimatorListener animatorListener : mAnimatorListeners) { + animatorListener.onAnimationStart(null); + } + } + + private void onUpdate() { + if (!mOnUpdateListeners.isEmpty()) { + float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(), + mTargetRect.width()); + float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(), + mTargetRect.height()); + mCurrentRect.set(mCurrentCenterX - currentWidth / 2, mCurrentCenterY - currentHeight / 2, + mCurrentCenterX + currentWidth / 2, mCurrentCenterY + currentHeight / 2); + for (OnUpdateListener onUpdateListener : mOnUpdateListeners) { + onUpdateListener.onUpdate(mCurrentRect, mCurrentScaleProgress); + } + } + } + + private void maybeOnEnd() { + if (mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded) { + for (Animator.AnimatorListener animatorListener : mAnimatorListeners) { + animatorListener.onAnimationEnd(null); + } + } + } + + public interface OnUpdateListener { + void onUpdate(RectF currentRect, float progress); + } +} diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java index 75be2e48e1..418f7f4424 100644 --- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java +++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java @@ -15,7 +15,6 @@ */ package com.android.quickstep; -import android.animation.Animator; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; @@ -132,6 +131,6 @@ public interface ActivityControlHelper { @NonNull RectF getWindowTargetRect(); - @NonNull Animator createActivityAnimationToHome(); + @NonNull AnimatorPlaybackController createActivityAnimationToHome(); } } diff --git a/src/com/android/launcher3/anim/FlingSpringAnim.java b/src/com/android/launcher3/anim/FlingSpringAnim.java new file mode 100644 index 0000000000..3d21d82a25 --- /dev/null +++ b/src/com/android/launcher3/anim/FlingSpringAnim.java @@ -0,0 +1,60 @@ +/* + * 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.anim; + +import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener; +import androidx.dynamicanimation.animation.FlingAnimation; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +/** + * Given a property to animate and a target value and starting velocity, first apply friction to + * the fling until we pass the target, then apply a spring force to pull towards the target. + */ +public class FlingSpringAnim { + + private static final float FLING_FRICTION = 1.5f; + // Have the spring pull towards the target if we've slowed down too much before reaching it. + private static final float FLING_END_THRESHOLD_PX = 50f; + private static final float SPRING_STIFFNESS = 350f; + private static final float SPRING_DAMPING = SpringForce.DAMPING_RATIO_LOW_BOUNCY; + + private final FlingAnimation mFlingAnim; + + public FlingSpringAnim(K object, FloatPropertyCompat property, float startPosition, + float targetPosition, float startVelocity, OnAnimationEndListener onEndListener) { + mFlingAnim = new FlingAnimation(object, property) + .setFriction(FLING_FRICTION) + .setMinimumVisibleChange(FLING_END_THRESHOLD_PX) + .setStartVelocity(startVelocity) + .setMinValue(Math.min(startPosition, targetPosition)) + .setMaxValue(Math.max(startPosition, targetPosition)); + mFlingAnim.addEndListener(((animation, canceled, value, velocity) -> { + SpringAnimation springAnim = new SpringAnimation(object, property) + .setStartVelocity(velocity) + .setSpring(new SpringForce(targetPosition) + .setStiffness(SPRING_STIFFNESS) + .setDampingRatio(SPRING_DAMPING)); + springAnim.addEndListener(onEndListener); + springAnim.start(); + })); + } + + public void start() { + mFlingAnim.start(); + } +} diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java index e5c70da5f6..2a5418d081 100644 --- a/src/com/android/launcher3/views/FloatingIconView.java +++ b/src/com/android/launcher3/views/FloatingIconView.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.views; +import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -55,8 +57,6 @@ import com.android.launcher3.shortcuts.DeepShortcutView; 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. */ @@ -143,9 +143,6 @@ public class FloatingIconView extends View implements Animator.AnimatorListener, setBackgroundDrawableBounds(bgScale); mRevealAnimator.setCurrentFraction(shapeRevealProgress); - if (Float.compare(shapeRevealProgress, 1f) >= 0f) { - mRevealAnimator.end(); - } } invalidate(); invalidateOutline(); @@ -160,6 +157,9 @@ public class FloatingIconView extends View implements Animator.AnimatorListener, @Override public void onAnimationEnd(Animator animator) { + if (mRevealAnimator != null) { + mRevealAnimator.end(); + } if (mEndRunnable != null) { mEndRunnable.run(); }