Update motion for one-handed bouncer.

This is a little awkward to implement properly because of the animation
parameters; see the comment in KeyguardSecurityContainer for more
information.

Video link:
https://drive.google.com/file/d/1EJIZo-yn_Pez3nKgipodQ_QwqyDjXPT8/view?usp=sharing&resourcekey=0-TQWNbg-y6f-OcN_FkrC52w

Test: Manually tested.
Bug: 195012405
Change-Id: I550847d7e47608649422709d96b31b90f0e6467d
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index a2ae5023..e854b02 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -104,4 +104,11 @@
          allow it to use the whole screen space, 0.6 will allow it to use just under half of the
          screen. -->
     <item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item>
+
+    <!-- The actual amount of translation that is applied to the bouncer when it animates from one
+         side of the screen to the other in one-handed mode. Note that it will always translate from
+         the side of the screen to the other (it will "jump" closer to the destination while the
+         opacity is zero), but this controls how much motion will actually be applied to it while
+         animating. Larger values will cause it to move "faster" while fading out/in. -->
+    <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen>
 </resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 64d214d..13cb036 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -21,8 +21,7 @@
 
 import static java.lang.Integer.max;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
@@ -40,6 +39,8 @@
 import android.view.WindowInsets;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 
 import androidx.annotation.VisibleForTesting;
@@ -85,6 +86,13 @@
 
     private static final long IME_DISAPPEAR_DURATION_MS = 125;
 
+    // The duration of the animation to switch bouncer sides.
+    private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500;
+
+    // How much of the switch sides animation should be dedicated to fading the bouncer out. The
+    // remainder will fade it back in again.
+    private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f;
+
     @VisibleForTesting
     KeyguardSecurityViewFlipper mSecurityViewFlipper;
     private AlertDialog mAlertDialog;
@@ -322,18 +330,87 @@
                 ? 0 : (int) (getMeasuredWidth() - mSecurityViewFlipper.getWidth());
 
         if (animate) {
-            mRunningOneHandedAnimator =
-                    mSecurityViewFlipper.animate().translationX(targetTranslation);
-            mRunningOneHandedAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-            mRunningOneHandedAnimator.setListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mRunningOneHandedAnimator = null;
+            // This animation is a bit fun to implement. The bouncer needs to move, and fade in/out
+            // at the same time. The issue is, the bouncer should only move a short amount (120dp or
+            // so), but obviously needs to go from one side of the screen to the other. This needs a
+            // pretty custom animation.
+            //
+            // This works as follows. It uses a ValueAnimation to simply drive the animation
+            // progress. This animator is responsible for both the translation of the bouncer, and
+            // the current fade. It will fade the bouncer out while also moving it along the 120dp
+            // path. Once the bouncer is fully faded out though, it will "snap" the bouncer closer
+            // to its destination, then fade it back in again. The effect is that the bouncer will
+            // move from 0 -> X while fading out, then (destination - X) -> destination while fading
+            // back in again.
+            // TODO(b/195012405): Make this animation properly abortable.
+            Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
+                    mContext, android.R.interpolator.fast_out_extra_slow_in);
+            Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
+            Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+
+            ValueAnimator anim = ValueAnimator.ofFloat(0.0f, 1.0f);
+            anim.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS);
+            anim.setInterpolator(Interpolators.LINEAR);
+
+            int initialTranslation = (int) mSecurityViewFlipper.getTranslationX();
+            int totalTranslation = (int) getResources().getDimension(
+                    R.dimen.one_handed_bouncer_move_animation_translation);
+
+            final boolean shouldRestoreLayerType = mSecurityViewFlipper.hasOverlappingRendering()
+                    && mSecurityViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
+            if (shouldRestoreLayerType) {
+                mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
+            }
+
+            anim.addUpdateListener(animation -> {
+                float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION;
+                boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
+
+                int currentTranslation = (int) (positionInterpolator.getInterpolation(
+                        animation.getAnimatedFraction()) * totalTranslation);
+                int translationRemaining = totalTranslation - currentTranslation;
+
+                // Flip the sign if we're going from right to left.
+                if (mIsSecurityViewLeftAligned) {
+                    currentTranslation = -currentTranslation;
+                    translationRemaining = -translationRemaining;
+                }
+
+                if (isFadingOut) {
+                    // The bouncer fades out over the first X%.
+                    float fadeOutFraction = MathUtils.constrainedMap(
+                            /* rangeMin= */0.0f,
+                            /* rangeMax= */1.0f,
+                            /* valueMin= */0.0f,
+                            /* valueMax= */switchPoint,
+                            animation.getAnimatedFraction());
+                    float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
+                    mSecurityViewFlipper.setAlpha(1f - opacity);
+
+                    // Animate away from the source.
+                    mSecurityViewFlipper.setTranslationX(initialTranslation + currentTranslation);
+                } else {
+                    // And in again over the remaining (100-X)%.
+                    float fadeInFraction = MathUtils.constrainedMap(
+                            /* rangeMin= */0.0f,
+                            /* rangeMax= */1.0f,
+                            /* valueMin= */switchPoint,
+                            /* valueMax= */1.0f,
+                            animation.getAnimatedFraction());
+
+                    float opacity = fadeInInterpolator.getInterpolation(fadeInFraction);
+                    mSecurityViewFlipper.setAlpha(opacity);
+
+                    // Fading back in, animate towards the destination.
+                    mSecurityViewFlipper.setTranslationX(targetTranslation - translationRemaining);
+                }
+
+                if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
+                    mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
                 }
             });
 
-            mRunningOneHandedAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-            mRunningOneHandedAnimator.start();
+            anim.start();
         } else {
             mSecurityViewFlipper.setTranslationX(targetTranslation);
         }
@@ -682,4 +759,3 @@
         mDisappearAnimRunning = false;
     }
 }
-