Add Z scaling during unfold to launcher

The unfold progresses are mapped to 0.85 - 1 range and set as a scale for launcher.

In case of multiple scale animations for workspace and hotseat, they are combined using MultiScaleProperty (e.g. opening an app while unfolding/going to all apps while unfolding). Note that this is a pretty difficult scenario to be in. If that happens, we multiply all values and bound the result between the max and min values.

Bug: 217368525
Test: atest MultiScalePropertyTest and manually
Change-Id: I6131c39f36deade0b7280c72edda2d72045344e9
diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
index 6b6bd6a..333df10 100644
--- a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
+++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
@@ -15,9 +15,14 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_UNFOLD_ANIMATION;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.Utilities.comp;
 
 import android.annotation.Nullable;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 
@@ -39,6 +44,8 @@
     // Percentage of the width of the quick search bar that will be reduced
     // from the both sides of the bar when progress is 0
     private static final float MAX_WIDTH_INSET_FRACTION = 0.15f;
+    private static final FloatProperty<View> UNFOLD_SCALE_PROPERTY =
+            SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_UNFOLD_ANIMATION);
 
     private final Launcher mLauncher;
 
@@ -62,6 +69,8 @@
         // Animated in all orientations
         mProgressProvider.addCallback(new UnfoldMoveFromCenterWorkspaceAnimator(launcher,
                 windowManager));
+        mProgressProvider
+                .addCallback(new LauncherScaleAnimationListener());
 
         // Animated only in natural orientation
         mNaturalOrientationProgressProvider
@@ -120,4 +129,26 @@
             }
         }
     }
+
+    private class LauncherScaleAnimationListener implements TransitionProgressListener {
+
+        @Override
+        public void onTransitionStarted() {
+        }
+
+        @Override
+        public void onTransitionFinished() {
+            setScale(1);
+        }
+
+        @Override
+        public void onTransitionProgress(float progress) {
+            setScale(MathUtils.constrainedMap(0.85f, 1, 0, 1, progress));
+        }
+
+        private void setScale(float value) {
+            UNFOLD_SCALE_PROPERTY.setValue(mLauncher.getWorkspace(), value);
+            UNFOLD_SCALE_PROPERTY.setValue(mLauncher.getHotseat(), value);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
index 7ae6cb7..8659b68 100644
--- a/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
+++ b/quickstep/src/com/android/quickstep/util/WorkspaceRevealAnim.java
@@ -15,7 +15,8 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_REVEAL_ANIM;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
@@ -27,6 +28,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.util.FloatProperty;
 import android.view.View;
 
 import com.android.launcher3.BaseQuickstepLauncher;
@@ -49,6 +51,8 @@
 
     // Should be used for animations running alongside this WorkspaceRevealAnim.
     public static final int DURATION_MS = 350;
+    private static final FloatProperty<View> REVEAL_SCALE_PROPERTY =
+            SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_REVEAL_ANIM);
 
     private final float mScaleStart;
     private final AnimatorSet mAnimators = new AnimatorSet();
@@ -90,7 +94,7 @@
     }
 
     private void addRevealAnimatorsForView(View v) {
-        ObjectAnimator scale = ObjectAnimator.ofFloat(v, SCALE_PROPERTY, mScaleStart, 1f);
+        ObjectAnimator scale = ObjectAnimator.ofFloat(v, REVEAL_SCALE_PROPERTY, mScaleStart, 1f);
         scale.setDuration(DURATION_MS);
         scale.setInterpolator(Interpolators.DECELERATED_EASE);
         mAnimators.play(scale);
@@ -103,7 +107,7 @@
         mAnimators.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                SCALE_PROPERTY.set(v, 1f);
+                REVEAL_SCALE_PROPERTY.set(v, 1f);
                 v.setAlpha(1f);
             }
         });
diff --git a/quickstep/src/com/android/quickstep/util/WorkspaceUnlockAnim.java b/quickstep/src/com/android/quickstep/util/WorkspaceUnlockAnim.java
index b01447e..aa3f0d7 100644
--- a/quickstep/src/com/android/quickstep/util/WorkspaceUnlockAnim.java
+++ b/quickstep/src/com/android/quickstep/util/WorkspaceUnlockAnim.java
@@ -21,6 +21,7 @@
 /**
  * Animation to animate in a workspace during the unlock transition.
  */
