AnimationParameterBuilders.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;

import static androidx.wear.protolayout.expression.Preconditions.checkNotNull;

import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.protolayout.expression.proto.AnimationParameterProto;
import androidx.wear.protolayout.protobuf.ExtensionRegistryLite;
import androidx.wear.protolayout.protobuf.InvalidProtocolBufferException;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** Builders for parameters that can be used to customize an animation. */
public final class AnimationParameterBuilders {
    private AnimationParameterBuilders() {}

    /** The repeat mode to specify how animation will behave when repeated. */
    @RequiresSchemaVersion(major = 1, minor = 200)
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @IntDef({REPEAT_MODE_UNKNOWN, REPEAT_MODE_RESTART, REPEAT_MODE_REVERSE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface RepeatMode {}

    /** The unknown repeat mode. */
    @RequiresSchemaVersion(major = 1, minor = 200)
    public static final int REPEAT_MODE_UNKNOWN = 0;

    /** The repeat mode where animation restarts from the beginning when repeated. */
    @RequiresSchemaVersion(major = 1, minor = 200)
    public static final int REPEAT_MODE_RESTART = 1;

    /** The repeat mode where animation is played in reverse when repeated. */
    @RequiresSchemaVersion(major = 1, minor = 200)
    public static final int REPEAT_MODE_REVERSE = 2;

    /** Animation parameters that can be added to any animatable node. */
    @RequiresSchemaVersion(major = 1, minor = 200)
    public static final class AnimationSpec {
        private final AnimationParameterProto.AnimationSpec mImpl;
        @Nullable private final Fingerprint mFingerprint;

        AnimationSpec(
                AnimationParameterProto.AnimationSpec impl, @Nullable Fingerprint fingerprint) {
            this.mImpl = impl;
            this.mFingerprint = fingerprint;
        }

        /** Gets animation parameters including duration, easing and repeat delay. */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @Nullable
        public AnimationParameters getAnimationParameters() {
            if (mImpl.hasAnimationParameters()) {
                return AnimationParameters.fromProto(mImpl.getAnimationParameters());
            } else {
                return null;
            }
        }

        /**
         * Gets the repeatable mode to be used for specifying repetition parameters for the
         * animation.
         */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @Nullable
        public Repeatable getRepeatable() {
            if (mImpl.hasRepeatable()) {
                return Repeatable.fromProto(mImpl.getRepeatable());
            } else {
                return null;
            }
        }

        /** Get the fingerprint for this object, or null if unknown. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @Nullable
        public Fingerprint getFingerprint() {
            return mFingerprint;
        }

        /** Creates a new wrapper instance from the proto. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AnimationSpec fromProto(
                @NonNull AnimationParameterProto.AnimationSpec proto,
                @Nullable Fingerprint fingerprint) {
            return new AnimationSpec(proto, fingerprint);
        }

        /**
         * Creates a new wrapper instance from the proto. Intended for testing purposes only. An
         * object created using this method can't be added to any other wrapper.
         */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AnimationSpec fromProto(
                @NonNull AnimationParameterProto.AnimationSpec proto) {
            return fromProto(proto, null);
        }

        /** Returns the internal proto instance. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public AnimationParameterProto.AnimationSpec toProto() {
            return mImpl;
        }

        @Override
        @NonNull
        public String toString() {
            return "AnimationSpec{"
                    + "animationParameters="
                    + getAnimationParameters()
                    + ", repeatable="
                    + getRepeatable()
                    + "}";
        }

        /** Builder for {@link AnimationSpec} */
        public static final class Builder {
            private final AnimationParameterProto.AnimationSpec.Builder mImpl =
                    AnimationParameterProto.AnimationSpec.newBuilder();
            private final Fingerprint mFingerprint = new Fingerprint(-2136602843);

            public Builder() {}

            /** Sets animation parameters including duration, easing and repeat delay. */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setAnimationParameters(
                    @NonNull AnimationParameters animationParameters) {
                mImpl.setAnimationParameters(animationParameters.toProto());
                mFingerprint.recordPropertyUpdate(
                        4,
                        checkNotNull(animationParameters.getFingerprint()).aggregateValueAsInt());
                return this;
            }

            /**
             * Sets the repeatable mode to be used for specifying repetition parameters for the
             * animation. If not set, animation won't be repeated.
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setRepeatable(@NonNull Repeatable repeatable) {
                mImpl.setRepeatable(repeatable.toProto());
                mFingerprint.recordPropertyUpdate(
                        5, checkNotNull(repeatable.getFingerprint()).aggregateValueAsInt());
                return this;
            }

            /** Builds an instance from accumulated values. */
            @NonNull
            public AnimationSpec build() {
                return new AnimationSpec(mImpl.build(), mFingerprint);
            }
        }
    }

    /** Animation specs of duration, easing and repeat delay. */
    @RequiresSchemaVersion(major = 1, minor = 200)
    public static final class AnimationParameters {
        private final AnimationParameterProto.AnimationParameters mImpl;
        @Nullable private final Fingerprint mFingerprint;

        AnimationParameters(
                AnimationParameterProto.AnimationParameters impl,
                @Nullable Fingerprint fingerprint) {
            this.mImpl = impl;
            this.mFingerprint = fingerprint;
        }

        /** Gets the duration of the animation in milliseconds. */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @IntRange(from = 0)
        public long getDurationMillis() {
            return mImpl.getDurationMillis();
        }

        /** Gets the easing to be used for adjusting an animation's fraction. */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @Nullable
        public Easing getEasing() {
            if (mImpl.hasEasing()) {
                return AnimationParameterBuilders.easingFromProto(mImpl.getEasing());
            } else {
                return null;
            }
        }

        /**
         * Gets animation delay in millis. When used outside repeatable, this is the delay to start
         * the animation in milliseconds. When set inside repeatable, this is the delay before
         * repeating animation in milliseconds.
         */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @IntRange(from = 0)
        public long getDelayMillis() {
            return mImpl.getDelayMillis();
        }

        /** Get the fingerprint for this object, or null if unknown. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @Nullable
        public Fingerprint getFingerprint() {
            return mFingerprint;
        }

        /** Creates a new wrapper instance from the proto. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static AnimationParameters fromProto(
                @NonNull AnimationParameterProto.AnimationParameters proto,
                @Nullable Fingerprint fingerprint) {
            return new AnimationParameters(proto, fingerprint);
        }

        @NonNull
        static AnimationParameters fromProto(
                @NonNull AnimationParameterProto.AnimationParameters proto) {
            return fromProto(proto, null);
        }

        /** Returns the internal proto instance. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public AnimationParameterProto.AnimationParameters toProto() {
            return mImpl;
        }

        @Override
        @NonNull
        public String toString() {
            return "AnimationParameters{"
                    + "durationMillis="
                    + getDurationMillis()
                    + ", easing="
                    + getEasing()
                    + ", delayMillis="
                    + getDelayMillis()
                    + "}";
        }

        /** Builder for {@link AnimationParameters} */
        public static final class Builder {
            private final AnimationParameterProto.AnimationParameters.Builder mImpl =
                    AnimationParameterProto.AnimationParameters.newBuilder();
            private final Fingerprint mFingerprint = new Fingerprint(-1301308590);

            public Builder() {}

            /**
             * Sets the duration of the animation in milliseconds. If not set, defaults to 300ms.
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setDurationMillis(@IntRange(from = 0) long durationMillis) {
                mImpl.setDurationMillis(durationMillis);
                mFingerprint.recordPropertyUpdate(1, Long.hashCode(durationMillis));
                return this;
            }

            /**
             * Sets the easing to be used for adjusting an animation's fraction. If not set,
             * defaults to Linear Interpolator.
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setEasing(@NonNull Easing easing) {
                mImpl.setEasing(easing.toEasingProto());
                mFingerprint.recordPropertyUpdate(
                        2, checkNotNull(easing.getFingerprint()).aggregateValueAsInt());
                return this;
            }

            /**
             * Sets animation delay in millis. When used outside repeatable, this is the delay to
             * start the animation in milliseconds. When set inside repeatable, this is the delay
             * before repeating animation in milliseconds. If not set, no delay will be applied.
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setDelayMillis(@IntRange(from = 0) long delayMillis) {
                mImpl.setDelayMillis(delayMillis);
                mFingerprint.recordPropertyUpdate(3, Long.hashCode(delayMillis));
                return this;
            }

            /** Builds an instance from accumulated values. */
            @NonNull
            public AnimationParameters build() {
                return new AnimationParameters(mImpl.build(), mFingerprint);
            }
        }
    }

    /**
     * Interface defining the easing to be used for adjusting an animation's fraction. This allows
     * animation to speed up and slow down, rather than moving at a constant rate. If not set,
     * defaults to Linear Interpolator.
     */
    @RequiresSchemaVersion(major = 1, minor = 200)
    public interface Easing {
        /**
         * The cubic polynomial easing that implements third-order Bezier curves. This is equivalent
         * to the Android PathInterpolator.
         *
         * @param x1 the x coordinate of the first control point. The line through the point (0, 0)
         *     and the first control point is tangent to the easing at the point (0, 0).
         * @param y1 the y coordinate of the first control point. The line through the point (0, 0)
         *     and the first control point is tangent to the easing at the point (0, 0).
         * @param x2 the x coordinate of the second control point. The line through the point (1, 1)
         *     and the second control point is tangent to the easing at the point (1, 1).
         * @param y2 the y coordinate of the second control point. The line through the point (1, 1)
         *     and the second control point is tangent to the easing at the point (1, 1).
         */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @NonNull
        static Easing cubicBezier(float x1, float y1, float x2, float y2) {
            return new CubicBezierEasing.Builder().setX1(x1).setY1(y1).setX2(x2).setY2(y2).build();
        }

        /**
         * Elements that begin and end at rest use this standard easing. They speed up quickly and
         * slow down gradually, in order to emphasize the end of the transition.
         *
         * <p>Standard easing puts subtle attention at the end of an animation, by giving more time
         * to deceleration than acceleration. It is the most common form of easing.
         *
         * <p>This is equivalent to the Compose {@code FastOutSlowInEasing}.
         */
        @NonNull Easing FAST_OUT_SLOW_IN_EASING = cubicBezier(0.4f, 0.0f, 0.2f, 1.0f);

        /**
         * Incoming elements are animated using deceleration easing, which starts a transition at
         * peak velocity (the fastest point of an element’s movement) and ends at rest.
         *
         * <p>This is equivalent to the Compose {@code LinearOutSlowInEasing}.
         */
        @NonNull Easing LINEAR_OUT_SLOW_IN_EASING = cubicBezier(0.0f, 0.0f, 0.2f, 1.0f);

        /**
         * Elements exiting a screen use acceleration easing, where they start at rest and end at
         * peak velocity.
         *
         * <p>This is equivalent to the Compose {@code FastOutLinearInEasing}.
         */
        @NonNull Easing FAST_OUT_LINEAR_IN_EASING = cubicBezier(0.4f, 0.0f, 1.0f, 1.0f);

        /** Get the protocol buffer representation of this object. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        AnimationParameterProto.Easing toEasingProto();

        /** Creates a {@link Easing} from a byte array generated by {@link #toEasingByteArray()}. */
        @NonNull
        static Easing fromByteArray(@NonNull byte[] byteArray) {
            try {
                return easingFromProto(
                        AnimationParameterProto.Easing.parseFrom(
                                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
            } catch (InvalidProtocolBufferException e) {
                throw new IllegalArgumentException("Byte array could not be parsed into Easing", e);
            }
        }

        /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
        @NonNull
        default byte[] toEasingByteArray() {
            return toEasingProto().toByteArray();
        }

        /** Get the fingerprint for this object or null if unknown. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @Nullable
        Fingerprint getFingerprint();

        /** Builder to create {@link Easing} objects. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        interface Builder {

            /** Builds an instance with values accumulated in this Builder. */
            @NonNull
            Easing build();
        }
    }

    /** Creates a new wrapper instance from the proto. */
    @RestrictTo(Scope.LIBRARY_GROUP)
    @NonNull
    public static Easing easingFromProto(
            @NonNull AnimationParameterProto.Easing proto, @Nullable Fingerprint fingerprint) {
        if (proto.hasCubicBezier()) {
            return CubicBezierEasing.fromProto(proto.getCubicBezier(), fingerprint);
        }
        throw new IllegalStateException("Proto was not a recognised instance of Easing");
    }

    @NonNull
    static Easing easingFromProto(@NonNull AnimationParameterProto.Easing proto) {
        return easingFromProto(proto, null);
    }

    /**
     * The cubic polynomial easing that implements third-order Bezier curves. This is equivalent to
     * the Android PathInterpolator.
     */
    @RequiresSchemaVersion(major = 1, minor = 200)
    static final class CubicBezierEasing implements Easing {
        private final AnimationParameterProto.CubicBezierEasing mImpl;
        @Nullable private final Fingerprint mFingerprint;

        CubicBezierEasing(
                AnimationParameterProto.CubicBezierEasing impl, @Nullable Fingerprint fingerprint) {
            this.mImpl = impl;
            this.mFingerprint = fingerprint;
        }

        /**
         * Gets the x coordinate of the first control point. The line through the point (0, 0) and
         * the first control point is tangent to the easing at the point (0, 0).
         */
        @RequiresSchemaVersion(major = 1, minor = 200)
        public float getX1() {
            return mImpl.getX1();
        }

        /**
         * Gets the y coordinate of the first control point. The line through the point (0, 0) and
         * the first control point is tangent to the easing at the point (0, 0).
         */
        @RequiresSchemaVersion(major = 1, minor = 200)
        public float getY1() {
            return mImpl.getY1();
        }

        /**
         * Gets the x coordinate of the second control point. The line through the point (1, 1) and
         * the second control point is tangent to the easing at the point (1, 1).
         */
        @RequiresSchemaVersion(major = 1, minor = 200)
        public float getX2() {
            return mImpl.getX2();
        }

        /**
         * Gets the y coordinate of the second control point. The line through the point (1, 1) and
         * the second control point is tangent to the easing at the point (1, 1).
         */
        @RequiresSchemaVersion(major = 1, minor = 200)
        public float getY2() {
            return mImpl.getY2();
        }

        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @Nullable
        public Fingerprint getFingerprint() {
            return mFingerprint;
        }

        /** Creates a new wrapper instance from the proto. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static CubicBezierEasing fromProto(
                @NonNull AnimationParameterProto.CubicBezierEasing proto,
                @Nullable Fingerprint fingerprint) {
            return new CubicBezierEasing(proto, fingerprint);
        }

        @NonNull
        static CubicBezierEasing fromProto(
                @NonNull AnimationParameterProto.CubicBezierEasing proto) {
            return fromProto(proto, null);
        }

        /** Returns the internal proto instance. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        AnimationParameterProto.CubicBezierEasing toProto() {
            return mImpl;
        }

        @Override
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public AnimationParameterProto.Easing toEasingProto() {
            return AnimationParameterProto.Easing.newBuilder().setCubicBezier(mImpl).build();
        }

        @Override
        @NonNull
        public String toString() {
            return "CubicBezierEasing{"
                    + "x1="
                    + getX1()
                    + ", y1="
                    + getY1()
                    + ", x2="
                    + getX2()
                    + ", y2="
                    + getY2()
                    + "}";
        }

        /** Builder for {@link CubicBezierEasing}. */
        static final class Builder implements Easing.Builder {
            private final AnimationParameterProto.CubicBezierEasing.Builder mImpl =
                    AnimationParameterProto.CubicBezierEasing.newBuilder();
            private final Fingerprint mFingerprint = new Fingerprint(856403705);

            public Builder() {}

            /**
             * Sets the x coordinate of the first control point. The line through the point (0, 0)
             * and the first control point is tangent to the easing at the point (0, 0).
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setX1(float x1) {
                mImpl.setX1(x1);
                mFingerprint.recordPropertyUpdate(1, Float.floatToIntBits(x1));
                return this;
            }

            /**
             * Sets the y coordinate of the first control point. The line through the point (0, 0)
             * and the first control point is tangent to the easing at the point (0, 0).
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setY1(float y1) {
                mImpl.setY1(y1);
                mFingerprint.recordPropertyUpdate(2, Float.floatToIntBits(y1));
                return this;
            }

            /**
             * Sets the x coordinate of the second control point. The line through the point (1, 1)
             * and the second control point is tangent to the easing at the point (1, 1).
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setX2(float x2) {
                mImpl.setX2(x2);
                mFingerprint.recordPropertyUpdate(3, Float.floatToIntBits(x2));
                return this;
            }

            /**
             * Sets the y coordinate of the second control point. The line through the point (1, 1)
             * and the second control point is tangent to the easing at the point (1, 1).
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setY2(float y2) {
                mImpl.setY2(y2);
                mFingerprint.recordPropertyUpdate(4, Float.floatToIntBits(y2));
                return this;
            }

            @Override
            @NonNull
            public CubicBezierEasing build() {
                return new CubicBezierEasing(mImpl.build(), mFingerprint);
            }
        }
    }

    /** The repeatable mode to be used for specifying how many times animation will be repeated. */
    @RequiresSchemaVersion(major = 1, minor = 200)
    public static final class Repeatable {

        /**
         * An infinite {@link Repeatable} where animation restarts from the beginning when repeated.
         */
        public static final Repeatable INFINITE_REPEATABLE_WITH_RESTART =
                new Repeatable.Builder().setRepeatMode(REPEAT_MODE_RESTART).build();

        /** An infinite {@link Repeatable} where animation is played in reverse when repeated. */
        public static final Repeatable INFINITE_REPEATABLE_WITH_REVERSE =
                new Repeatable.Builder().setRepeatMode(REPEAT_MODE_REVERSE).build();

        private final AnimationParameterProto.Repeatable mImpl;
        @Nullable private final Fingerprint mFingerprint;

        Repeatable(AnimationParameterProto.Repeatable impl, @Nullable Fingerprint fingerprint) {
            this.mImpl = impl;
            this.mFingerprint = fingerprint;
        }

        /**
         * Gets the number specifying how many times animation will be repeated. this method can
         * only be called if {@link #hasInfiniteIteration()} is false.
         *
         * @throws IllegalStateException if {@link #hasInfiniteIteration()} is true.
         */
        @RequiresSchemaVersion(major = 1, minor = 200)
        public int getIterations() {
            if (hasInfiniteIteration()) {
                throw new IllegalStateException("Repeatable has infinite iteration.");
            }
            return mImpl.getIterations();
        }

        /** Returns true if the animation has indefinite repeat. */
        public boolean hasInfiniteIteration() {
            return isInfiniteIteration(mImpl.getIterations());
        }

        /** Gets the repeat mode to specify how animation will behave when repeated. */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @RepeatMode
        public int getRepeatMode() {
            return mImpl.getRepeatMode().getNumber();
        }

        /** Gets optional custom parameters for the forward passes of animation. */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @Nullable
        public AnimationParameters getForwardRepeatOverride() {
            if (mImpl.hasForwardRepeatOverride()) {
                return AnimationParameters.fromProto(mImpl.getForwardRepeatOverride());
            } else {
                return null;
            }
        }

        /** Gets optional custom parameters for the reverse passes of animation. */
        @RequiresSchemaVersion(major = 1, minor = 200)
        @Nullable
        public AnimationParameters getReverseRepeatOverride() {
            if (mImpl.hasReverseRepeatOverride()) {
                return AnimationParameters.fromProto(mImpl.getReverseRepeatOverride());
            } else {
                return null;
            }
        }

        /** Get the fingerprint for this object, or null if unknown. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @Nullable
        public Fingerprint getFingerprint() {
            return mFingerprint;
        }

        /** Creates a new wrapper instance from the proto. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public static Repeatable fromProto(
                @NonNull AnimationParameterProto.Repeatable proto,
                @Nullable Fingerprint fingerprint) {
            return new Repeatable(proto, fingerprint);
        }

        @NonNull
        static Repeatable fromProto(@NonNull AnimationParameterProto.Repeatable proto) {
            return fromProto(proto, null);
        }

        /** Returns the internal proto instance. */
        @RestrictTo(Scope.LIBRARY_GROUP)
        @NonNull
        public AnimationParameterProto.Repeatable toProto() {
            return mImpl;
        }

        static boolean isInfiniteIteration(int iteration) {
            return iteration < 1;
        }

        @Override
        @NonNull
        public String toString() {
            return "Repeatable{"
                    + "iterations="
                    + getIterations()
                    + ", repeatMode="
                    + getRepeatMode()
                    + ", forwardRepeatOverride="
                    + getForwardRepeatOverride()
                    + ", reverseRepeatOverride="
                    + getReverseRepeatOverride()
                    + "}";
        }

        /** Builder for {@link Repeatable} */
        public static final class Builder {
            private final AnimationParameterProto.Repeatable.Builder mImpl =
                    AnimationParameterProto.Repeatable.newBuilder();
            private final Fingerprint mFingerprint = new Fingerprint(2110475048);

            public Builder() {}

            /**
             * Sets the number specifying how many times animation will be repeated. If not set,
             * defaults to repeating infinitely.
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setIterations(@IntRange(from = 1) int iterations) {
                mImpl.setIterations(iterations);
                mFingerprint.recordPropertyUpdate(1, iterations);
                return this;
            }

            /**
             * Sets the repeat mode to specify how animation will behave when repeated. If not set,
             * defaults to restart.
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setRepeatMode(@RepeatMode int repeatMode) {
                mImpl.setRepeatMode(AnimationParameterProto.RepeatMode.forNumber(repeatMode));
                mFingerprint.recordPropertyUpdate(2, repeatMode);
                return this;
            }

            /**
             * Sets optional custom parameters for the forward passes of animation. If not set, use
             * the main animation parameters set outside of {@link Repeatable}.
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setForwardRepeatOverride(
                    @NonNull AnimationParameters forwardRepeatOverride) {
                mImpl.setForwardRepeatOverride(forwardRepeatOverride.toProto());
                mFingerprint.recordPropertyUpdate(
                        6,
                        checkNotNull(forwardRepeatOverride.getFingerprint()).aggregateValueAsInt());
                return this;
            }

            /**
             * Sets optional custom parameters for the reverse passes of animation. If not set, use
             * the main animation parameters set outside of {@link Repeatable}.
             */
            @RequiresSchemaVersion(major = 1, minor = 200)
            @NonNull
            public Builder setReverseRepeatOverride(
                    @NonNull AnimationParameters reverseRepeatOverride) {
                mImpl.setReverseRepeatOverride(reverseRepeatOverride.toProto());
                mFingerprint.recordPropertyUpdate(
                        7,
                        checkNotNull(reverseRepeatOverride.getFingerprint()).aggregateValueAsInt());
                return this;
            }

            /** Builds an instance from accumulated values. */
            @NonNull
            public Repeatable build() {
                return new Repeatable(mImpl.build(), mFingerprint);
            }
        }
    }
}