AnimatableNode.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 static androidx.wear.protolayout.expression.pipeline.AnimationsHelper.maybeSplitToMainAndAuxAnimationSpec;

import android.animation.ArgbEvaluator;
import android.animation.TypeEvaluator;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Pair;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;

/** Data animatable source node within a dynamic data pipeline. */
abstract class AnimatableNode {
    static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator();

    private boolean mIsVisible = false;
    @NonNull
    final QuotaAwareAnimator mQuotaAwareAnimator;

    protected AnimatableNode(@NonNull QuotaManager quotaManager, @NonNull AnimationSpec spec) {
        this(quotaManager, spec, null);
    }

    protected AnimatableNode(
            @NonNull QuotaManager quotaManager,
            @NonNull AnimationSpec spec,
            @Nullable TypeEvaluator<?> evaluator) {
        // When a reverse duration which is different from forward duration is provided for a
        // reverse repeated animation, we need to split the spec into two and use
        // QuotaAwareAnimatorWithAux to create two ValueAnimators internally to achieve the
        // required effect. For other cases, use QuotaAwareAnimator.
        Pair<AnimationSpec, AnimationSpec> specs = maybeSplitToMainAndAuxAnimationSpec(spec);
        if (specs != null) {
            mQuotaAwareAnimator =
                    new QuotaAwareAnimatorWithAux(quotaManager, specs.first, specs.second,
                            evaluator);
        } else {
            mQuotaAwareAnimator = new QuotaAwareAnimator(quotaManager, spec, evaluator);
        }
    }

    @VisibleForTesting
    AnimatableNode(@NonNull QuotaAwareAnimator quotaAwareAnimator) {
        mQuotaAwareAnimator = quotaAwareAnimator;
    }

    /**
     * Starts the animator (if present) if the node is visible and there is a quota, otherwise, skip
     * it.
     */
    @UiThread
    protected void startOrSkipAnimator() {
        if (mIsVisible) {
            mQuotaAwareAnimator.tryStartAnimation();
        } else {
            stopOrPauseAnimator();
        }
    }

    /**
     * Sets the node's visibility and resumes or stops the corresponding animator (if present).
     *
     * <p>If it's becoming visible, paused animations are resumed, other infinite animations that
     * haven't started yet will start.
     *
     * <p>If it's becoming invisible, all animations should skip to end or pause.
     */
    @UiThread
    void setVisibility(boolean visible) {
        if (mIsVisible == visible) {
            return;
        }
        mIsVisible = visible;
        if (mIsVisible) {
            startOrResumeAnimator();
        } else if (mQuotaAwareAnimator.isRunning()) {
            stopOrPauseAnimator();
        }
    }

    /**
     * Starts or resumes the animator if there is a quota, depending on whether the animation was
     * paused.
     */
    private void startOrResumeAnimator() {
        mQuotaAwareAnimator.tryStartOrResumeInfiniteAnimation();
    }

    /** Returns whether this node has a running animation. */
    boolean hasRunningAnimation() {
        return mQuotaAwareAnimator.isRunning();
    }

    /** Returns whether the animator in this node has an infinite duration. */
    @VisibleForTesting
    protected boolean isInfiniteAnimator() {
        return mQuotaAwareAnimator.isInfiniteAnimator();
    }

    /**
     * Pauses the animator in this node if it has infinite duration, stop it otherwise. Note that
     * this method has no effect on infinite animators that are not running since Animator#pause
     * will be a no-op in that case.
     */
    private void stopOrPauseAnimator() {
        mQuotaAwareAnimator.stopOrPauseAnimator();
    }
}