/*
* Copyright 2016-present The Material Motion Authors. All Rights Reserved.
*
* 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.motion.sources;
import android.annotation.SuppressLint;
import android.support.animation.DynamicAnimation;
import android.support.animation.DynamicAnimation.ViewProperty;
import android.support.animation.SpringAnimation;
import android.support.animation.SpringForce;
import android.support.v4.util.SimpleArrayMap;
import android.util.Property;
import android.view.View;
import com.google.android.indefinite.observable.IndefiniteObservable.Subscription;
import com.google.android.material.motion.MotionBuilder;
import com.google.android.material.motion.MotionObserver;
import com.google.android.material.motion.MotionObserver.SimpleMotionObserver;
import com.google.android.material.motion.ReactiveProperty;
import com.google.android.material.motion.ReactiveProperty.PropertyReactiveProperty;
import com.google.android.material.motion.interactions.MaterialSpring;
import com.google.android.material.motion.properties.ViewProperties;
import com.google.android.material.motion.properties.ViewProperties.DerivativeProperty;
import java.util.HashSet;
import java.util.Set;
/**
* A source for physics springs.
*/
public final class DynamicSpringSource<T> extends SpringSource<T> {
public static final System SYSTEM = new System() {
@Override
public <T> SpringSource<T> create(MaterialSpring<?, T> spring) {
return new DynamicSpringSource<>(spring);
}
};
private final MaterialSpring<?, T> interaction;
private final DynamicSpringBuilder<T> builder;
private final Set<MotionObserver<T>> observers = new HashSet<>();
private Subscription destinationSubscription;
private Subscription frictionSubscription;
private Subscription tensionSubscription;
private boolean initialized;
public DynamicSpringSource(MaterialSpring<?, T> interaction) {
super(interaction);
this.interaction = interaction;
builder = new DynamicSpringBuilder<>(interaction);
}
@Override
protected void onConnect(MotionObserver<T> observer) {
observers.add(observer);
}
@Override
protected void onEnable() {
initialized = false;
tensionSubscription = interaction.tension.subscribe(new SimpleMotionObserver<Float>() {
@Override
public void next(Float value) {
startBuild();
}
});
frictionSubscription = interaction.friction.subscribe(new SimpleMotionObserver<Float>() {
@Override
public void next(Float value) {
startBuild();
}
});
destinationSubscription = interaction.destination.subscribe(new SimpleMotionObserver<T>() {
@Override
public void next(T value) {
startBuild();
}
});
initialized = true;
startBuild();
}
private void startBuild() {
if (initialized) {
for (MotionObserver<T> observer : observers) {
//noinspection unchecked
observer.build(
builder,
interaction.initialValue.read(),
interaction.initialVelocity.read(),
interaction.destination.read());
}
}
}
@Override
protected void onDisable() {
if (tensionSubscription != null) {
tensionSubscription.unsubscribe();
frictionSubscription.unsubscribe();
destinationSubscription.unsubscribe();
}
builder.stop();
}
@Override
protected void onDisconnect(MotionObserver<T> observer) {
observers.remove(observer);
}
private static class DynamicSpringBuilder<T> extends MotionBuilder<T> {
private final MaterialSpring<?, T> interaction;
private final SimpleArrayMap<Key, SpringAnimation> animations = new SimpleArrayMap<>();
public DynamicSpringBuilder(MaterialSpring<?, T> interaction) {
this.interaction = interaction;
}
@Override
public void start(ReactiveProperty<T> property, T[] values) {
float[] initialValues = new float[interaction.vectorizer.getVectorLength()];
float[] initialVelocities = new float[interaction.vectorizer.getVectorLength()];
float[] destinations = new float[interaction.vectorizer.getVectorLength()];
interaction.vectorizer.vectorize(values[0], initialValues);
interaction.vectorizer.vectorize(values[1], initialVelocities);
interaction.vectorizer.vectorize(values[2], destinations);
startAnimations(
(PropertyReactiveProperty<View, T>) property,
initialValues,
initialVelocities,
destinations);
}
private void startAnimations(
PropertyReactiveProperty<View, T> reactiveProperty,
float[] initialValues,
float[] initialVelocities,
float[] destinations) {
View target = reactiveProperty.target;
Property<View, T> property = reactiveProperty.property;
if (property instanceof DerivativeProperty) {
DerivativeProperty<View, T> derivative = (DerivativeProperty<View, T>) property;
for (int i = 0; i < derivative.vectorizer.getVectorLength(); i++) {
startAnimation(
target,
derivative.properties[i],
derivative.setterTransformation(target, initialValues[i]),
derivative.setterTransformation(target, initialVelocities[i]),
derivative.setterTransformation(target, destinations[i]));
}
} else if (property.getType() == Float.class) {
//noinspection unchecked
startAnimation(
target,
(Property<View, Float>) property,
initialValues[0],
initialVelocities[0],
destinations[0]);
} else {
throw new IllegalArgumentException("Property not supported: " + property);
}
}
@SuppressLint("NewApi")
private void startAnimation(
View target,
Property<View, Float> property,
float initialValue,
float initialVelocity,
float destination) {
ViewProperty viewProperty;
if (property == View.TRANSLATION_X) {
viewProperty = DynamicAnimation.TRANSLATION_X;
} else if (property == View.TRANSLATION_Y) {
viewProperty = DynamicAnimation.TRANSLATION_Y;
} else if (property == View.TRANSLATION_Z) {
viewProperty = DynamicAnimation.TRANSLATION_Z;
} else if (property == View.SCALE_X) {
viewProperty = DynamicAnimation.SCALE_X;
} else if (property == View.SCALE_Y) {
viewProperty = DynamicAnimation.SCALE_Y;
} else if (property == View.ROTATION) {
viewProperty = DynamicAnimation.ROTATION;
} else if (property == View.ROTATION_X) {
viewProperty = DynamicAnimation.ROTATION_X;
} else if (property == View.ROTATION_Y) {
viewProperty = DynamicAnimation.ROTATION_Y;
} else if (property == View.X) {
viewProperty = DynamicAnimation.X;
} else if (property == View.Y) {
viewProperty = DynamicAnimation.Y;
} else if (property == View.Z) {
viewProperty = DynamicAnimation.Z;
} else if (property == View.ALPHA) {
viewProperty = DynamicAnimation.ALPHA;
} else if (property == ViewProperties.SCROLL_X) {
viewProperty = DynamicAnimation.SCROLL_X;
} else if (property == ViewProperties.SCROLL_Y) {
viewProperty = DynamicAnimation.SCROLL_Y;
} else {
throw new IllegalArgumentException("Property not supported: " + property);
}
Key key = new Key(target, property);
SpringAnimation animation = animations.get(key);
if (animation == null) {
animation = new SpringAnimation(target, viewProperty);
animation.setStartValue(initialValue);
animation.setStartVelocity(initialVelocity);
}
float stiffness = stiffnessFromOrigamiValue(interaction.tension.read());
float dampingRatio = dampingRatioFromOrigamiValue(stiffness, interaction.friction.read());
SpringForce force = new SpringForce(destination);
force.setStiffness(stiffness);
force.setDampingRatio(dampingRatio);
animation.setSpring(force);
animation.animateToFinalPosition(destination);
animations.put(key, animation);
}
@Override
public void stop() {
if (animations.isEmpty()) {
return;
}
for (int i = 0, count = animations.size(); i < count; i++) {
animations.valueAt(i).cancel();
}
animations.clear();
}
public static float stiffnessFromOrigamiValue(float value) {
return value == 0f ? 0f : (value - 30f) * 3.62f + 194f;
}
public static float dampingRatioFromOrigamiValue(float stiffness, float value) {
float friction = value == 0f ? 0f : (value - 8f) * 3f + 25f;
return (float) (friction / (2 * Math.sqrt(stiffness)));
}
private static class Key {
private View target;
private Property<View, Float> property;
public Key(View target, Property<View, Float> property) {
this.target = target;
this.property = property;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
if (!target.equals(key.target)) return false;
return property.equals(key.property);
}
@Override
public int hashCode() {
int result = target.hashCode();
result = 31 * result + property.hashCode();
return result;
}
}
}
}