QuotaAwareAnimatorWithAux.java

/*
 * Copyright 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 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.TypeEvaluator;
import android.animation.ValueAnimator;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.HandlerCompat;
import androidx.wear.protolayout.expression.pipeline.AnimationsHelper.RepeatDelays;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;

/**
 * This class handles the animation with custom reverse duration. To have different duration for
 * forward and reverse animations, two animators are played alternately as follows:
 *
 * <p>1. After the start delay, start both animators. 2. Main animator plays forward part of the
 * animation, aux animator waits for its extra start delay of (forward duration + reverse delay). 3.
 * Main animator pauses before repeat and calls aux animator to resume after reverse delay. 4. Aux
 * animator plays reverse part of animation; main animator is paused. 5. Aux animator pauses before
 * repeat and calls main animator to resume after forward delay. 6. Main animator plays forward part
 * of the animation; aux animator is paused. 7. .....
 */
class QuotaAwareAnimatorWithAux extends QuotaAwareAnimator {

    @NonNull private final QuotaReleasingAnimatorListener mAuxListener;
    @NonNull private final ValueAnimator mAuxAnimator;
    private boolean mSuppressForwardUpdate = false;
    private boolean mSuppressReverseUpdate = false;
    private final boolean mEndsWithForward;

    QuotaAwareAnimatorWithAux(
            @NonNull QuotaManager quotaManager,
            @NonNull AnimationSpec spec,
            @NonNull AnimationSpec auxSpec,
            @Nullable TypeEvaluator<?> evaluator) {
        super(quotaManager, spec, evaluator, /* alwaysPauseWhenRepeatForward= */ true);

        mAuxAnimator = new ValueAnimator();
        applyAnimationSpecToAnimator(mAuxAnimator, auxSpec);
        RepeatDelays repeatDelays = getRepeatDelays(auxSpec);
        mAuxListener =
                new QuotaReleasingAnimatorListener(
                        quotaManager,
                        mAuxAnimator.getRepeatMode(),
                        repeatDelays.mForwardRepeatDelay,
                        repeatDelays.mReverseRepeatDelay,
                        mAnimator::resume,
                        mUiHandler,
                        /* alwaysPauseWhenRepeatForward= */ true);
        mAuxAnimator.addListener(mAuxListener);

        mAcquireQuotaAndAnimateRunnable = this::acquireQuotaAndAnimate;
        mListener.setResumeRunnable(mAuxAnimator::resume);
        mEndsWithForward = mAnimator.getRepeatCount() > mAuxAnimator.getRepeatCount();
    }

    @Override
    void addUpdateCallback(@NonNull UpdateCallback updateCallback) {
        // 1. Do not update the animated value when pausing for swap, or there is a jumping frame.
        // 2. Suppress the update temporarily to avoid assigning one of the end values depending
        // on the repeating count.
        mAnimator.addUpdateListener(
                animation -> {
                    if (!mSuppressForwardUpdate && !mAnimator.isPaused()) {
                        updateCallback.onUpdate(animation.getAnimatedValue());
                    }
                });

        mAuxAnimator.addUpdateListener(
                animation -> {
                    if (!mSuppressReverseUpdate && !mAuxAnimator.isPaused()) {
                        updateCallback.onUpdate(animation.getAnimatedValue());
                    }
                });
    }

    @Override
    void setFloatValues(float... values) {
        super.setFloatValues(values);

        // reverse the value array
        float temp;
        for (int i = 0; i < values.length / 2; i++) {
            temp = values[i];
            values[i] = values[values.length - 1 - i];
            values[values.length - 1 - i] = temp;
        }
        setFloatValues(mAuxAnimator, mEvaluator, values);
    }

    @Override
    void setIntValues(int... values) {
        super.setIntValues(values);

        // reverse the value array
        int temp;
        for (int i = 0; i < values.length / 2; i++) {
            temp = values[i];
            values[i] = values[values.length - 1 - i];
            values[values.length - 1 - i] = temp;
        }
        setIntValues(mAuxAnimator, mEvaluator, values);
    }

    @Override
    protected void acquireQuotaAndAnimate() {
        super.acquireQuotaAndAnimate();
        if (mAnimator.isStarted()) {
            mAuxAnimator.start();
        }
    }

    @Override
    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);
                // to simplify the synchronization after pause, always resume the main animator
                // and set the aux animator to beginning to be ready to resume before repeating
                // the main one.
                mAnimator.resume();
                mAuxAnimator.setCurrentFraction(0);
            }
        } else if (!isRunning()) {
            // Infinite animators created when this node was invisible have not started yet.
            tryStartAnimation();
        }
    }

    @Override
    void stopOrPauseAnimator() {
        super.stopOrPauseAnimator();
        if (isInfiniteAnimator()) {
            mAuxAnimator.pause();
            mUiHandler.removeCallbacks(mAuxListener.mResumeRepeatRunnable);
        }
    }

    @Override
    protected void endAnimator() {
        mSuppressForwardUpdate = !mEndsWithForward;
        mSuppressReverseUpdate = mEndsWithForward;
        mAnimator.end();
        mAuxAnimator.end();
        mSuppressForwardUpdate = false;
        mSuppressReverseUpdate = false;
    }

    /** Returns whether this node has a running animation. */
    @Override
    protected boolean isRunning() {
        return super.isRunning() || mAuxAnimator.isRunning();
    }

    @Override
    protected boolean isPaused() {
        return super.isPaused()
                && mAuxAnimator.isPaused()
                && !HandlerCompat.hasCallbacks(mUiHandler, mAuxListener.mResumeRepeatRunnable);
    }
}