+// TODO(b/219444608): use SCALE_PROPERTY_FACTORY once the scale is reset to 1.0 after unlocking.
 public class WorkspaceUnlockAnim {
     /** Scale for the workspace icons at the beginning of the animation. */
     private static final float START_SCALE = 0.9f;
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
index b56c012..4300392 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -27,6 +27,8 @@
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 
+import com.android.launcher3.util.MultiScalePropertyFactory;
+
 public class LauncherAnimUtils {
     /**
      * Durations for various state animations. These are not defined in resources to allow
@@ -64,6 +66,25 @@
                 }
             };
 
+    /**
+     * Property to set the scale of workspace and hotseat. The value is based on a combination
+     * of all the ones set, to have a smooth experience even in the case of overlapping scaling
+     * animation.
+     */
+    public static final MultiScalePropertyFactory<View> SCALE_PROPERTY_FACTORY =
+            new MultiScalePropertyFactory<View>("scale_property") {
+                @Override
+                protected void apply(View view, float scale) {
+                    view.setScaleX(scale);
+                    view.setScaleY(scale);
+                }
+            };
+
+    public static final int SCALE_INDEX_UNFOLD_ANIMATION = 1;
+    public static final int SCALE_INDEX_UNLOCK_ANIMATION = 2;
+    public static final int SCALE_INDEX_WORKSPACE_STATE = 3;
+    public static final int SCALE_INDEX_REVEAL_ANIM = 4;
+
     /** Increase the duration if we prevented the fling, as we are going against a high velocity. */
     public static int blockedFlingDurationFactor(float velocity) {
         return (int) Utilities.boundToRange(Math.abs(velocity) / 2, 2f, 6f);
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 1b9647a..98e785f 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -18,7 +18,8 @@
 
 import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
@@ -42,6 +43,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM;
 
 import android.animation.ValueAnimator;
+import android.util.FloatProperty;
 import android.view.View;
 import android.view.animation.Interpolator;
 
@@ -62,6 +64,9 @@
  */
 public class WorkspaceStateTransitionAnimation {
 
+    private static final FloatProperty<View> WORKSPACE_STATE_SCALE_PROPERTY =
+            SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_WORKSPACE_STATE);
+
     private final Launcher mLauncher;
     private final Workspace mWorkspace;
 
@@ -117,7 +122,8 @@
             ((PendingAnimation) propertySetter).add(getSpringScaleAnimator(mLauncher,
                     mWorkspace, mNewScale));
         } else {
-            propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
+            propertySetter.setFloat(mWorkspace, WORKSPACE_STATE_SCALE_PROPERTY, mNewScale,
+                    scaleInterpolator);
         }
 
         mWorkspace.setPivotToScaleWithSelf(hotseat);
@@ -128,7 +134,7 @@
         } else {
             Interpolator hotseatScaleInterpolator = config.getInterpolator(ANIM_HOTSEAT_SCALE,
                     scaleInterpolator);
-            propertySetter.setFloat(hotseat, SCALE_PROPERTY, hotseatScale,
+            propertySetter.setFloat(hotseat, WORKSPACE_STATE_SCALE_PROPERTY, hotseatScale,
                     hotseatScaleInterpolator);
         }
 
@@ -205,9 +211,9 @@
                 .setDampingRatio(damping)
                 .setMinimumVisibleChange(MIN_VISIBLE_CHANGE_SCALE)
                 .setEndValue(scale)
-                .setStartValue(SCALE_PROPERTY.get(v))
+                .setStartValue(WORKSPACE_STATE_SCALE_PROPERTY.get(v))
                 .setStartVelocity(velocityPxPerS)
-                .build(v, SCALE_PROPERTY);
+                .build(v, WORKSPACE_STATE_SCALE_PROPERTY);
 
     }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/util/MultiScalePropertyFactory.java b/src/com/android/launcher3/util/MultiScalePropertyFactory.java
new file mode 100644
index 0000000..f27d0f0
--- /dev/null
+++ b/src/com/android/launcher3/util/MultiScalePropertyFactory.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 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.util;
+
+import android.util.ArrayMap;
+import android.util.FloatProperty;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Allows to combine multiple values set by several sources.
+ *
+ * The various sources are meant to use [set], providing different `setterIndex` params. When it is
+ * not set, 0 is used. This is meant to cover the case multiple animations are going on at the same
+ * time.
+ *
+ * This class behaves similarly to [MultiValueAlpha], but is meant to be more abstract and reusable.
+ * It sets the multiplication of all values, bounded to the max and the min values.
+ *
+ * @param <T> Type where to apply the property.
+ */
+public abstract class MultiScalePropertyFactory<T> {
+
+    private final String mName;
+    private final ArrayMap<Integer, MultiScaleProperty> mProperties =
+            new ArrayMap<Integer, MultiScaleProperty>();
+
+    // This is an optimization for cases when set is called repeatedly with the same setterIndex.
+    private float mMinOfOthers = 0;
+    private float mMaxOfOthers = 0;
+    private float mMultiplicationOfOthers = 0;
+    private Integer mLastIndexSet = -1;
+    private float mLastAggregatedValue = 1.0f;
+
+    public MultiScalePropertyFactory(String name) {
+        mName = name;
+    }
+
+    /** Returns the [MultiFloatProperty] associated with [inx], creating it if not present. */
+    public MultiScaleProperty get(Integer index) {
+        return mProperties.computeIfAbsent(index,
+                (k) -> new MultiScaleProperty(index, mName + "_" + index));
+    }
+
+
+    /**
+     * Each [setValue] will be aggregated with the other properties values created by the
+     * corresponding factory.
+     */
+    class MultiScaleProperty extends FloatProperty<T> {
+        private final int mInx;
+        private float mValue = 1.0f;
+
+        MultiScaleProperty(int inx, String name) {
+            super(name);
+            mInx = inx;
+        }
+
+        @Override
+        public void setValue(T obj, float newValue) {
+            if (mLastIndexSet != mInx) {
+                mMinOfOthers = Float.MAX_VALUE;
+                mMaxOfOthers = Float.MIN_VALUE;
+                mMultiplicationOfOthers = 1.0f;
+                mProperties.forEach((key, property) -> {
+                    if (key != mInx) {
+                        mMinOfOthers = Math.min(mMinOfOthers, property.mValue);
+                        mMaxOfOthers = Math.max(mMaxOfOthers, property.mValue);
+                        mMultiplicationOfOthers *= property.mValue;
+                    }
+                });
+                mLastIndexSet = mInx;
+            }
+            float minValue = Math.min(mMinOfOthers, newValue);
+            float maxValue = Math.max(mMaxOfOthers, newValue);
+            float multValue = mMultiplicationOfOthers * newValue;
+            mLastAggregatedValue = Utilities.boundToRange(multValue, minValue, maxValue);
+            mValue = newValue;
+            apply(obj, mLastAggregatedValue);
+        }
+
+        @Override
+        public Float get(T t) {
+            return mLastAggregatedValue;
+        }
+
+        @Override
+        public String toString() {
+            return String.valueOf(mValue);
+        }
+    }
+
+    /** Applies value to object after setValue method is called. */
+    protected abstract void apply(T obj, float value);
+}
diff --git a/tests/src/com/android/launcher3/util/MultiScalePropertyTest.kt b/tests/src/com/android/launcher3/util/MultiScalePropertyTest.kt
new file mode 100644
index 0000000..c4a8db6
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/MultiScalePropertyTest.kt
@@ -0,0 +1,92 @@
+package com.android.launcher3.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for [MultiScalePropertyFactory] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MultiScalePropertyTest {
+
+    private val received = mutableListOf<Float>()
+
+    private val factory =
+        object : MultiScalePropertyFactory<Int?>("Test") {
+            override fun apply(obj: Int?, value: Float) {
+                received.add(value)
+            }
+        }
+
+    private val p1 = factory.get(1)
+    private val p2 = factory.get(2)
+    private val p3 = factory.get(3)
+
+    @Test
+    fun set_multipleSame_bothAppliedd() {
+        p1.set(null, 0.5f)
+        p1.set(null, 0.5f)
+
+        assertThat(received).containsExactly(0.5f, 0.5f)
+    }
+
+    @Test
+    fun set_differentIndexes_oneValuesNotCounted() {
+        val v1 = 0.5f
+        val v2 = 1.0f
+        p1.set(null, v1)
+        p2.set(null, v2)
+
+        assertThat(received).containsExactly(v1, v1)
+    }
+
+    @Test
+    fun set_onlyOneSetToOne_oneApplied() {
+        p1.set(null, 1.0f)
+
+        assertThat(received).containsExactly(1.0f)
+    }
+
+    @Test
+    fun set_onlyOneLessThanOne_applied() {
+        p1.set(null, 0.5f)
+
+        assertThat(received).containsExactly(0.5f)
+    }
+
+    @Test
+    fun set_differentIndexes_boundToMin() {
+        val v1 = 0.5f
+        val v2 = 0.6f
+        p1.set(null, v1)
+        p2.set(null, v2)
+
+        assertThat(received).containsExactly(v1, v1)
+    }
+
+    @Test
+    fun set_allHigherThanOne_boundToMax() {
+        val v1 = 3.0f
+        val v2 = 2.0f
+        val v3 = 1.0f
+        p1.set(null, v1)
+        p2.set(null, v2)
+        p3.set(null, v3)
+
+        assertThat(received).containsExactly(v1, v1, v1)
+    }
+
+    @Test
+    fun set_differentIndexes_firstModified_aggregationApplied() {
+        val v1 = 0.5f
+        val v2 = 0.6f
+        val v3 = 4f
+        p1.set(null, v1)
+        p2.set(null, v2)
+        p3.set(null, v3)
+
+        assertThat(received).containsExactly(v1, v1, v1 * v2 * v3)
+    }
+}