Don't end atomic animation when passing through state
Previously we were ending the atomic animation with the assumption
that it should be complete/almost complete by the time you drag to
the next state. However, it is very easy to drag quickly enough where
that assumption doesn't hold, and thus you just see the atomic
animation pop to the end (i.e. recents showing without animation).
Now instead of ending the atomic animation, we let it continue. But
because the new state animation will have an atomic component that
interferes with the still playing atomic animation, we have to
control the atomic component separately; we control the non-atomic
components until the atomic animation ends, at which point we create
a separate controller to control the atomic components.
Bug: 76449024
Change-Id: Ia4bf19e26d0838f952d9e500fbdd8aba19856a41
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 977572e..6e48c36 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -28,6 +28,7 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
+import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import com.android.launcher3.Launcher;
@@ -36,6 +37,7 @@
import com.android.launcher3.LauncherStateManager.AnimationConfig;
import com.android.launcher3.LauncherStateManager.StateHandler;
import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.AnimatorSetBuilder;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
@@ -77,11 +79,18 @@
// Ratio of transition process [0, 1] to drag displacement (px)
private float mProgressMultiplier;
private float mDisplacementShift;
+ private boolean mCanBlockFling;
+ private boolean mBlockFling;
private AnimatorSet mAtomicAnim;
private boolean mPassedOverviewAtomicThreshold;
- private boolean mCanBlockFling;
- private boolean mBlockFling;
+ // mAtomicAnim plays the atomic components of the state animations when we pass the threshold.
+ // However, if we reinit to transition to a new state (e.g. OVERVIEW -> ALL_APPS) before the
+ // atomic animation finishes, we only control the non-atomic components so that we don't
+ // interfere with the atomic animation. When the atomic animation ends, we start controlling
+ // the atomic components as well, using this controller.
+ private AnimatorPlaybackController mAtomicComponentsController;
+ private float mAtomicComponentsStartProgress;
public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) {
mLauncher = l;
@@ -185,16 +194,30 @@
mStartProgress = 0;
mPassedOverviewAtomicThreshold = false;
- if (mAtomicAnim != null) {
- // Most likely the animation is finished by now, but just in case it's not,
- // make sure the next state animation starts from the expected state.
- mAtomicAnim.end();
- }
if (mCurrentAnimation != null) {
mCurrentAnimation.setOnCancelRunnable(null);
}
int animComponents = goingBetweenNormalAndOverview(mFromState, mToState)
? NON_ATOMIC_COMPONENT : ANIM_ALL;
+ if (mAtomicAnim != null) {
+ // Control the non-atomic components until the atomic animation finishes, then control
+ // the atomic components as well.
+ animComponents = NON_ATOMIC_COMPONENT;
+ mAtomicAnim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animation) {
+ cancelAtomicComponentsController();
+ mAtomicComponentsStartProgress = mCurrentAnimation.getProgressFraction();
+ long duration = (long) (getShiftRange() * 2);
+ mAtomicComponentsController = AnimatorPlaybackController.wrap(
+ createAtomicAnimForState(mToState, duration), duration);
+ mAtomicComponentsController.dispatchOnStart();
+ }
+ });
+ }
+ if (goingBetweenNormalAndOverview(mFromState, mToState)) {
+ cancelAtomicComponentsController();
+ }
mProgressMultiplier = initCurrentAnimation(animComponents);
mCurrentAnimation.dispatchOnStart();
return true;
@@ -210,6 +233,7 @@
public void onDragStart(boolean start) {
if (mCurrentAnimation == null) {
mFromState = mToState = null;
+ mAtomicComponentsController = null;
reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive());
mDisplacementShift = 0;
} else {
@@ -246,6 +270,9 @@
protected void updateProgress(float fraction) {
mCurrentAnimation.setPlayFraction(fraction);
+ if (mAtomicComponentsController != null) {
+ mAtomicComponentsController.setPlayFraction(fraction - mAtomicComponentsStartProgress);
+ }
maybeUpdateAtomicAnim(mFromState, mToState, fraction);
}
@@ -267,15 +294,9 @@
if (mAtomicAnim != null) {
mAtomicAnim.cancel();
}
- AnimatorSetBuilder builder = new AnimatorSetBuilder();
- AnimationConfig config = new AnimationConfig();
- config.animComponents = ATOMIC_COMPONENT;
- config.duration = targetState == OVERVIEW ? ATOMIC_NORMAL_TO_OVERVIEW_DURATION
+ long duration = targetState == OVERVIEW ? ATOMIC_NORMAL_TO_OVERVIEW_DURATION
: ATOMIC_OVERVIEW_TO_NORMAL_DURATION;
- for (StateHandler handler : mLauncher.getStateManager().getStateHandlers()) {
- handler.setStateWithAnimation(targetState, builder, config);
- }
- mAtomicAnim = builder.build();
+ mAtomicAnim = createAtomicAnimForState(targetState, duration);
mAtomicAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -283,9 +304,21 @@
}
});
mAtomicAnim.start();
+ mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
}
+ private AnimatorSet createAtomicAnimForState(LauncherState targetState, long duration) {
+ AnimatorSetBuilder builder = new AnimatorSetBuilder();
+ AnimationConfig config = new AnimationConfig();
+ config.animComponents = ATOMIC_COMPONENT;
+ config.duration = duration;
+ for (StateHandler handler : mLauncher.getStateManager().getStateHandlers()) {
+ handler.setStateWithAnimation(targetState, builder, config);
+ }
+ return builder.build();
+ }
+
@Override
public void onDragEnd(float velocity, boolean fling) {
final int logAction;
@@ -353,6 +386,38 @@
targetState, velocity, fling);
mCurrentAnimation.dispatchOnStart();
anim.start();
+ if (mAtomicAnim == null) {
+ startAtomicComponentsAnim(endProgress, anim.getDuration());
+ } else {
+ mAtomicAnim.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ startAtomicComponentsAnim(endProgress, anim.getDuration());
+ }
+ });
+ }
+ }
+
+ /**
+ * Animates the atomic components from the current progress to the final progress.
+ *
+ * Note that this only applies when we are controlling the atomic components separately from
+ * the non-atomic components, which only happens if we reinit before the atomic animation
+ * finishes.
+ */
+ private void startAtomicComponentsAnim(float toProgress, long duration) {
+ if (mAtomicComponentsController != null) {
+ ValueAnimator atomicAnim = mAtomicComponentsController.getAnimationPlayer();
+ atomicAnim.setFloatValues(mAtomicComponentsController.getProgressFraction(), toProgress);
+ atomicAnim.setDuration(duration);
+ atomicAnim.start();
+ atomicAnim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAtomicComponentsController = null;
+ }
+ });
+ }
}
private long getRemainingAtomicDuration() {
@@ -409,7 +474,15 @@
protected void clearState() {
mCurrentAnimation = null;
+ cancelAtomicComponentsController();
mDetector.finishedScrolling();
mDetector.setDetectableScrollConditions(0, false);
}
+
+ private void cancelAtomicComponentsController() {
+ if (mAtomicComponentsController != null) {
+ mAtomicComponentsController.getAnimationPlayer().cancel();
+ mAtomicComponentsController = null;
+ }
+ }
}