Merge "Adding CannedAnimationCoordinator to enable running a predefined animation in Launcher" into udc-qpr-dev

This commit is contained in:
Treehugger Robot
2023-08-23 20:28:04 +00:00
committed by Android (Google) Code Review
3 changed files with 177 additions and 3 deletions

View File

@@ -198,6 +198,7 @@ import com.android.launcher3.touch.ItemLongClickListener;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.ActivityResultInfo;
import com.android.launcher3.util.ActivityTracker;
import com.android.launcher3.util.CannedAnimationCoordinator;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
@@ -421,6 +422,9 @@ public class Launcher extends StatefulActivity<LauncherState>
private StartupLatencyLogger mStartupLatencyLogger;
private CellPosMapper mCellPosMapper = CellPosMapper.DEFAULT;
private final CannedAnimationCoordinator mAnimationCoordinator =
new CannedAnimationCoordinator(this);
@Override
@TargetApi(Build.VERSION_CODES.S)
protected void onCreate(Bundle savedInstanceState) {
@@ -3422,4 +3426,11 @@ public class Launcher extends StatefulActivity<LauncherState>
public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
// Overridden
}
/**
* Returns the animation coordinator for playing one-off animations
*/
public CannedAnimationCoordinator getAnimationCoordinator() {
return mAnimationCoordinator;
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2023 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.animation.AnimatorSet
import android.animation.ValueAnimator
import android.util.Log
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import androidx.core.view.OneShotPreDrawListener
import com.android.app.animation.Interpolators.LINEAR
import com.android.launcher3.anim.AnimatorListeners
import com.android.launcher3.anim.AnimatorPlaybackController
import com.android.launcher3.anim.PendingAnimation
import com.android.launcher3.statemanager.StatefulActivity
import java.util.function.Consumer
private const val TAG = "CannedAnimCoordinator"
/**
* Utility class to run a canned animation on Launcher.
*
* This class takes care to registering animations with stateManager and ensures that only one
* animation is playing at a time.
*/
class CannedAnimationCoordinator(private val activity: StatefulActivity<*>) {
private val launcherLayoutListener = OnGlobalLayoutListener { scheduleRecreateAnimOnPreDraw() }
private var recreatePending = false
private var animationProvider: Any? = null
private var animationDuration: Long = 0L
private var animationFactory: Consumer<PendingAnimation>? = null
private var animationController: AnimatorPlaybackController? = null
private var currentAnim: AnimatorPlaybackController? = null
/**
* Sets the current animation cancelling any previously set animation.
*
* Callers can control the animation using {@link #getPlaybackController}. The state is
* automatically cleared when the playback controller ends. The animation is automatically
* recreated when any layout change happens. Callers can also ask for recreation by calling
* {@link #recreateAnimation}
*/
fun setAnimation(provider: Any, factory: Consumer<PendingAnimation>, duration: Long) {
if (provider != animationProvider) {
Log.e(TAG, "Trying to play two animations together, $provider and $animationProvider")
}
// Cancel any previously running animation
endCurrentAnimation(false)
animationController?.dispatchOnCancel()?.dispatchOnEnd()
animationProvider = provider
animationFactory = factory
animationDuration = duration
// Setup a new controller and link it with launcher state animation
val anim = AnimatorSet()
anim.play(
ValueAnimator.ofFloat(0f, 1f).apply {
interpolator = LINEAR
this.duration = duration
addUpdateListener { anim -> currentAnim?.setPlayFraction(anim.animatedFraction) }
}
)
val controller = AnimatorPlaybackController.wrap(anim, duration)
anim.addListener(
AnimatorListeners.forEndCallback { success ->
if (animationController != controller) {
return@forEndCallback
}
endCurrentAnimation(success)
animationController = null
animationFactory = null
animationProvider = null
activity.rootView.viewTreeObserver.apply {
if (isAlive) {
removeOnGlobalLayoutListener(launcherLayoutListener)
}
}
}
)
// Recreate animation whenever layout happens in case transforms change during layout
activity.rootView.viewTreeObserver.apply {
if (isAlive) {
addOnGlobalLayoutListener(launcherLayoutListener)
}
}
// Link this to the state manager so that it auto-cancels when state changes
recreatePending = false
animationController =
controller.apply { activity.stateManager.setCurrentUserControlledAnimation(this) }
recreateAnimation(provider)
}
private fun endCurrentAnimation(success: Boolean) {
currentAnim?.apply {
// When cancelling an animation, apply final progress so that all transformations
// are restored
setPlayFraction(1f)
if (!success) dispatchOnCancel()
dispatchOnEnd()
}
currentAnim = null
}
/** Returns the current animation controller to control the animation */
fun getPlaybackController(provider: Any): AnimatorPlaybackController? {
return if (provider == animationProvider) animationController
else {
Log.d(TAG, "Wrong controller access from $provider, actual provider $animationProvider")
null
}
}
private fun scheduleRecreateAnimOnPreDraw() {
if (!recreatePending) {
recreatePending = true
OneShotPreDrawListener.add(activity.rootView) {
if (recreatePending) {
recreatePending = false
animationProvider?.apply { recreateAnimation(this) }
}
}
}
}
/** Notify the controller to recreate the animation. The animation progress is preserved */
fun recreateAnimation(provider: Any) {
if (provider != animationProvider) {
Log.e(TAG, "Ignore recreate request from $provider, actual provider $animationProvider")
return
}
endCurrentAnimation(false /* success */)
if (animationFactory == null || animationController == null) {
return
}
currentAnim =
PendingAnimation(animationDuration)
.apply { animationFactory?.accept(this) }
.createPlaybackController()
.apply { setPlayFraction(animationController!!.progressFraction) }
}
}

View File

@@ -40,8 +40,7 @@ public class MultiScalePropertyFactory<T extends View> {
private static final boolean DEBUG = false;
private static final String TAG = "MultiScaleProperty";
private final String mName;
private final ArrayMap<Integer, MultiScaleProperty> mProperties =
new ArrayMap<Integer, MultiScaleProperty>();
private final ArrayMap<Integer, MultiScaleProperty> mProperties = new ArrayMap<>();
// This is an optimization for cases when set is called repeatedly with the same setterIndex.
private float mMinOfOthers = 0;
@@ -55,7 +54,7 @@ public class MultiScalePropertyFactory<T extends View> {
}
/** Returns the [MultiFloatProperty] associated with [inx], creating it if not present. */
public MultiScaleProperty get(Integer index) {
public FloatProperty<T> get(Integer index) {
return mProperties.computeIfAbsent(index,
(k) -> new MultiScaleProperty(index, mName + "_" + index));
}