/* * 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 { 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 mAnimationFactory; private ArrayList 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 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 The object that each SpringAnimation is attached to. */ public interface AnimationFactory { /** * 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; } }