blob: ac864e91b3259608d163658a3af28e562443860f [file] [log] [blame]
package com.android.launcher3.util;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import com.android.launcher3.ButtonDropTarget;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.Launcher;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.dragndrop.DragView;
public class FlingAnimation implements AnimatorUpdateListener, Runnable {
/**
* Maximum acceleration in one dimension (pixels per milliseconds)
*/
private static final float MAX_ACCELERATION = 0.5f;
private static final int DRAG_END_DELAY = 300;
private final ButtonDropTarget mDropTarget;
private final Launcher mLauncher;
protected final DragObject mDragObject;
protected final DragOptions mDragOptions;
protected final DragLayer mDragLayer;
protected final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f);
protected final float mUX, mUY;
protected Rect mIconRect;
protected RectF mFrom;
protected int mDuration;
protected float mAnimationTimeFraction;
protected float mAX, mAY;
public FlingAnimation(DragObject d, PointF vel, ButtonDropTarget dropTarget, Launcher launcher,
DragOptions options) {
mDropTarget = dropTarget;
mLauncher = launcher;
mDragObject = d;
mUX = vel.x / 1000;
mUY = vel.y / 1000;
mDragLayer = mLauncher.getDragLayer();
mDragOptions = options;
}
@Override
public void run() {
mIconRect = mDropTarget.getIconRect(mDragObject);
mDragObject.dragView.cancelAnimation();
mDragObject.dragView.requestLayout();
// Initiate from
Rect from = new Rect();
mDragLayer.getViewRectRelativeToSelf(mDragObject.dragView, from);
mFrom = new RectF(from);
mFrom.inset(
((1 - mDragObject.dragView.getScaleX()) * from.width()) / 2f,
((1 - mDragObject.dragView.getScaleY()) * from.height()) / 2f);
mDuration = Math.abs(mUY) > Math.abs(mUX) ? initFlingUpDuration() : initFlingLeftDuration();
mAnimationTimeFraction = ((float) mDuration) / (mDuration + DRAG_END_DELAY);
final int duration = mDuration + DRAG_END_DELAY;
final long startTime = AnimationUtils.currentAnimationTimeMillis();
// NOTE: Because it takes time for the first frame of animation to actually be
// called and we expect the animation to be a continuation of the fling, we have
// to account for the time that has elapsed since the fling finished. And since
// we don't have a startDelay, we will always get call to update when we call
// start() (which we want to ignore).
final TimeInterpolator tInterpolator = new TimeInterpolator() {
private int mCount = -1;
private float mOffset = 0f;
@Override
public float getInterpolation(float t) {
if (mCount < 0) {
mCount++;
} else if (mCount == 0) {
mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
startTime) / duration);
mCount++;
}
return Math.min(1f, mOffset + t);
}
};
mDropTarget.onDrop(mDragObject, mDragOptions);
ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
anim.setDuration(duration).setInterpolator(tInterpolator);
anim.addUpdateListener(this);
anim.addListener(forEndCallback(() -> {
mLauncher.getStateManager().goToState(NORMAL);
mDropTarget.completeDrop(mDragObject);
}));
mDragLayer.playDropAnimation(mDragObject.dragView, anim, DragLayer.ANIMATION_END_DISAPPEAR);
}
/**
* The fling animation is based on the following system
* - Apply a constant force in the y direction to causing the fling to decelerate.
* - The animation runs for the time taken by the object to go out of the screen.
* - Calculate a constant acceleration in x direction such that the object reaches
* {@link #mIconRect} in the given time.
*/
protected int initFlingUpDuration() {
float sY = -mFrom.bottom;
float d = mUY * mUY + 2 * sY * MAX_ACCELERATION;
if (d >= 0) {
// sY can be reached under the MAX_ACCELERATION. Use MAX_ACCELERATION for y direction.
mAY = MAX_ACCELERATION;
} else {
// sY is not reachable, decrease the acceleration so that sY is almost reached.
d = 0;
mAY = mUY * mUY / (2 * -sY);
}
double t = (-mUY - Math.sqrt(d)) / mAY;
float sX = -mFrom.centerX() + mIconRect.exactCenterX();
// Find horizontal acceleration such that: u*t + a*t*t/2 = s
mAX = (float) ((sX - t * mUX) * 2 / (t * t));
return (int) Math.round(t);
}
/**
* The fling animation is based on the following system
* - Apply a constant force in the x direction to causing the fling to decelerate.
* - The animation runs for the time taken by the object to go out of the screen.
* - Calculate a constant acceleration in y direction such that the object reaches
* {@link #mIconRect} in the given time.
*/
protected int initFlingLeftDuration() {
float sX = -mFrom.right;
float d = mUX * mUX + 2 * sX * MAX_ACCELERATION;
if (d >= 0) {
// sX can be reached under the MAX_ACCELERATION. Use MAX_ACCELERATION for x direction.
mAX = MAX_ACCELERATION;
} else {
// sX is not reachable, decrease the acceleration so that sX is almost reached.
d = 0;
mAX = mUX * mUX / (2 * -sX);
}
double t = (-mUX - Math.sqrt(d)) / mAX;
float sY = -mFrom.centerY() + mIconRect.exactCenterY();
// Find vertical acceleration such that: u*t + a*t*t/2 = s
mAY = (float) ((sY - t * mUY) * 2 / (t * t));
return (int) Math.round(t);
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float t = animation.getAnimatedFraction();
if (t > mAnimationTimeFraction) {
t = 1;
} else {
t = t / mAnimationTimeFraction;
}
final DragView dragView = (DragView) mDragLayer.getAnimatedView();
final float time = t * mDuration;
dragView.setTranslationX(time * mUX + mFrom.left + mAX * time * time / 2);
dragView.setTranslationY(time * mUY + mFrom.top + mAY * time * time / 2);
dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t));
}
}