QuotaAwareAnimator.java
/*
* Copyright 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 androidx.wear.protolayout.expression.pipeline;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Wrapper for Animator that is aware of quota. Animator's animations will be played only if given
* quota manager allows. If not, non infinite animation will jump to an end. Any existing listeners
* on wrapped {@link Animator} will be replaced.
*/
class QuotaAwareAnimator {
@Nullable private ValueAnimator mAnimator;
@NonNull private final QuotaReleasingAnimatorListener mListener;
QuotaAwareAnimator(@Nullable ValueAnimator animator, @NonNull QuotaManager quotaManager) {
this.mAnimator = animator;
this.mListener = new QuotaReleasingAnimatorListener(quotaManager);
if (this.mAnimator != null) {
this.mAnimator.addListener(mListener);
}
}
/**
* Sets the new animator with {link @QuotaReleasingListener} added. Previous animator will be
* canceled.
*/
void updateAnimator(@NonNull ValueAnimator animator) {
cancelAnimator();
this.mAnimator = animator;
this.mAnimator.addListener(mListener);
this.mAnimator.addPauseListener(mListener);
}
/** Resets the animator to null. Previous animator will be canceled. */
void resetAnimator() {
cancelAnimator();
mAnimator = null;
}
/**
* Tries to start animation. This method will call start on animation, but when animation is due
* to start (i.e. after the given delay), listener will check the quota and allow/disallow
* animation to be played.
*/
@UiThread
void tryStartAnimation() {
if (mAnimator == null) {
return;
}
mAnimator.start();
}
/**
* Tries to start/resume infinite animation. This method will call start/resume on animation,
* but when animation is due to start (i.e. after the given delay), listener will check the
* quota and allow/disallow animation to be played.
*/
@UiThread
void tryStartOrResumeInfiniteAnimation() {
if (mAnimator == null) {
return;
}
ValueAnimator localAnimator = mAnimator;
if (localAnimator.isPaused()) {
localAnimator.resume();
} else if (isInfiniteAnimator()) {
// Infinite animators created when this node was invisible have not started yet.
localAnimator.start();
}
// No need to jump to an end of animation if it can't be played as they are infinite.
}
/**
* Stops or pauses the animator, depending on it's state. If stopped, it will assign the end
* value.
*/
@UiThread
void stopOrPauseAnimator() {
if (mAnimator == null) {
return;
}
ValueAnimator localAnimator = mAnimator;
if (isInfiniteAnimator()) {
localAnimator.pause();
} else {
// This causes the animation to assign the end value of the property being animated.
stopAnimator();
}
}
/** Stops the animator, which will cause it to assign the end value. */
@UiThread
void stopAnimator() {
if (mAnimator == null) {
return;
}
mAnimator.end();
}
/** Cancels the animator, which will stop in its tracks. */
@UiThread
void cancelAnimator() {
if (mAnimator == null) {
return;
}
// This calls both onCancel and onEnd methods from listener.
mAnimator.cancel();
mAnimator.removeListener(mListener);
mAnimator.removePauseListener(mListener);
}
/** Returns whether the animator in this class has an infinite duration. */
protected boolean isInfiniteAnimator() {
return mAnimator != null && mAnimator.getTotalDuration() == Animator.DURATION_INFINITE;
}
/**
* Returns whether this node has a running or started animation. Started means that animation is
* scheduled to run, but it has set time delay.
*/
boolean hasRunningOrStartedAnimation() {
return mAnimator != null
&& (mAnimator.isRunning() || /* delayed animation */ mAnimator.isStarted());
}
/**
* The listener used for animatable nodes to release quota when the animation is finished or
* paused. Additionally, when {@link
* android.animation.Animator.AnimatorListener#onAnimationStart(Animator)} is called, this
* listener will check quota, and if there isn't any available, it will jump to an end of
* animation.
*/
private static final class QuotaReleasingAnimatorListener extends AnimatorListenerAdapter {
@NonNull private final QuotaManager mQuotaManager;
// We need to keep track of whether the animation has started because pipeline has initiated
// and it has received quota, or onAnimationStart listener has been called because of the
// inner ValueAnimator implementation (i.e., when calling end() on animator to assign it end
// value, ValueAnimator will call start first if animation is not running to get it to the
// end state.
@NonNull final AtomicBoolean mIsUsingQuota = new AtomicBoolean(false);
QuotaReleasingAnimatorListener(@NonNull QuotaManager quotaManager) {
this.mQuotaManager = quotaManager;
}
@Override
public void onAnimationStart(Animator animation) {
acquireQuota(animation);
}
@Override
public void onAnimationResume(Animator animation) {
acquireQuota(animation);
}
@Override
@UiThread
public void onAnimationEnd(Animator animation) {
releaseQuota();
}
@Override
@UiThread
public void onAnimationPause(Animator animation) {
releaseQuota();
}
/**
* This method will block the given Animator from running animation if there is no enough
* quota. In that case, animation will jump to an end.
*/
private void acquireQuota(Animator animation) {
if (!mQuotaManager.tryAcquireQuota(1)) {
mIsUsingQuota.set(false);
animation.end();
// End will fire end value via UpdateListener. We don't want any new updates to be
// pushed to the callback.
if (animation instanceof ValueAnimator) {
((ValueAnimator) animation).removeAllUpdateListeners();
}
} else {
mIsUsingQuota.set(true);
}
}
private void releaseQuota() {
if (mIsUsingQuota.compareAndSet(true, false)) {
mQuotaManager.releaseQuota(1);
}
}
}
}