/*
* Copyright 2017 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 com.google.android.material.animation;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.Log;
import android.util.Property;
import androidx.annotation.AnimatorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleableRes;
import androidx.collection.SimpleArrayMap;
import java.util.ArrayList;
import java.util.List;
/**
* A motion spec contains multiple named {@link MotionTiming motion timings}.
*
*
Inflate an instance of MotionSpec from XML by creating a Property
* Animation resource in {@code res/animator}. The file must contain an {@code }
* or a {@code } of object animators.
*
* This class will store a map of String keys to MotionTiming values. Each animator's {@code
* android:propertyName} attribute will be used as the key, while the other attributes {@code
* android:startOffset}, {@code android:duration}, {@code android:interpolator}, {@code
* android:repeatCount}, and {@code android:repeatMode} will be used to create the MotionTiming
* instance.
*
*
A motion spec resource can either be an <objectAnimator> or a <set> of multiple
* <objectAnimator>.
*
*
{@code
*
*
*
*
* }
*/
public class MotionSpec {
private static final String TAG = "MotionSpec";
private final SimpleArrayMap timings = new SimpleArrayMap<>();
private final SimpleArrayMap propertyValues =
new SimpleArrayMap<>();
/** Returns whether this motion spec contains a MotionTiming with the given name. */
public boolean hasTiming(String name) {
return timings.get(name) != null;
}
/**
* Returns the MotionTiming with the given name, or throws IllegalArgumentException if it does not
* exist.
*/
public MotionTiming getTiming(String name) {
if (!hasTiming(name)) {
throw new IllegalArgumentException();
}
return timings.get(name);
}
/** Sets a MotionTiming with the given name. */
public void setTiming(String name, @Nullable MotionTiming timing) {
timings.put(name, timing);
}
/**
* Returns whether this motion spec contains a {@link PropertyValuesHolder[]} with the given name.
*/
public boolean hasPropertyValues(String name) {
return propertyValues.get(name) != null;
}
/**
* Get values for a property in this MotionSpec.
*
* @param name Name of the property to get values for, e.g. "width" or "opacity".
* @return Array of {@link PropertyValuesHolder} values for the property.
*/
@NonNull
public PropertyValuesHolder[] getPropertyValues(String name) {
if (!hasPropertyValues(name)) {
throw new IllegalArgumentException();
}
return clonePropertyValuesHolder(propertyValues.get(name));
}
/**
* Set values for a property in this MotionSpec.
*
* @param name Name of the property to set values for, e.g. "width" or "opacity".
* @param values Array of {@link PropertyValuesHolder} values for the property.
*/
public void setPropertyValues(String name, PropertyValuesHolder[] values) {
propertyValues.put(name, values);
}
@NonNull
private PropertyValuesHolder[] clonePropertyValuesHolder(@NonNull PropertyValuesHolder[] values) {
PropertyValuesHolder[] ret = new PropertyValuesHolder[values.length];
for (int i = 0; i < values.length; i++) {
ret[i] = values[i].clone();
}
return ret;
}
/**
* Creates and returns an {@link ObjectAnimator} that animates the given property. This can be
* added to an {@link AnimatorSet} to play multiple synchronized animations.
*
* @param name Name of the property to be animated.
* @param target The target whose property is to be animated. See {@link
* ObjectAnimator#ofPropertyValuesHolder(T, PropertyValuesHolder...)} for more details.
* @param property The {@link Property} object being animated.
* @return An {@link ObjectAnimator} which animates the given property.
*/
@NonNull
public ObjectAnimator getAnimator(
@NonNull String name, @NonNull T target, @NonNull Property property) {
ObjectAnimator animator =
ObjectAnimator.ofPropertyValuesHolder(target, getPropertyValues(name));
animator.setProperty(property);
getTiming(name).apply(animator);
return animator;
}
/**
* Returns the total duration of this motion spec, which is the maximum delay+duration of its
* motion timings.
*/
public long getTotalDuration() {
long duration = 0;
for (int i = 0, count = timings.size(); i < count; i++) {
MotionTiming timing = timings.valueAt(i);
duration = Math.max(duration, timing.getDelay() + timing.getDuration());
}
return duration;
}
/**
* Inflates an instance of MotionSpec from the animator resource indexed in the given attributes
* array.
*/
@Nullable
public static MotionSpec createFromAttribute(
@NonNull Context context, @NonNull TypedArray attributes, @StyleableRes int index) {
if (attributes.hasValue(index)) {
int resourceId = attributes.getResourceId(index, 0);
if (resourceId != 0) {
return createFromResource(context, resourceId);
}
}
return null;
}
/** Inflates an instance of MotionSpec from the given animator resource. */
@Nullable
public static MotionSpec createFromResource(@NonNull Context context, @AnimatorRes int id) {
try {
Animator animator = AnimatorInflater.loadAnimator(context, id);
if (animator instanceof AnimatorSet) {
AnimatorSet set = (AnimatorSet) animator;
return createSpecFromAnimators(set.getChildAnimations());
} else if (animator != null) {
List animators = new ArrayList<>();
animators.add(animator);
return createSpecFromAnimators(animators);
} else {
return null;
}
} catch (Exception e) {
Log.w(TAG, "Can't load animation resource ID #0x" + Integer.toHexString(id), e);
return null;
}
}
@NonNull
private static MotionSpec createSpecFromAnimators(@NonNull List animators) {
MotionSpec spec = new MotionSpec();
for (int i = 0, count = animators.size(); i < count; i++) {
addInfoFromAnimator(spec, animators.get(i));
}
return spec;
}
private static void addInfoFromAnimator(@NonNull MotionSpec spec, Animator animator) {
if (animator instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) animator;
spec.setPropertyValues(anim.getPropertyName(), anim.getValues());
spec.setTiming(anim.getPropertyName(), MotionTiming.createFromAnimator(anim));
} else {
throw new IllegalArgumentException("Animator must be an ObjectAnimator: " + animator);
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MotionSpec)) {
return false;
}
MotionSpec that = (MotionSpec) o;
return timings.equals(that.timings);
}
@Override
public int hashCode() {
return timings.hashCode();
}
@NonNull
@Override
public String toString() {
StringBuilder out = new StringBuilder();
out.append('\n');
out.append(getClass().getName());
out.append('{');
out.append(Integer.toHexString(System.identityHashCode(this)));
out.append(" timings: ");
out.append(timings);
out.append("}\n");
return out.toString();
}
}