blob: 6e1349b319d887674984beca455a4c3802b82c63 [file] [log] [blame]
/*
* 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 static androidx.wear.protolayout.expression.pipeline.AnimationsHelper.applyAnimationSpecToAnimator;
import static androidx.wear.protolayout.expression.pipeline.AnimationsHelper.getRepeatDelays;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.os.HandlerCompat;
import androidx.wear.protolayout.expression.pipeline.AnimationsHelper.RepeatDelays;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
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 {
@NonNull protected final ValueAnimator mAnimator;
@NonNull protected final QuotaManager mQuotaManager;
@NonNull protected final QuotaReleasingAnimatorListener mListener;
@NonNull protected final Handler mUiHandler;
private long mStartDelay = 0;
protected Runnable mAcquireQuotaAndAnimateRunnable = this::acquireQuotaAndAnimate;
@Nullable protected final TypeEvaluator<?> mEvaluator;
interface UpdateCallback {
void onUpdate(@NonNull Object animatedValue);
}
QuotaAwareAnimator(@NonNull QuotaManager quotaManager, @NonNull AnimationSpec spec) {
this(quotaManager, spec, null);
}
/**
* If an evaluator other than a float or int type shall be used when calculating the animated
* values of this animation, use this constructor to set the preferred type evaluator.
*/
QuotaAwareAnimator(
@NonNull QuotaManager quotaManager,
@NonNull AnimationSpec spec,
@Nullable TypeEvaluator<?> evaluator) {
this(quotaManager, spec, evaluator, false);
}
protected QuotaAwareAnimator(
@NonNull QuotaManager quotaManager,
@NonNull AnimationSpec spec,
@Nullable TypeEvaluator<?> evaluator,
boolean alwaysPauseWhenRepeatForward) {
mQuotaManager = quotaManager;
mAnimator = new ValueAnimator();
mUiHandler = new Handler(Looper.getMainLooper());
applyAnimationSpecToAnimator(mAnimator, spec);
// The start delay would be handled outside ValueAnimator, to make sure that the quota was
// not consumed during the delay.
mStartDelay = mAnimator.getStartDelay();
mAnimator.setStartDelay(0);
RepeatDelays repeatDelays = getRepeatDelays(spec);
mListener =
new QuotaReleasingAnimatorListener(
quotaManager,
mAnimator.getRepeatMode(),
repeatDelays.mForwardRepeatDelay,
repeatDelays.mReverseRepeatDelay,
mAnimator::resume,
mUiHandler,
alwaysPauseWhenRepeatForward);
mAnimator.addListener(mListener);
mEvaluator = evaluator;
}
/**
* Adds a listener that is sent update events through the life of the animation. This method is
* called on every frame of the animation after the values of the animation have been
* calculated.
*/
void addUpdateCallback(@NonNull UpdateCallback updateCallback) {
mAnimator.addUpdateListener(
animation -> updateCallback.onUpdate(animation.getAnimatedValue()));
}
/**
* Sets float values that will be animated between.
*
* @param values A set of values that the animation will animate between over time.
*/
void setFloatValues(float... values) {
setFloatValues(mAnimator, mEvaluator, values);
}
protected static void setFloatValues(
ValueAnimator animator, @Nullable TypeEvaluator<?> evaluator, float... values) {
animator.cancel();
// ValueAnimator#setEvaluator only valid after values are set, and only need to set once.
boolean needToSetEvaluator = animator.getValues() == null && evaluator != null;
animator.setFloatValues(values);
if (needToSetEvaluator) {
animator.setEvaluator(evaluator);
}
}
/**
* Sets integer values that will be animated between.
*
* @param values A set of values that the animation will animate between over time.
*/
void setIntValues(int... values) {
setIntValues(mAnimator, mEvaluator, values);
}
protected static void setIntValues(
ValueAnimator animator, @Nullable TypeEvaluator<?> evaluator, int... values) {
animator.cancel();
// ValueAnimator#setEvaluator only valid after values are set, and only need to set once.
boolean needToSetEvaluator = animator.getValues() == null && evaluator != null;
animator.setIntValues(values);
if (needToSetEvaluator) {
animator.setEvaluator(evaluator);
}
}
/**
* Tries to start animation. This method first handles the start delay if any, then checks the
* quota to start tha animation or skip and jump to the end directly.
*/
@UiThread
void tryStartAnimation() {
if (isRunning()) {
return;
}
if (mStartDelay > 0) {
// Do nothing if we already has pending call to acquireQuotaAndAnimate
if (!HandlerCompat.hasCallbacks(mUiHandler, mAcquireQuotaAndAnimateRunnable)) {
mUiHandler.postDelayed(mAcquireQuotaAndAnimateRunnable, mStartDelay);
}
} else {
acquireQuotaAndAnimate();
}
}
protected void acquireQuotaAndAnimate() {
// Only valid after setFloatValues/setIntValues has been called
if (mAnimator.getValues() == null) {
return;
}
if (mQuotaManager.tryAcquireQuota(1)) {
mListener.mIsUsingQuota.set(true);
mAnimator.start();
} else {
mListener.mIsUsingQuota.set(false);
// No need to jump to an end of animation if it can't be played when they are infinite.
if (!isInfiniteAnimator()) {
mAnimator.end();
}
}
}
/**
* 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() {
// Early out for finite animation, already running animation or no valid values before any
// setFloatValues or setIntValues call
if (!isInfiniteAnimator() || mAnimator.getValues() == null) {
return;
}
if (isPaused()) {
if (mQuotaManager.tryAcquireQuota(1)) {
mListener.mIsUsingQuota.set(true);
mAnimator.resume();
}
} else if (!isRunning()) {
// Infinite animators created when this node was invisible have not started yet.
tryStartAnimation();
}
}
/**
* Stops or pauses the animator, depending on it's state. If stopped, it will assign the end
* value.
*/
@UiThread
void stopOrPauseAnimator() {
if (isInfiniteAnimator()) {
// remove pending call to start the animation if any
mUiHandler.removeCallbacks(mAcquireQuotaAndAnimateRunnable);
// remove resume callback if the animation is during the repeat delay
mUiHandler.removeCallbacks(mListener.mResumeRepeatRunnable);
mAnimator.pause();
if (mListener.mIsUsingQuota.compareAndSet(true, false)) {
mQuotaManager.releaseQuota(1);
}
} else {
// This causes the animation to assign the end value of the property being animated.
// Quota will be released at onAnimationEnd()
stopAnimator();
}
}
/** Stops the animator, which will cause it to assign the end value. */
@UiThread
void stopAnimator() {
// remove pending call to start the animation if any
mUiHandler.removeCallbacks(mAcquireQuotaAndAnimateRunnable);
if (mAnimator.getValues() != null) {
endAnimator();
}
}
protected void endAnimator() {
mAnimator.end();
}
/** Returns whether the animator in this class has an infinite duration. */
protected boolean isInfiniteAnimator() {
return mAnimator.getTotalDuration() == Animator.DURATION_INFINITE;
}
/** Returns whether this node has a running animation. */
boolean isRunning() {
return mAnimator.isRunning();
}
/** Returns whether this node has a paused animation. */
boolean isPaused() {
return mAnimator.isPaused()
// Not during repeat delay
&& !HandlerCompat.hasCallbacks(mUiHandler, mListener.mResumeRepeatRunnable);
}
/**
* 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.
*/
protected 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 it is skipped by calling {@link android.animation
// .Animator#end()} because no quota is available.
@NonNull
final AtomicBoolean mIsUsingQuota = new AtomicBoolean(false);
private final int mRepeatMode;
private final long mForwardRepeatDelay;
private final long mReverseRepeatDelay;
@NonNull
private final Handler mHandler;
@NonNull
Runnable mResumeRepeatRunnable;
private boolean mIsReverse;
/**
* Only intended to be true with {@link QuotaAwareAnimatorWithAux} to play main and aux
* animators alternately, the pause and resume is still required to swap animators even
* without repeat delay.
*/
private final boolean mAlwaysPauseWhenRepeatForward;
QuotaReleasingAnimatorListener(
@NonNull QuotaManager quotaManager,
int repeatMode,
long forwardRepeatDelay,
long reverseRepeatDelay,
@NonNull Runnable resumeRepeatRunnable,
@NonNull Handler uiHandler,
boolean alwaysPauseWhenRepeatForward) {
this.mQuotaManager = quotaManager;
this.mRepeatMode = repeatMode;
this.mForwardRepeatDelay = forwardRepeatDelay;
this.mReverseRepeatDelay = reverseRepeatDelay;
this.mResumeRepeatRunnable = resumeRepeatRunnable;
this.mHandler = uiHandler;
mIsReverse = false;
mAlwaysPauseWhenRepeatForward = alwaysPauseWhenRepeatForward;
}
/**
* Only intended to be called from QuotaAwareAnimatorWithAux To play main and aux animators
* alternately, resume aux animator after pausing main animator, and resume main animator
* after pause aux animator.
*/
void setResumeRunnable(@NonNull Runnable runnable) {
mResumeRepeatRunnable = runnable;
}
@Override
@UiThread
public void onAnimationStart(Animator animation, boolean isReverse) {
super.onAnimationStart(animation, isReverse);
mIsReverse = isReverse;
}
@Override
@UiThread
public void onAnimationEnd(Animator animation) {
if (mIsUsingQuota.compareAndSet(true, false)) {
mQuotaManager.releaseQuota(1);
}
mHandler.removeCallbacks(mResumeRepeatRunnable);
}
@Override
@UiThread
public void onAnimationRepeat(Animator animation) {
if (mRepeatMode == ValueAnimator.REVERSE) {
mIsReverse = !mIsReverse;
} else {
mIsReverse = false;
}
if ((mAlwaysPauseWhenRepeatForward || mForwardRepeatDelay > 0) && !mIsReverse) {
animation.pause();
mHandler.postDelayed(mResumeRepeatRunnable, mForwardRepeatDelay);
} else if (mReverseRepeatDelay > 0 && mIsReverse) {
animation.pause();
mHandler.postDelayed(mResumeRepeatRunnable, mReverseRepeatDelay);
}
}
}
}