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)); } }