blob: eec3a48ee3d82d24fd634b6139691ba3d56c6163 [file] [log] [blame]
/*
* Copyright (C) 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.android.launcher3.anim;
import android.support.animation.FloatPropertyCompat;
import android.support.animation.SpringAnimation;
import android.support.animation.SpringForce;
import android.support.annotation.IntDef;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import com.android.launcher3.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
/**
* Handler class that manages springs for a set of views that should all move based on the same
* {@link MotionEvent}s.
*
* Supports setting either X or Y velocity on the list of springs added to this handler.
*/
public class SpringAnimationHandler<T> {
private static final String TAG = "SpringAnimationHandler";
private static final boolean DEBUG = false;
private static final float VELOCITY_DAMPING_FACTOR = 0.175f;
@Retention(RetentionPolicy.SOURCE)
@IntDef({Y_DIRECTION, X_DIRECTION})
public @interface Direction {}
public static final int Y_DIRECTION = 0;
public static final int X_DIRECTION = 1;
private int mVelocityDirection;
private VelocityTracker mVelocityTracker;
private float mCurrentVelocity = 0;
private boolean mShouldComputeVelocity = false;
private AnimationFactory<T> mAnimationFactory;
private ArrayList<SpringAnimation> mAnimations = new ArrayList<>();
/**
* @param direction Either {@link #X_DIRECTION} or {@link #Y_DIRECTION}.
* Determines which direction we use to calculate and set the velocity.
* @param factory The AnimationFactory is responsible for initializing and updating the
* SpringAnimations added to this class.
*/
public SpringAnimationHandler(@Direction int direction, AnimationFactory<T> factory) {
mVelocityDirection = direction;
mAnimationFactory = factory;
}
/**
* Adds a spring to the list of springs handled by this class.
* @param spring The new spring to be added.
* @param setDefaultValues If True, sets the spring to the default
* {@link AnimationFactory} values.
*/
public void add(SpringAnimation spring, boolean setDefaultValues) {
if (setDefaultValues) {
mAnimationFactory.setDefaultValues(spring);
}
spring.setStartVelocity(mCurrentVelocity);
mAnimations.add(spring);
}
/**
* Adds a new or recycled animation to the list of springs handled by this class.
*
* @param view The view the spring is attached to.
* @param object Used to initialize and update the spring.
*/
public void add(View view, T object) {
SpringAnimation spring = (SpringAnimation) view.getTag(R.id.spring_animation_tag);
if (spring == null) {
spring = mAnimationFactory.initialize(object);
view.setTag(R.id.spring_animation_tag, spring);
}
mAnimationFactory.update(spring, object);
add(spring, false /* setDefaultValues */);
}
/**
* Stops and removes the spring attached to {@param view}.
*/
public void remove(View view) {
remove((SpringAnimation) view.getTag(R.id.spring_animation_tag));
}
public void remove(SpringAnimation animation) {
if (animation.canSkipToEnd()) {
animation.skipToEnd();
}
mAnimations.remove(animation);
}
public void addMovement(MotionEvent event) {
int action = event.getActionMasked();
if (DEBUG) Log.d(TAG, "addMovement#action=" + action);
switch (action) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_DOWN:
reset();
break;
}
getVelocityTracker().addMovement(event);
mShouldComputeVelocity = true;
}
public void animateToFinalPosition(float position, int startValue) {
animateToFinalPosition(position, startValue, mShouldComputeVelocity);
}
/**
* @param position The final animation position.
* @param startValue < 0 if scrolling from start to end; > 0 if scrolling from end to start
* The magnitude of the number changes how the spring will move.
* @param setVelocity If true, we set the velocity to {@link #mCurrentVelocity} before
* starting the animation.
*/
private void animateToFinalPosition(float position, int startValue, boolean setVelocity) {
if (DEBUG) {
Log.d(TAG, "animateToFinalPosition#position=" + position + ", startValue=" + startValue);
}
if (mShouldComputeVelocity) {
mCurrentVelocity = computeVelocity();
}
int size = mAnimations.size();
for (int i = 0; i < size; ++i) {
mAnimations.get(i).setStartValue(startValue);
if (setVelocity) {
mAnimations.get(i).setStartVelocity(mCurrentVelocity);
}
mAnimations.get(i).animateToFinalPosition(position);
}
reset();
}
/**
* Similar to {@link #animateToFinalPosition(float, int)}, but used in cases where we want to
* manually set the velocity.
*/
public void animateToPositionWithVelocity(float position, int startValue, float velocity) {
if (DEBUG) {
Log.d(TAG, "animateToPosition#pos=" + position + ", start=" + startValue
+ ", velocity=" + velocity);
}
mCurrentVelocity = velocity;
mShouldComputeVelocity = false;
animateToFinalPosition(position, startValue, true);
}
public boolean isRunning() {
// All the animations run at the same time so we can just check the first one.
return !mAnimations.isEmpty() && mAnimations.get(0).isRunning();
}
public void skipToEnd() {
if (DEBUG) Log.d(TAG, "setStartVelocity#skipToEnd");
if (DEBUG) Log.v(TAG, "setStartVelocity#skipToEnd", new Exception());
int size = mAnimations.size();
for (int i = 0; i < size; ++i) {
if (mAnimations.get(i).canSkipToEnd()) {
mAnimations.get(i).skipToEnd();
}
}
}
public void reset() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mCurrentVelocity = 0;
mShouldComputeVelocity = false;
}
private float computeVelocity() {
getVelocityTracker().computeCurrentVelocity(1000 /* millis */);
float velocity = isVerticalDirection()
? getVelocityTracker().getYVelocity()
: getVelocityTracker().getXVelocity();
velocity *= VELOCITY_DAMPING_FACTOR;
if (DEBUG) Log.d(TAG, "computeVelocity=" + velocity);
return velocity;
}
private boolean isVerticalDirection() {
return mVelocityDirection == Y_DIRECTION;
}
private VelocityTracker getVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
return mVelocityTracker;
}
/**
* This interface is used to initialize and update the SpringAnimations added to the
* {@link SpringAnimationHandler}.
*
* @param <T> The object that each SpringAnimation is attached to.
*/
public interface AnimationFactory<T> {
/**
* Initializes a new Spring for {@param object}.
*/
SpringAnimation initialize(T object);
/**
* Updates the value of {@param spring} based on {@param object}.
*/
void update(SpringAnimation spring, T object);
/**
* Sets the factory default values for the given {@param spring}.
*/
void setDefaultValues(SpringAnimation spring);
}
/**
* Helper method to create a new SpringAnimation for {@param view}.
*/
public static SpringAnimation forView(View view, FloatPropertyCompat property, float finalPos) {
SpringAnimation spring = new SpringAnimation(view, property, finalPos);
spring.setSpring(new SpringForce(finalPos));
return spring;
}
}