From 12131a40fef26c56e2443d532cfefdd85d35e142 Mon Sep 17 00:00:00 2001 From: Nicolo' Mazzucato Date: Mon, 7 Feb 2022 20:36:35 +0100 Subject: [PATCH] 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 --- .../LauncherUnfoldAnimationController.java | 31 +++++ .../quickstep/util/WorkspaceRevealAnim.java | 10 +- .../quickstep/util/WorkspaceUnlockAnim.java | 1 + .../android/launcher3/LauncherAnimUtils.java | 21 ++++ .../WorkspaceStateTransitionAnimation.java | 16 ++- .../util/MultiScalePropertyFactory.java | 109 ++++++++++++++++++ .../launcher3/util/MultiScalePropertyTest.kt | 92 +++++++++++++++ 7 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 src/com/android/launcher3/util/MultiScalePropertyFactory.java create mode 100644 tests/src/com/android/launcher3/util/MultiScalePropertyTest.kt diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java index 6b6bd6a8b3..333df10dde 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 @@ public class LauncherUnfoldAnimationController { // 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 UNFOLD_SCALE_PROPERTY = + SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_UNFOLD_ANIMATION); private final Launcher mLauncher; @@ -62,6 +69,8 @@ public class LauncherUnfoldAnimationController { // Animated in all orientations mProgressProvider.addCallback(new UnfoldMoveFromCenterWorkspaceAnimator(launcher, windowManager)); + mProgressProvider + .addCallback(new LauncherScaleAnimationListener()); // Animated only in natural orientation mNaturalOrientationProgressProvider @@ -120,4 +129,26 @@ public class LauncherUnfoldAnimationController { } } } + + 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 7ae6cb7661..8659b687c3 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.Animator; 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 @@ public class WorkspaceRevealAnim { // Should be used for animations running alongside this WorkspaceRevealAnim. public static final int DURATION_MS = 350; + private static final FloatProperty 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 @@ public class WorkspaceRevealAnim { } 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 @@ public class WorkspaceRevealAnim { 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 b01447ed27..aa3f0d7860 100644 --- a/quickstep/src/com/android/quickstep/util/WorkspaceUnlockAnim.java +++ b/quickstep/src/com/android/quickstep/util/WorkspaceUnlockAnim.java @@ -21,6 +21,7 @@ import com.android.launcher3.Utilities; /** * 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 b56c0127ff..430039276e 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -27,6 +27,8 @@ import android.util.IntProperty; 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 @@ public class LauncherAnimUtils { } }; + /** + * 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 SCALE_PROPERTY_FACTORY = + new MultiScalePropertyFactory("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 1b9647afc0..98e785f47b 100644 --- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java +++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java @@ -18,7 +18,8 @@ package com.android.launcher3; 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.ANIM_WORKSPACE_T 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 @@ import com.android.systemui.plugins.ResourceProvider; */ public class WorkspaceStateTransitionAnimation { + private static final FloatProperty WORKSPACE_STATE_SCALE_PROPERTY = + SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_WORKSPACE_STATE); + private final Launcher mLauncher; private final Workspace mWorkspace; @@ -117,7 +122,8 @@ public class WorkspaceStateTransitionAnimation { ((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 @@ public class WorkspaceStateTransitionAnimation { } 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 @@ public class WorkspaceStateTransitionAnimation { .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 0000000000..f27d0f0d86 --- /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 Type where to apply the property. + */ +public abstract class MultiScalePropertyFactory { + + private final String mName; + private final ArrayMap mProperties = + new ArrayMap(); + + // 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 { + 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 0000000000..c4a8db6adf --- /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() + + private val factory = + object : MultiScalePropertyFactory("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) + } +}