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
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 d61ed72..ef46b3b 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 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 @@
@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 @@
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 e95e2a0..b0bd71b 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 @@
@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 4e010d2..4792cc7 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_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.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 java.util.function.Consumer;
+import androidx.annotation.UiThread;
+
/**
* Input consumer for handling events originating from an activity other than Launcher
*/
@@ -331,12 +330,13 @@
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 fd53f9c..efc228b 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.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.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.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 @@
}
}
+ /**
+ * @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 @@
}
@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 @@
} 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 @@
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 @@
// 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 @@
/** 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 @@
}
HomeAnimationFactory homeAnimFactory;
- Animator windowAnim;
if (mGestureEndTarget == HOME) {
if (mActivity != null) {
homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity);
@@ -872,27 +875,34 @@
@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 @@
// 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 @@
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 @@
((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 0000000..2edeb3a
--- /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<RectFSpringAnim> RECT_CENTER_X =
+ new FloatPropertyCompat<RectFSpringAnim>("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<RectFSpringAnim> RECT_CENTER_Y =
+ new FloatPropertyCompat<RectFSpringAnim>("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<RectFSpringAnim> RECT_SCALE_PROGRESS =
+ new FloatProperty<RectFSpringAnim>("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<OnUpdateListener> mOnUpdateListeners = new ArrayList<>();
+ private final List<Animator.AnimatorListener> 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 75be2e4..418f7f4 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 @@
@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 0000000..3d21d82
--- /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 <K> FlingSpringAnim(K object, FloatPropertyCompat<K> 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 e5c70da..2a5418d 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 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 @@
setBackgroundDrawableBounds(bgScale);
mRevealAnimator.setCurrentFraction(shapeRevealProgress);
- if (Float.compare(shapeRevealProgress, 1f) >= 0f) {
- mRevealAnimator.end();
- }
}
invalidate();
invalidateOutline();
@@ -160,6 +157,9 @@
@Override
public void onAnimationEnd(Animator animator) {
+ if (mRevealAnimator != null) {
+ mRevealAnimator.end();
+ }
if (mEndRunnable != null) {
mEndRunnable.run();
}