AnimationsHelper.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.ValueAnimator;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Pair;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationParameters;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.CubicBezierEasing;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.Easing;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.RepeatMode;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto.Repeatable;
import java.time.Duration;
import java.util.EnumMap;
import java.util.Map;
/**
* Helper class for Animations in ProtoLayout. It contains helper methods used in rendered and
* constants for default values.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class AnimationsHelper {
private static final Duration DEFAULT_ANIM_DURATION = Duration.ofMillis(300);
private static final Interpolator DEFAULT_ANIM_INTERPOLATOR = new LinearInterpolator();
private static final Duration DEFAULT_ANIM_DELAY = Duration.ZERO;
private static final int DEFAULT_REPEAT_COUNT = 0;
private static final int DEFAULT_REPEAT_MODE = ValueAnimator.RESTART;
private static final Map<RepeatMode, Integer> sRepeatModeForAnimator =
new EnumMap<>(RepeatMode.class);
static {
sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_UNKNOWN, DEFAULT_REPEAT_MODE);
sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_RESTART, ValueAnimator.RESTART);
sRepeatModeForAnimator.put(RepeatMode.REPEAT_MODE_REVERSE, ValueAnimator.REVERSE);
}
private AnimationsHelper() {
}
/** Returns the main duration from the given {@link AnimationSpec} or default value if not
* set. */
@SuppressWarnings("deprecation") // Make sure the deprecated method is valid for compatibility
@NonNull
public static Duration getMainDurationOrDefault(@NonNull AnimationSpec spec) {
return spec.hasAnimationParameters()
&& spec.getAnimationParameters().getDurationMillis() > 0
? Duration.ofMillis(spec.getAnimationParameters().getDurationMillis())
: spec.getDurationMillis() > 0
? Duration.ofMillis(spec.getDurationMillis())
: DEFAULT_ANIM_DURATION;
}
/** Returns the main delay from the given {@link AnimationSpec} or default value if not set. */
@SuppressWarnings("deprecation") // Make sure the deprecated method is valid for compatibility
@NonNull
public static Duration getMainDelayOrDefault(@NonNull AnimationSpec spec) {
return spec.getAnimationParameters().hasDelayMillis()
? Duration.ofMillis(spec.getAnimationParameters().getDelayMillis())
: spec.getStartDelayMillis() > 0
? Duration.ofMillis(spec.getStartDelayMillis())
: DEFAULT_ANIM_DELAY;
}
/**
* Returns the main easing converted to the Interpolator from the given {@link AnimationSpec} or
* default value if not set.
*/
@SuppressWarnings("deprecation") // Make sure the deprecated method is valid for compatibility
@NonNull
public static Interpolator getMainInterpolatorOrDefault(@NonNull AnimationSpec spec) {
Interpolator interpolator = DEFAULT_ANIM_INTERPOLATOR;
Easing easing = null;
if (spec.getAnimationParameters().hasEasing()) {
easing = spec.getAnimationParameters().getEasing();
} else if (spec.hasEasing()) {
easing = spec.getEasing();
}
if (easing != null) {
switch (easing.getInnerCase()) {
case CUBIC_BEZIER:
if (easing.hasCubicBezier()) {
CubicBezierEasing cbe = easing.getCubicBezier();
interpolator = new PathInterpolator(cbe.getX1(), cbe.getY1(), cbe.getX2(),
cbe.getY2());
}
break;
case INNER_NOT_SET:
break;
}
}
return interpolator;
}
/**
* Returns the repeat count from the given {@link AnimationSpec} or default value if not set
*/
public static int getRepeatCountOrDefault(@NonNull AnimationSpec spec) {
int repeatCount = DEFAULT_REPEAT_COUNT;
if (spec.hasRepeatable()) {
Repeatable repeatable = spec.getRepeatable();
if (repeatable.getIterations() <= 0) {
repeatCount = ValueAnimator.INFINITE;
} else {
// -1 because ValueAnimator uses count as number of how many times will animation be
// repeated in addition to the first play.
repeatCount = repeatable.getIterations() - 1;
}
}
return repeatCount;
}
/** Returns the repeat mode from the given {@link AnimationSpec} or default value if not set. */
public static int getRepeatModeOrDefault(@NonNull AnimationSpec spec) {
int repeatMode = DEFAULT_REPEAT_MODE;
if (spec.hasRepeatable()) {
Repeatable repeatable = spec.getRepeatable();
Integer repeatModeFromMap = sRepeatModeForAnimator.get(repeatable.getRepeatMode());
if (repeatModeFromMap != null) {
repeatMode = repeatModeFromMap;
}
}
return repeatMode;
}
// public static Duration getOverrideForwardDurationOrDefault(@NonNull AnimationSpec spec) {...}
@NonNull
public static Duration getOverrideReverseDurationOrDefault(@NonNull AnimationSpec spec) {
if (spec.hasRepeatable()) {
Repeatable repeatable = spec.getRepeatable();
if (repeatable.hasReverseRepeatOverride()) {
AnimationParameters reverseParameters = repeatable.getReverseRepeatOverride();
return reverseParameters.getDurationMillis() > 0
? Duration.ofMillis(reverseParameters.getDurationMillis())
: getMainDurationOrDefault(spec);
}
}
return getMainDurationOrDefault(spec);
}
/**
* Returns true when a reverse duration which is different from forward duration is provided
* for a reverse repeated animation.
*/
static boolean hasCustomReverseDuration(@NonNull AnimationSpec spec) {
return spec.hasRepeatable()
&& getRepeatCountOrDefault(spec) != 0
&& getRepeatModeOrDefault(spec) == ValueAnimator.REVERSE
&& getOverrideReverseDurationOrDefault(spec).toMillis()
!= getMainDurationOrDefault(spec).toMillis();
}
static class RepeatDelays {
int mForwardRepeatDelay;
int mReverseRepeatDelay;
RepeatDelays(int forwardRepeatDelay, int reverseRepeatDelay) {
mForwardRepeatDelay = forwardRepeatDelay;
mReverseRepeatDelay = reverseRepeatDelay;
}
}
/** Return the pair of forward repeat delay and reverse repeat delay */
static RepeatDelays getRepeatDelays(AnimationSpec spec) {
int mainDelay = (int) getMainDelayOrDefault(spec).toMillis();
int forwardRepeatDelay = mainDelay;
int reverseRepeatDelay = mainDelay;
int repeatCount = getRepeatCountOrDefault(spec);
if (repeatCount > 0 || repeatCount == ValueAnimator.INFINITE) {
if (spec.getRepeatable().getForwardRepeatOverride().hasDelayMillis()) {
forwardRepeatDelay =
spec.getRepeatable().getForwardRepeatOverride().getDelayMillis();
}
if (getRepeatModeOrDefault(spec) == ValueAnimator.REVERSE
&& spec.getRepeatable().getReverseRepeatOverride().hasDelayMillis()) {
reverseRepeatDelay =
spec.getRepeatable().getReverseRepeatOverride().getDelayMillis();
}
}
return new RepeatDelays(forwardRepeatDelay, reverseRepeatDelay);
}
/**
* When a reverse duration which is different from forward duration is provided for a reverse
* repeated animation, the spec would be split into main and aux specs. Main spec is for the
* animator which require/release quota and plays the forward part of animation; and aux spec is
* for the animator which plays the reverse part of animation, syncs with the main animator and
* consumes no quota. These two specs are passed into {@link QuotaAwareAnimatorWithAux} to
* create
* main and aux animators which are played alternately. For other cases, null is returned as no
* split would happen.
*/
@Nullable
static Pair<AnimationSpec, AnimationSpec> maybeSplitToMainAndAuxAnimationSpec(
@NonNull AnimationSpec spec) {
if (!hasCustomReverseDuration(spec)) {
return null;
}
int forwardDuration = (int) getMainDurationOrDefault(spec).toMillis();
int reverseDuration = (int) getOverrideReverseDurationOrDefault(spec).toMillis();
Repeatable repeatable = spec.getRepeatable();
RepeatDelays repeatDelays = getRepeatDelays(spec);
Easing easing = null;
if (spec.getAnimationParameters().hasEasing()) {
easing = spec.getAnimationParameters().getEasing();
}
AnimationParameters.Builder mainParametersBuilder =
AnimationParameters.newBuilder()
.setDurationMillis(forwardDuration)
.setDelayMillis((int) getMainDelayOrDefault(spec).toMillis());
if (easing != null) {
mainParametersBuilder.setEasing(easing);
}
AnimationSpec mainAnimatorSpec =
AnimationSpec.newBuilder()
.setAnimationParameters(mainParametersBuilder.build())
.setRepeatable(
Repeatable.newBuilder()
.setIterations((repeatable.getIterations() + 1) / 2)
.setRepeatMode(RepeatMode.REPEAT_MODE_RESTART)
.setForwardRepeatOverride(
AnimationParameters.newBuilder()
.setDelayMillis(
repeatDelays.mReverseRepeatDelay)
.build())
.build())
.build();
AnimationParameters.Builder auxParametersBuilder =
AnimationParameters.newBuilder()
.setDurationMillis(reverseDuration)
// The aux animator plays the reverse part of animation, so wait until
// the first pass of forward animation has run, plus repeat delay if any.
.setDelayMillis(forwardDuration + repeatDelays.mReverseRepeatDelay);
if (spec.getRepeatable().getReverseRepeatOverride().hasEasing()) {
easing = spec.getRepeatable().getReverseRepeatOverride().getEasing();
}
if (easing != null) {
auxParametersBuilder.setEasing(easing);
}
AnimationSpec auxAnimatorSpec =
AnimationSpec.newBuilder()
.setAnimationParameters(auxParametersBuilder.build())
.setRepeatable(
Repeatable.newBuilder()
.setIterations(repeatable.getIterations() / 2)
.setRepeatMode(RepeatMode.REPEAT_MODE_RESTART)
.setForwardRepeatOverride(
AnimationParameters.newBuilder()
.setDelayMillis(
repeatDelays.mForwardRepeatDelay)
.build())
.build())
.build();
return Pair.create(mainAnimatorSpec, auxAnimatorSpec);
}
/**
* Sets animation parameters (duration, delay, easing, repeat mode and count) to the given
* animator. These will be values from the given AnimationSpec if they are set and default
* values otherwise.
*/
public static void applyAnimationSpecToAnimator(
@NonNull ValueAnimator animator, @NonNull AnimationSpec spec) {
animator.setDuration(getMainDurationOrDefault(spec).toMillis());
animator.setStartDelay(getMainDelayOrDefault(spec).toMillis());
animator.setInterpolator(getMainInterpolatorOrDefault(spec));
animator.setRepeatCount(getRepeatCountOrDefault(spec));
animator.setRepeatMode(getRepeatModeOrDefault(spec));
}
/**
* Sets animation parameters (duration, delay, easing, repeat mode and count) to the given
* animation. These will be values from the given AnimationSpec if they are set and default
* values otherwise.
*/
public static void applyAnimationSpecToAnimation(
@NonNull Animation animation, @NonNull AnimationSpec spec) {
animation.setDuration(getMainDurationOrDefault(spec).toMillis());
animation.setStartOffset(getMainDelayOrDefault(spec).toMillis());
animation.setInterpolator(getMainInterpolatorOrDefault(spec));
animation.setRepeatCount(getRepeatCountOrDefault(spec));
animation.setRepeatMode(getRepeatModeOrDefault(spec));
}
}