1 package com.android.launcher3.util;
2 
3 import static com.android.launcher3.LauncherState.NORMAL;
4 import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
5 
6 import android.animation.TimeInterpolator;
7 import android.animation.ValueAnimator;
8 import android.animation.ValueAnimator.AnimatorUpdateListener;
9 import android.graphics.PointF;
10 import android.graphics.Rect;
11 import android.graphics.RectF;
12 import android.view.animation.AnimationUtils;
13 import android.view.animation.DecelerateInterpolator;
14 
15 import com.android.launcher3.ButtonDropTarget;
16 import com.android.launcher3.DropTarget.DragObject;
17 import com.android.launcher3.Launcher;
18 import com.android.launcher3.dragndrop.DragLayer;
19 import com.android.launcher3.dragndrop.DragOptions;
20 import com.android.launcher3.dragndrop.DragView;
21 
22 public class FlingAnimation implements AnimatorUpdateListener, Runnable {
23 
24     /**
25      * Maximum acceleration in one dimension (pixels per milliseconds)
26      */
27     private static final float MAX_ACCELERATION = 0.5f;
28     private static final int DRAG_END_DELAY = 300;
29 
30     private final ButtonDropTarget mDropTarget;
31     private final Launcher mLauncher;
32 
33     protected final DragObject mDragObject;
34     protected final DragOptions mDragOptions;
35     protected final DragLayer mDragLayer;
36     protected final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(0.75f);
37     protected final float mUX, mUY;
38 
39     protected Rect mIconRect;
40     protected RectF mFrom;
41     protected int mDuration;
42     protected float mAnimationTimeFraction;
43 
44     protected float mAX, mAY;
45 
FlingAnimation(DragObject d, PointF vel, ButtonDropTarget dropTarget, Launcher launcher, DragOptions options)46     public FlingAnimation(DragObject d, PointF vel, ButtonDropTarget dropTarget, Launcher launcher,
47             DragOptions options) {
48         mDropTarget = dropTarget;
49         mLauncher = launcher;
50         mDragObject = d;
51         mUX = vel.x / 1000;
52         mUY = vel.y / 1000;
53         mDragLayer = mLauncher.getDragLayer();
54         mDragOptions = options;
55     }
56 
57     @Override
run()58     public void run() {
59         mIconRect = mDropTarget.getIconRect(mDragObject);
60         mDragObject.dragView.cancelAnimation();
61         mDragObject.dragView.requestLayout();
62 
63         // Initiate from
64         Rect from = new Rect();
65         mDragLayer.getViewRectRelativeToSelf(mDragObject.dragView, from);
66 
67         mFrom = new RectF(from);
68         mFrom.inset(
69                 ((1 - mDragObject.dragView.getScaleX()) * from.width()) / 2f,
70                 ((1 - mDragObject.dragView.getScaleY()) * from.height()) / 2f);
71         mDuration = Math.abs(mUY) > Math.abs(mUX) ? initFlingUpDuration() : initFlingLeftDuration();
72 
73         mAnimationTimeFraction = ((float) mDuration) / (mDuration + DRAG_END_DELAY);
74 
75         final int duration = mDuration + DRAG_END_DELAY;
76         final long startTime = AnimationUtils.currentAnimationTimeMillis();
77 
78         // NOTE: Because it takes time for the first frame of animation to actually be
79         // called and we expect the animation to be a continuation of the fling, we have
80         // to account for the time that has elapsed since the fling finished.  And since
81         // we don't have a startDelay, we will always get call to update when we call
82         // start() (which we want to ignore).
83         final TimeInterpolator tInterpolator = new TimeInterpolator() {
84             private int mCount = -1;
85             private float mOffset = 0f;
86 
87             @Override
88             public float getInterpolation(float t) {
89                 if (mCount < 0) {
90                     mCount++;
91                 } else if (mCount == 0) {
92                     mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
93                             startTime) / duration);
94                     mCount++;
95                 }
96                 return Math.min(1f, mOffset + t);
97             }
98         };
99 
100         mDropTarget.onDrop(mDragObject, mDragOptions);
101         ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
102         anim.setDuration(duration).setInterpolator(tInterpolator);
103         anim.addUpdateListener(this);
104         anim.addListener(forEndCallback(() -> {
105             mLauncher.getStateManager().goToState(NORMAL);
106             mDropTarget.completeDrop(mDragObject);
107         }));
108         mDragLayer.playDropAnimation(mDragObject.dragView, anim, DragLayer.ANIMATION_END_DISAPPEAR);
109     }
110 
111     /**
112      * The fling animation is based on the following system
113      *   - Apply a constant force in the y direction to causing the fling to decelerate.
114      *   - The animation runs for the time taken by the object to go out of the screen.
115      *   - Calculate a constant acceleration in x direction such that the object reaches
116      *     {@link #mIconRect} in the given time.
117      */
initFlingUpDuration()118     protected int initFlingUpDuration() {
119         float sY = -mFrom.bottom;
120 
121         float d = mUY * mUY + 2 * sY * MAX_ACCELERATION;
122         if (d >= 0) {
123             // sY can be reached under the MAX_ACCELERATION. Use MAX_ACCELERATION for y direction.
124             mAY = MAX_ACCELERATION;
125         } else {
126             // sY is not reachable, decrease the acceleration so that sY is almost reached.
127             d = 0;
128             mAY = mUY * mUY / (2 * -sY);
129         }
130         double t = (-mUY - Math.sqrt(d)) / mAY;
131 
132         float sX = -mFrom.centerX() + mIconRect.exactCenterX();
133 
134         // Find horizontal acceleration such that: u*t + a*t*t/2 = s
135         mAX = (float) ((sX - t * mUX) * 2 / (t * t));
136         return (int) Math.round(t);
137     }
138 
139     /**
140      * The fling animation is based on the following system
141      *   - Apply a constant force in the x direction to causing the fling to decelerate.
142      *   - The animation runs for the time taken by the object to go out of the screen.
143      *   - Calculate a constant acceleration in y direction such that the object reaches
144      *     {@link #mIconRect} in the given time.
145      */
initFlingLeftDuration()146     protected int initFlingLeftDuration() {
147         float sX = -mFrom.right;
148 
149         float d = mUX * mUX + 2 * sX * MAX_ACCELERATION;
150         if (d >= 0) {
151             // sX can be reached under the MAX_ACCELERATION. Use MAX_ACCELERATION for x direction.
152             mAX = MAX_ACCELERATION;
153         } else {
154             // sX is not reachable, decrease the acceleration so that sX is almost reached.
155             d = 0;
156             mAX = mUX * mUX / (2 * -sX);
157         }
158         double t = (-mUX - Math.sqrt(d)) / mAX;
159 
160         float sY = -mFrom.centerY() + mIconRect.exactCenterY();
161 
162         // Find vertical acceleration such that: u*t + a*t*t/2 = s
163         mAY = (float) ((sY - t * mUY) * 2 / (t * t));
164         return (int) Math.round(t);
165     }
166 
167     @Override
onAnimationUpdate(ValueAnimator animation)168     public void onAnimationUpdate(ValueAnimator animation) {
169         float t = animation.getAnimatedFraction();
170         if (t > mAnimationTimeFraction) {
171             t = 1;
172         } else {
173             t = t / mAnimationTimeFraction;
174         }
175         final DragView dragView = (DragView) mDragLayer.getAnimatedView();
176         final float time = t * mDuration;
177         dragView.setTranslationX(time * mUX + mFrom.left + mAX * time * time / 2);
178         dragView.setTranslationY(time * mUY + mFrom.top + mAY * time * time / 2);
179         dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t));
180     }
181 }
182